package com.appolica.tabcontroller;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import com.appolica.tabcontroller.listener.OnFragmentChangeListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* The core of the library. This class controls the switching between fragments.
*/
public class TabController {
private static final String TAG = "TabController";
private static final String BUNDLE_KEY = "bundle-key-tab-controller";
private final FragmentManager fragmentManager;
private final int containerId;
private ShowHideHandler showHideHandler;
private OnFragmentChangeListener changeListener;
/**
* Create a new instance of {@link TabController} with the default {@link ShowHideHandler}.
*
* @param fragmentManager The support fragment manager, used to switch between fragments.
* @param containerId The view id of the container for your fragments.
*
* @see ShowHideHandler
*/
public TabController(FragmentManager fragmentManager, int containerId) {
this(fragmentManager, containerId, new ShowHideFrHandler());
}
/**
* Create a new instance of {@link TabController}.
*
* @param fragmentManager The support fragment manager, used to switch between fragments.
* @param containerId The view id of the container for your fragments.
* @param showHideHandler The {@link ShowHideHandler} which determines how your fragments
* are going to be shown/hidden.
*
* @see ShowHideHandler
*/
public TabController(FragmentManager fragmentManager, int containerId, ShowHideHandler showHideHandler) {
this.fragmentManager = fragmentManager;
FragmentManager.enableDebugLogging(false);
this.containerId = containerId;
this.showHideHandler = showHideHandler;
}
/**
* Show the given fragment in the container, provided to the constructor. If
* FragmentManager::findFragmentByTag returns null for the tag, given by the provider,
* your fragment's instance will be obtained by calling FragmentProvider::getInstance.
* Otherwise it will be reused.
*
* If there is already a visible fragment, it will be hidden. How fragments are shown/hidden
* depends on {@link ShowHideHandler}.
*
* @param provider The {@link FragmentProvider} for the fragment you want to show.
*
* @see FragmentProvider
*/
public void switchTo(FragmentProvider provider) {
List<Notifier> notifiers = new ArrayList<>();
inTransaction(transaction -> {
final Fragment fragmentToShow = fragmentManager.findFragmentByTag(provider.getTag());
if (fragmentToShow != null) {
if (!showHideHandler.isVisible(fragmentToShow)) {
//Fragment is active but not visible
hideVisibleFragment(transaction);
showFragment(fragmentToShow, transaction);
notifiers.add(listener -> listener.onFragmentShown(provider, fragmentToShow));
} else {
//Fragment is already visible on the screen
notifiers.add(listener -> listener.onFragmentAlreadyVisible(provider, fragmentToShow));
}
} else {
//Fragment does not exist
hideVisibleFragment(transaction);
Fragment addedFragment = addToFragmentManager(provider, transaction);
notifiers.add(listener -> listener.onFragmentCreated(provider, addedFragment));
notifiers.add(listener -> listener.onFragmentShown(provider, addedFragment));
}
return notifiers;
});
}
private void hide(FragmentProvider currentFragment) {
final Fragment visibleFragment = fragmentManager.findFragmentByTag(currentFragment.getTag());
if (visibleFragment != null) {
inTransaction(transaction -> {
showHideHandler.hide(transaction, visibleFragment);
return new ArrayList<>();
});
}
}
private void showFragment(Fragment fragment, FragmentTransaction transaction) {
showHideHandler.show(transaction, fragment);
}
private Fragment addToFragmentManager(FragmentProvider fragmentType, FragmentTransaction transaction) {
Fragment fragment = fragmentType.getInstance();
transaction.add(containerId, fragment, fragmentType.getTag());
return fragment;
}
private void hideVisibleFragment(FragmentTransaction fragmentTransaction) {
final Fragment visibleFragment = getVisibleFragment();
if (visibleFragment != null) {
showHideHandler.hide(fragmentTransaction, visibleFragment);
}
}
/**
* Iterates through the fragments, returned by FragmentManager::getFragments.
*
* @return The first visible fragment found in the list or null.
*/
@Nullable
public Fragment getVisibleFragment() {
final List<Fragment> fragments = getFMFragments();
for (Fragment fragment : fragments) {
if (showHideHandler.isVisible(fragment)) {
return fragment;
}
}
return null;
}
/**
* Find a fragment by it's {@link FragmentProvider}. Same as calling
* fragmentManager.findFragmentByTag(fragmentProvider.getTag());
*
* @param fragmentProvider The fragment provider of the fragment you want to obtain.
* @return The fragment which the given {@link FragmentProvider} is for.
*/
@Nullable
public Fragment getFragment(@NonNull FragmentProvider fragmentProvider) {
return fragmentManager.findFragmentByTag(fragmentProvider.getTag());
}
/**
* Restore the state of the {@link TabController}. This will show the last visible fragment
* before saving the state.
* <br>
* Make sure you call {@link #save(Bundle)} when saving your instance
* state.
*
* @param savedInstanceState The saved instance state Bundle where the TabController state
* wil be obtained from.
*/
public void restore(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
final Bundle controllerState = savedInstanceState.getBundle(BUNDLE_KEY);
if (controllerState == null) {
throw new IllegalStateException("TabController's bundle not found in savedInstanceState. Did you call TabController::save in onSaveInstanceState(outState)?");
}
final List<Fragment> fragments = getFMFragments();
inTransaction(transaction -> {
for (Fragment fragment : fragments) {
showHideHandler.restore(controllerState, transaction, fragment);
}
return new ArrayList<>();
});
}
}
/**
* Save the state of the {@link TabController} in order to be able to restore your last visible
* fragment when your app restores.
* <br>
* Make sure you call {@link #restore(Bundle)} when restoring your instance state.
* @param savedInstanceState The output Bundle where the state will be saved to.
*/
public void save(Bundle savedInstanceState) {
final Bundle controllerState = new Bundle();
final List<Fragment> fragments = getFMFragments();
for (Fragment fragment : fragments) {
showHideHandler.save(controllerState, fragment);
}
savedInstanceState.putBundle(BUNDLE_KEY, controllerState);
}
private void inTransaction(@NonNull TransactionBody body) {
inTransaction(body, FragmentTransaction::commitNow);
}
private void inAsyncTransaction(@NonNull TransactionBody body) {
inTransaction(body, FragmentTransaction::commit);
}
private void inTransaction(@NonNull TransactionBody body, @NonNull TransactionCommitter committer) {
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setAllowOptimization(false);
List<Notifier> notifiers = body.runInTransaction(transaction);
committer.commitTransaction(transaction);
if (changeListener != null)
for (Notifier notifier : notifiers) {
notifier.notifyListener(changeListener);
}
}
/**
* Set listener to be notified on one of the {@link TabController}'s events.
*
* @param changeListener Your implementation of the listener.
*
* @see OnFragmentChangeListener
*/
public void setChangeListener(OnFragmentChangeListener changeListener) {
this.changeListener = changeListener;
}
@NonNull
private List<Fragment> getFMFragments() {
final List<Fragment> fmList = fragmentManager.getFragments();
final List<Fragment> resultList = new ArrayList<>();
if (fmList != null) {
resultList.addAll(fmList);
final Iterator<Fragment> iterator = resultList.iterator();
while (iterator.hasNext()) {
if (iterator.next() == null) {
iterator.remove();
}
}
}
return resultList;
}
private static interface TransactionBody {
@NonNull
public List<Notifier> runInTransaction(FragmentTransaction fragmentTransaction);
}
private static interface TransactionCommitter {
public void commitTransaction(FragmentTransaction fragmentTransaction);
}
private static interface Notifier {
public void notifyListener(@NonNull OnFragmentChangeListener onFragmentChangeListener);
}
}