フラグメントを使用したデュアルペイン

簡単な紹介、またはこれがすべて必要な理由



少し前までは、画面を回転させるときにシングルペインモードとデュアルペインモードの切り替えを実装する必要がありました。 私が見つけた既成の解決策は私には合わなかったので、仕事を洗練させて自分の自転車を発明しなければなりませんでした。



代替テキスト








資料、および材料設計の表記では、標準的な画面回転処理中に場所を非効率的に使用できるため、単一ペイン(階層の下に画面上にフラグメントが1つあります)とデュアル/マルチペインの2つのモードを区別する必要があることが示されています(ユーザーは、階層を順番に進む複数のフラグメントと対話するように求められます)



私が見たこの問題を解決するためのすべてのアプローチは、ViewPagerまたは追加のアクティビティのいずれかを使用しました。 FragmentManagerと2つのコンテナのみを使用して、このケースをわずかに異なる形式で決定しました。



一般的な外観



最初に行うことは、ユーザーがバックスタックと対話する方法を決定することです。 私は次の種類のプロモーションを好みました。



ポートレート:



A→A(不可視)、B→A(不可視)、B(不可視)、C→(popBackStack)→A(不可視)、B



風景:



A、B→A(非表示)、B、C→(popBackStack)→A、B



つまり、一般的なビューは、1つまたは2つのビューがユーザーに表示されるViewPagerに似ています。

次のことも考慮する必要があります。



  1. メインフラグメントの変更に対応する必要があります(たとえば、ユーザーが別の[ドロワー]タブに切り替えた)。
  2. フラグメントが表示されなくなった時点、つまり古いフラグメントが新しいフラグメントで削除された時点でのみ 、フラグメントの最後の状態をユーザーに表示する必要があります。


さあ始めましょう



まず、結果のコンポーネントを読みやすくするutil-classを作成します。



構成
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]; } }; }
      
      







デバイス構成の変更に耐えられるようにするには、ソリューション自体の状態に関連するすべてのものがParcelableインターフェイスを実装する必要があることに注意してください。



追加して、コールバックを完全に満たして、バックスタックの深さが変化する瞬間をキャッチします。



OnBackStackChangeListener
 public interface OnBackStackChangeListener { void onBackStackChanged(); }
      
      







コンポーネントの主要部分



このコンポーネントの実装に着手する際に最初に理解することは、フラグメントの状態を維持するすべての作業を手動で行う必要があることです。さらに、getCanonicalName()によって返される値によってフラグメントの状態を復元するにはリフレクションを使用する必要があることを理解する必要があります。 State



クラスは、これらの目的のためにDTOを実装し、同一の保存された状態を復元するのに十分です。



都道府県
 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)



メソッドが使用されます



最も退屈なものはすべて背後にあり、FragmentManagerでデコレータの作業を検討し、必要なメソッドを実装し、 Activity.onSaveInstanceState(Bundle)



状態を保存し、方向に従ってActivity.onSaveInstanceState(Bundle)



onCreateでActivity.onSaveInstanceState(Bundle)



復元するだけです。



MultipaneFragmentManager
 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]; } }; }
      
      







コンテナからフラグメントをデタッチした後、 FragmentManager.executePendingTransactions()



メソッドが呼び出されることに注意してください。これは、衝突が発生しないようにするために必要です。 これは、ランドスケープ内のフラグメントを別のコンテナに移動するときに、トランザクションがそれぞれ非同期的に発生するという事実により発生する可能性があります。 したがって、このソリューションではアニメーションを定性的に実装することはできません。対応するコンテナのフラグメントの入力にアニメーションを追加することでのみ回避でき、出力には回避できません。 また、この方法を使用すると、弱いデバイスではUIが多少遅くなりますが、ほとんどの場合、移行中のフリーズは見えなくなります。







以上で、実装への参照+例: gitlab.com/i.komarov/multipane-fragmentmanager







建設的な批判と、別の解決策の提案を表明できることを嬉しく思います。







UPD :代替方法が自分の好みに合わなかった理由を説明するように求められました。







したがって、提示された最初のオプションはViewPager



使用ViewPager



。 私の意見では、その主な欠点は、フラグメントの状態を維持するのが難しいことです(フラグメントの状態とViewPager



自体の状態の両方を保存する必要があります)。







また、構成変更間でプレゼンターを保存するために最も信頼性の高いメカニズムであるLoader



使用していないため、 ViewPager



を使用するとその動作に悪影響を与える可能性があります。







次に-追加のActivity



を使用して、公式ドキュメントのマスター/詳細フローの概念で説明されている詳細情報を表示すると、少し混乱しました。 ユーザーが詳細情報セクションに移動して、画面をめくると仮定します。 この場合、新しいアクティビティ内で処理が行われる必要があります。このアクティビティは、この画面の状態に関するデータをベースアクティビティに転送し、最終的に詳細からフラグメントの状態が復元されます。 引数を介したデータ転送には、転送されるデータ量に独自の非常に小さな制限があることを忘れないでください。 ビューコンポーネント間の遷移の階層にさらに多くのステップがあると、その実装は言うまでもなく、そのようなソリューションの動作メカニズムを想像することさえ難しくなります。 実際、2レベルの階層のみを表示する必要がある場合、このソリューションは「箱から出してすぐに」利用できるという理由だけで、提案されたソリューションの競合と見なすことができます。








All Articles