public class Config { public enum Orientation { LANDSCAPE, PORTRAIT } }
public class Info implements Parcelable { private static final byte ORIENTATION_LANDSCAPE = 0; private static final byte ORIENTATION_PORTRAIT = 1; @IdRes private int generalContainer; @IdRes private int detailsContainer; private Config.Orientation orientation; public Info(Parcel in) { this.generalContainer = in.readInt(); this.detailsContainer = in.readInt(); this.orientation = in.readByte() == ORIENTATION_LANDSCAPE ? Config.Orientation.LANDSCAPE : Config.Orientation.PORTRAIT; } public Info(int generalContainer, int detailsContainer, Config.Orientation orientation) { this.generalContainer = generalContainer; this.detailsContainer = detailsContainer; this.orientation = orientation; } public int getGeneralContainer() { return generalContainer; } public void setGeneralContainer(int generalConteiner) { this.generalContainer = generalConteiner; } public int getDetailsContainer() { return detailsContainer; } public void setDetailsContainer(int detailsContainer) { this.detailsContainer = detailsContainer; } public Config.Orientation getOrientation() { return orientation; } public void setOrientation(Config.Orientation orientation) { this.orientation = orientation; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(generalContainer); dest.writeInt(detailsContainer); dest.writeByte(orientation == Config.Orientation.LANDSCAPE ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT); } public static Parcelable.Creator<Info> CREATOR = new Creator<Info>() { @Override public Info createFromParcel(Parcel in) { return new Info(in); } @Override public Info[] newArray(int size) { return new Info[0]; } }; }
public interface OnBackStackChangeListener { void onBackStackChanged(); }
State
class implements DTO for these purposes, being sufficient to restore the state identical to the saved state. public class State implements Parcelable { private String fragmentName; private Fragment.SavedState fragmentState; public State(Parcel in) { fragmentName = in.readString(); fragmentState = in.readParcelable(Fragment.SavedState.class.getClassLoader()); } public State(String fragmentName, Fragment.SavedState fragmentState) { this.fragmentName = fragmentName; this.fragmentState = fragmentState; } public String getFragmentName() { return fragmentName; } public void setFragmentName(String fragmentName) { this.fragmentName = fragmentName; } public Fragment.SavedState getFragmentState() { return fragmentState; } public void setFragmentState(Fragment.SavedState fragmentState) { this.fragmentState = fragmentState; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(fragmentName); dest.writeParcelable(fragmentState, 0); } public static Parcelable.Creator<State> CREATOR = new Creator<State>() { @Override public State createFromParcel(Parcel in) { return new State(in); } @Override public State[] newArray(int size) { return new State[0]; } }; }
FragmentManager.saveFragmentInstanceState(Fragment)
Activity.onSaveInstanceState(Bundle)
and restoring according to orientation - onCreate. public class MultipaneFragmentManager implements Parcelable { public static final String KEY_DUALPANE_OBJECT = "net.styleru.i_komarov.core.MultipaneFragmentManager"; private static final String TAG = "MultipaneFragmentManager"; private FragmentManager fragmentManager; private OnBackStackChangeListener listenerNull = new OnBackStackChangeListener() { @Override public void onBackStackChanged() { } }; private OnBackStackChangeListener listener = listenerNull; private LinkedList<State> fragmentStateList; private Info info; private boolean onRestoreInstanceState; private boolean onSaveInstanceState; public MultipaneFragmentManager(Parcel in) { in.readList(fragmentStateList, LinkedList.class.getClassLoader()); info = in.readParcelable(Info.class.getClassLoader()); this.onRestoreInstanceState = false; this.onSaveInstanceState = false; } public MultipaneFragmentManager(FragmentManager fragmentManager, Info info) { this.fragmentManager = fragmentManager; this.fragmentStateList = new LinkedList<>(); this.info = info; onRestoreInstanceState = true; } public void attachFragmentManager(FragmentManager fragmentManager) { this.fragmentManager = fragmentManager; } public void detachFragmentManager() { this.fragmentManager = null; } public void setOrientation(Config.Orientation orientation) { this.info.setOrientation(orientation); } public void add(Fragment fragment) { this.add(fragment, true); listener.onBackStackChanged(); } public boolean allInLayout() { if(info.getOrientation() == Config.Orientation.LANDSCAPE) { if(fragmentManager.findFragmentById(info.getGeneralContainer()) != null && fragmentManager.findFragmentById(info.getDetailsContainer()) != null) { return true; } else { return false; } } else { if(getBackStackDepth() > 1) { return true; } else { return false; } } } @SuppressLint("LongLogTag") public synchronized void replace(Fragment fragment) { Log.d(TAG, "replace called, backstack was: " + fragmentStateList.size()); if(info.getOrientation() == Config.Orientation.PORTRAIT) { if(fragmentManager.findFragmentById(info.getGeneralContainer()) != null) { fragmentManager.beginTransaction().remove(fragmentManager.findFragmentById(info.getGeneralContainer())).commit(); fragmentManager.executePendingTransactions(); } fragmentManager.beginTransaction().replace(info.getGeneralContainer(), fragment).commit(); fragmentManager.executePendingTransactions(); } else { if(fragmentManager.findFragmentById(info.getDetailsContainer()) != null) { fragmentManager.beginTransaction() .remove(fragmentManager.findFragmentById(info.getDetailsContainer())) .commit(); fragmentManager.executePendingTransactions(); } fragmentManager.beginTransaction() .replace(info.getDetailsContainer(), fragment) .commit(); } } private synchronized void add(Fragment fragment, boolean addToBackStack) { if(info.getOrientation() == Config.Orientation.PORTRAIT) { if(fragmentManager.findFragmentById(info.getGeneralContainer()) != null) { if(addToBackStack) { saveOldestVisibleFragmentState(); } fragmentManager.beginTransaction().remove(fragmentManager.findFragmentById(info.getGeneralContainer())).commit(); fragmentManager.executePendingTransactions(); } fragmentManager.beginTransaction().replace(info.getGeneralContainer(), fragment).commit(); fragmentManager.executePendingTransactions(); } else if(fragmentManager.findFragmentById(info.getGeneralContainer()) == null) { fragmentManager.beginTransaction().replace(info.getGeneralContainer(), fragment).commit(); fragmentManager.executePendingTransactions(); } else if(fragmentManager.findFragmentById(info.getDetailsContainer()) == null) { fragmentManager.beginTransaction().replace(info.getDetailsContainer(), fragment).commit(); fragmentManager.executePendingTransactions(); } else { if(addToBackStack) { saveOldestVisibleFragmentState(); } saveDetailsFragmentState(); fragmentManager.beginTransaction() .remove(fragmentManager.findFragmentById(info.getGeneralContainer())) .remove(fragmentManager.findFragmentById(info.getDetailsContainer())) .commit(); fragmentManager.executePendingTransactions(); fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.getLast())) .replace(info.getDetailsContainer(), fragment) .commit(); fragmentManager.executePendingTransactions(); fragmentStateList.removeLast(); } } @SuppressLint("LongLogTag") public void popBackStack() { Log.d(TAG, "popBackStack called, backstack was: " + fragmentStateList.size()); if(info.getOrientation() == Config.Orientation.PORTRAIT) { //fragmentStateList.removeLast(); fragmentManager.beginTransaction() .remove(fragmentManager.findFragmentById(info.getGeneralContainer())) .commit(); fragmentManager.executePendingTransactions(); fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.getLast())) .commit(); fragmentStateList.removeLast(); } else if(fragmentStateList.size() > 0) { //fragmentStateList.removeLast(); saveOldestVisibleFragmentState(); fragmentManager.beginTransaction() .remove(fragmentManager.findFragmentById(info.getDetailsContainer())) .remove(fragmentManager.findFragmentById(info.getGeneralContainer())) .commit(); fragmentManager.executePendingTransactions(); fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.get(fragmentStateList.size() - 2))) .replace(info.getDetailsContainer(), restoreFragment(fragmentStateList.getLast())) .commit(); //remove the fragment that was in the details container before popbackstack was called as it is no longer accessible to user fragmentStateList.removeLast(); fragmentStateList.removeLast(); } else if(getFragmentCount() == 2) { fragmentManager.beginTransaction() .remove(fragmentManager.findFragmentById(info.getDetailsContainer())) .commit(); fragmentManager.executePendingTransactions(); } listener.onBackStackChanged(); } @SuppressLint("LongLogTag") public void onRestoreInstanceState() { onSaveInstanceState = false; if(!onRestoreInstanceState) { onRestoreInstanceState = true; if (fragmentStateList != null) { if(info.getOrientation() == Config.Orientation.LANDSCAPE) { if (fragmentStateList.size() > 1) { fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.get(fragmentStateList.size() - 2))) .replace(info.getDetailsContainer(), restoreFragment(fragmentStateList.getLast())) .commit(); //remove state of visible fragments fragmentStateList.removeLast(); fragmentStateList.removeLast(); Log.d(TAG, "restored in landscape mode, backstack: " + fragmentStateList.size()); } else if (fragmentStateList.size() == 1) { fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.getLast())) .commit(); //remove state of only visible fragment fragmentStateList.removeLast(); Log.d(TAG, "restored in landscape mode, backstack is clear"); } } else { fragmentManager.beginTransaction() .replace(info.getGeneralContainer(), restoreFragment(fragmentStateList.getLast())) .commit(); //remove state of visible fragment fragmentStateList.removeLast(); Log.d(TAG, "restored in portrait mode, backstack: " + fragmentStateList.size()); } } } fragmentManager.executePendingTransactions(); } @SuppressLint("LongLogTag") public void onSaveInstanceState() { if(!onSaveInstanceState) { onRestoreInstanceState = false; onSaveInstanceState = true; if(info.getOrientation() == Config.Orientation.LANDSCAPE) { if(saveOldestVisibleFragmentState()) { saveDetailsFragmentState(); } Log.d(TAG, "saved state before recreating fragments in portrait, now stack is: " + fragmentStateList.size()); } else if(info.getOrientation() == Config.Orientation.PORTRAIT) { saveOldestVisibleFragmentState(); Log.d(TAG, "saved state before recreating fragments in landscape, now stack is: " + fragmentStateList.size()); } FragmentTransaction transaction = fragmentManager.beginTransaction(); if (fragmentManager.findFragmentById(info.getGeneralContainer()) != null) { transaction.remove(fragmentManager.findFragmentById(info.getGeneralContainer())); } if (fragmentManager.findFragmentById(info.getDetailsContainer()) != null) { transaction.remove(fragmentManager.findFragmentById(info.getDetailsContainer())); } transaction.commit(); } } public int getBackStackDepth() { return fragmentStateList.size(); } public int getFragmentCount() { int count = 0; if(fragmentManager.findFragmentById(info.getGeneralContainer()) != null) { count++; if(info.getOrientation() == Config.Orientation.LANDSCAPE && fragmentManager.findFragmentById(info.getDetailsContainer()) != null) { count++; } count += getBackStackDepth(); } return count; } private Fragment restoreFragment(State state) { try { Fragment fragment = ((Fragment) Class.forName(state.getFragmentName()).newInstance()); fragment.setInitialSavedState(state.getFragmentState()); return fragment; } catch (InstantiationException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } @SuppressLint("LongLogTag") private boolean saveOldestVisibleFragmentState() { Fragment current = fragmentManager.findFragmentById(info.getGeneralContainer()); if (current != null) { Log.d(TAG, "saveOldestVisibleFragmentState called, current was not null"); fragmentStateList.add(new State(current.getClass().getCanonicalName(), fragmentManager.saveFragmentInstanceState(current))); } return current != null; } @SuppressLint("LongLogTag") private boolean saveDetailsFragmentState() { Fragment details = fragmentManager.findFragmentById(info.getDetailsContainer()); if(details != null) { Log.d(TAG, "saveDetailsFragmentState called, details was not null"); fragmentStateList.add(new State(details.getClass().getCanonicalName(), fragmentManager.saveFragmentInstanceState(details))); } return details != null; } public void setOnBackStackChangeListener(OnBackStackChangeListener listener) { this.listener = listener; } public void removeOnBackStackChangeListener() { this.listener = listenerNull; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeList(fragmentStateList); dest.writeParcelable(info, 0); } public static Parcelable.Creator<MultipaneFragmentManager> CREATOR = new Creator<MultipaneFragmentManager>() { @Override public MultipaneFragmentManager createFromParcel(Parcel in) { return new MultipaneFragmentManager(in); } @Override public MultipaneFragmentManager[] newArray(int size) { return new MultipaneFragmentManager[0]; } }; }
It should be noted separately that after detaching the fragments from containers, the FragmentManager.executePendingTransactions()
method is called, this is required in order to avoid collisions. It can occur due to the fact that transactions occur asynchronously, respectively, when moving a fragment to landscape in another container, a problem may arise due to the fact that it has not yet been untied from the previous one. Thus, the animation in this solution cannot be implemented qualitatively, only workaround will be possible with the addition of animations to the input of fragments in the corresponding containers, but not to the output. Also, the use of this method can slightly slow down the UI on weak devices, but for the most part, the friezes will be invisible during transitions.
That's all, reference to the implementation + example: gitlab.com/i.komarov/multipane-fragmentmanager
I will be glad to display constructive criticism, as well as to offer alternative solutions.
UPD : I was asked to describe why I didn’t like alternative methods.
So, the first of the options presented is the use of ViewPager
. Its main drawbacks, in my opinion, are the complexity of maintaining the state of the fragments (both the state of the fragments and the state of the ViewPager
itself are ViewPager
), plus my personal reluctance to use the View component as a controller.
Also, since I’m not using the most reliable mechanism — the Loader
— to save a presenter between configuration changes, using ViewPager
can negatively affect its operation.
Further, the use of an additional Activity
for displaying detailed information described in the Master / Detail flow of the concept in the official documentation somewhat confused me. Suppose that the user went to the detailed information section, then turned the screen. In this case, processing should occur within the new activity, which will transfer data on the state of this screen to the base activity, from which, finally, the state of the fragment with the details will be restored. This mechanism seemed to me too overloaded, because you should not forget that the transfer of data through arguments has its very small limit on the amount of data transferred. With more steps in the hierarchy of transitions between view components, it can be difficult to even imagine the mechanism of such a decision, let alone its implementation. In fact, if you need to display only a two-level hierarchy, this solution can be considered a competitor of the proposed one, but only because of its availability “out of the box”.
Source: https://habr.com/ru/post/316746/
All Articles