フラグメントを使用してAndroidのタブごとに個別のバックスタック


158

Androidアプリでナビゲーション用のタブを実装しようとしています。TabActivityとActivityGroupは非推奨であるため、代わりにFragmentsを使用して実装したいと思います。

タブごとに1つのフラグメントを設定し、タブがクリックされたときにフラグメントを切り替える方法を知っています。しかし、どうすれば各タブに個別のバックスタックを設定できますか?

たとえば、フラグメントAとBはタブ1の下にあり、フラグメントCとDはタブ2の下にあります。アプリを起動すると、フラグメントAが表示され、タブ1が選択されます。次に、フラグメントAがフラグメントBに置き換えられます。タブ2を選択すると、フラグメントCが表示されます。タブ1を選択すると、フラグメントBが再び表示されます。この時点で、戻るボタンを使用してフラグメントAを表示できるはずです。

また、デバイスを回転させても、各タブの状態が維持されることが重要です。

BRマーティン

回答:


23

フレームワークは現在、これを自動的に行いません。タブごとに独自のバックスタックを作成して管理する必要があります。

正直なところ、これは本当に問題のあることのようです。私はそれがまともなUIになるとは想像できません-私がいるタブに応じてバックキーが異なることをする場合、特にバックキーが上部にあるときにアクティビティ全体を閉じるという通常の動作を持っている場合スタックは...厄介に聞こえます。

WebブラウザーUIのようなものを構築しようとしている場合、ユーザーに自然なUXを取得するには、コンテキストに応じて多くの微妙な動作の微調整が必​​要になるため、独自のバックスタックを実行する必要があります。フレームワークのデフォルトの実装に依存するのではなく、管理。例として、戻るキーが標準のブラウザとどのようにやり取りできるかに注意してみてください。(ブラウザの各「ウィンドウ」は基本的にタブです。)


7
しないでください。そして、フレームワークはほとんど役に立たない。こういうことを自動的にサポートするわけではありません。先に述べたように、とにかくバック動作を注意深く制御する必要がある非常に特殊な状況を除いて、ユーザーエクスペリエンスがまともなものになるとは想像できません。
ハックボット2011

9
このタイプのナビゲーションでは、タブがあり、各タブにページの階層があります。たとえば、iPhoneアプリケーションでは非常に一般的です(App StoreとiPodアプリを確認できます)。ユーザーエクスペリエンスはかなりまともです。
ドミトリーリャドネンコ、2011

13
これは非常識です。iPhoneには、戻るボタンさえありません。タブにフラグメントを実装するための非常に単純なコードを示すAPIデモがあります。質問されたのは、タブごとに異なるバックスタックを使用することでした。私の返答は、フレームワークはこれを自動的に提供しないということです。なぜなら、[戻る]ボタンの意味は、ユーザーエクスペリエンスが不安定になる可能性が高いためです。必要であれば、自分でかなり簡単にバックセマンティクスを実装できます。
ハックボット

4
繰り返しになりますが、iPhoneには「戻る」ボタンがないため、意味的にはAndroidのようなバックスタックの動作はありません。また、「より良いだけ固執する活動とし、自分自身に多くの時間を節約する」ことはありません任意の活動は、あなたが自分自身の異なるバック・スタックとUIのタブを維持置くことはできませんので、ここでは意味が。実際、アクティビティのバックスタック管理は、Fragmentフレームワークによって提供されるものよりも柔軟性が低くなります。
ハックボット

22
@hackbod私はあなたのポイントをフォローしようとしていますが、カスタムのバックスタック動作を実装するのに問題があります。この設計に携わっていれば、それがいかに簡単であるかについて、確かな洞察を得ることができると思います。OPのユースケースにデモアプリがある可能性はありますか?それは、非常に一般的な状況であり、特に、これらのリクエストを行うクライアントのためにiOSアプリを作成して移植する必要がある私たちにとっては...個別の管理各FragmentActivity内のバックスタックをフラグメント化します。
Richard Le Mesurier、2011年

138

私はこの質問にひどく遅れています。しかし、このスレッドは非常に有益で参考になったので、2ペンスをここに投稿した方がいいと思いました。

私はこのような画面フローが必要でした(2つのタブと各タブに2つのビューがある最小限のデザイン)、

tabA
    ->  ScreenA1, ScreenA2
tabB
    ->  ScreenB1, ScreenB2

以前は同じ要件がありましたが、TabActivityGroup(当時は非推奨でした)アクティビティを使用してそれを実行しました。今回はフラグメントを使いたかった。

これが私がやった方法です。

1.ベースフラグメントクラスを作成する

public class BaseFragment extends Fragment {
    AppMainTabActivity mActivity;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mActivity = (AppMainTabActivity) this.getActivity();
    }

    public void onBackPressed(){
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data){
    }
}

アプリのすべてのフラグメントは、この基本クラスを拡張できます。特別なフラグメントを使用したい場合はListFragment、そのための基本クラスも作成する必要があります。あなたが完全に記事を読んだ場合、あなたはの使用法について明確にonBackPressed()なりonActivityResult()ます。

2.プロジェクトのどこからでもアクセスできるタブ識別子を作成します

public class AppConstants{
    public static final String TAB_A  = "tab_a_identifier";
    public static final String TAB_B  = "tab_b_identifier";

    //Your other constants, if you have them..
}

ここでは何も説明しません。

3. OK、メインタブアクティビティ-コード内のコメントを確認してください。

public class AppMainFragmentActivity extends FragmentActivity{
    /* Your Tab host */
    private TabHost mTabHost;

    /* A HashMap of stacks, where we use tab identifier as keys..*/
    private HashMap<String, Stack<Fragment>> mStacks;

    /*Save current tabs identifier in this..*/
    private String mCurrentTab;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.app_main_tab_fragment_layout);

        /*  
         *  Navigation stacks for each tab gets created.. 
         *  tab identifier is used as key to get respective stack for each tab
         */
        mStacks             =   new HashMap<String, Stack<Fragment>>();
        mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
        mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());

        mTabHost                =   (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setOnTabChangedListener(listener);
        mTabHost.setup();

        initializeTabs();
    }


    private View createTabView(final int id) {
        View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
        ImageView imageView =   (ImageView) view.findViewById(R.id.tab_icon);
        imageView.setImageDrawable(getResources().getDrawable(id));
        return view;
    }

    public void initializeTabs(){
        /* Setup your tab icons and content views.. Nothing special in this..*/
        TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);
        mTabHost.setCurrentTab(-3);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
        mTabHost.addTab(spec);


        spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
        mTabHost.addTab(spec);
    }


    /*Comes here when user switch tab, or we do programmatically*/
    TabHost.OnTabChangeListener listener    =   new TabHost.OnTabChangeListener() {
      public void onTabChanged(String tabId) {
        /*Set current tab..*/
        mCurrentTab                     =   tabId;

        if(mStacks.get(tabId).size() == 0){
          /*
           *    First time this tab is selected. So add first fragment of that tab.
           *    Dont need animation, so that argument is false.
           *    We are adding a new fragment which is not present in stack. So add to stack is true.
           */
          if(tabId.equals(AppConstants.TAB_A)){
            pushFragments(tabId, new AppTabAFirstFragment(), false,true);
          }else if(tabId.equals(AppConstants.TAB_B)){
            pushFragments(tabId, new AppTabBFirstFragment(), false,true);
          }
        }else {
          /*
           *    We are switching tabs, and target tab is already has atleast one fragment. 
           *    No need of animation, no need of stack pushing. Just show the target fragment
           */
          pushFragments(tabId, mStacks.get(tabId).lastElement(), false,false);
        }
      }
    };


    /* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
    public void setCurrentTab(int val){
          mTabHost.setCurrentTab(val);
    }


    /* 
     *      To add fragment to a tab. 
     *  tag             ->  Tab identifier
     *  fragment        ->  Fragment to show, in tab identified by tag
     *  shouldAnimate   ->  should animate transaction. false when we switch tabs, or adding first fragment to a tab
     *                      true when when we are pushing more fragment into navigation stack. 
     *  shouldAdd       ->  Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
     *                      true in all other cases.
     */
    public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
      if(shouldAdd)
          mStacks.get(tag).push(fragment);
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      if(shouldAnimate)
          ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }


    public void popFragments(){
      /*    
       *    Select the second last fragment in current tab's stack.. 
       *    which will be shown after the fragment transaction given below 
       */
      Fragment fragment             =   mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);

      /*pop current fragment from stack.. */
      mStacks.get(mCurrentTab).pop();

      /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      ft.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }   


    @Override
    public void onBackPressed() {
        if(mStacks.get(mCurrentTab).size() == 1){
          // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
          finish();
          return;
        }

        /*  Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
         *  when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
         *  kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
         */
        ((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();

        /* Goto previous fragment in navigation stack of this tab */
            popFragments();
    }


    /*
     *   Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
     *  in that fragment, and called it from the activity. But couldn't resist myself.
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(mStacks.get(mCurrentTab).size() == 0){
            return;
        }

        /*Now current fragment on screen gets onActivityResult callback..*/
        mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
    }
}

4. app_main_tab_fragment_layout.xml(誰かが興味を持っている場合に備えて)

<?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>

        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>

        <TabWidget
            android:id="@android:id/tabs"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>

    </LinearLayout>
</TabHost>

5. AppTabAFirstFragment.java(すべてのタブと同様に、タブAの最初のフラグメント)

public class AppTabAFragment extends BaseFragment {
    private Button mGotoButton;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view       =   inflater.inflate(R.layout.fragment_one_layout, container, false);

        mGoToButton =   (Button) view.findViewById(R.id.goto_button);
        mGoToButton.setOnClickListener(listener);

        return view;
    }

    private OnClickListener listener        =   new View.OnClickListener(){
        @Override
        public void onClick(View v){
            /* Go to next fragment in navigation stack*/
            mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
        }
    }
}

これは最も洗練された正しい方法ではないかもしれません。しかし、それは私の場合には美しく機能しました。また、この要件はポートレートモードでのみ必要でした。このコードを両方の向きをサポートするプロジェクトで使用する必要はありませんでした。だから私がそこで直面しているどのような課題を言うことはできません。

編集:

完全なプロジェクトが必要な場合は、サンプルプロジェクトをgithubにプッシュしました。


2
すべてのフラグメントのデータを格納し、それらのすべてを再作成し、スタックを再構築します...単純な向きの変更には多くの作業が必要です。
Michael Eilers Smith 2013

3
@omegataiはあなたに完全に同意します。Androidはスタックを管理しないため、すべての問題が発生します(iOSが行い、方向を変更したり、複数のフラグメントを持つタブは簡単です)。これにより、このQ /スレッド。今、それに戻って行くん良いん...
Krishnabhadra

1
@Renjithこれは、タブを切り替えるとフラグメントが毎回再作成されるためです。一度でも考えないでください。フラグメントはタブスイッチ全体で再利用されます。AタブからBに切り替えると、Aタブがメモリから解放されます。したがって、アクティビティにデータを保存し、サーバーからデータを取得する前に、アクティビティにデータがあるかどうかを毎回確認してください。
Krishnabhadra 2013年

2
@Krishnabhadraわかりました。私が間違っている場合に備えて訂正させてください。あなたの例のように、アクティビティは1つだけなので、バンドルは1つです。BaseFragment(プロジェクトを参照)にアダプターインスタンスを作成し、そこにデータを保存します。ビューを作成するときはいつでも使用してください。
Renjith 2013年

1
うまくいきました。どうもありがとう。プロジェクト全体をアップロードするのは良い考えです!:-)
Vinay W

96

最近アプリについて説明したのとまったく同じ動作を実装する必要がありました。アプリケーションの画面と全体的なフローはすでに定義されているため、それに固執する必要がありました(iOSアプリのクローンです...)。幸いなことに、画面上の戻るボタンを取り除くことができました:)

私たちは、TabActivity、FragmentActivities(フラグメントのサポートライブラリを使用していた)、およびFragmentsを組み合わせてソリューションをハッキングしました。振り返ってみると、それがアーキテクチャの最良の決定ではなかったと私は確信していますが、うまく機能させることができました。もう一度行う必要がある場合は、おそらくアクティビティベースのソリューション(フラグメントなし)を実行するか、タブのアクティビティを1つだけにして、残りをすべてビューにしてみます(私が見つけたものははるかに多くなります)。アクティビティ全体よりも再利用可能)。

したがって、要件は、いくつかのタブと各タブにネスト可能な画面を持つことでした:

tab 1
  screen 1 -> screen 2 -> screen 3
tab 2
  screen 4
tab 3
  screen 5 -> 6

等...

つまり、ユーザーはタブ1から開始し、画面1から画面2、次に画面3に移動し、タブ3に切り替えて画面4から6に移動します。タブ1に戻ると、画面3が再び表示され、戻るを押すと画面2に戻ります。再び戻ると、彼は画面1にいます。タブ3に切り替えると、画面6に戻ります。

アプリケーションのメインアクティビティはMainTabActivityで、これはTabActivityを拡張したものです。各タブはアクティビティに関連付けられています。たとえば、ActivityInTab1、2、3とします。そして、各画面はフラグメントになります。

MainTabActivity
  ActivityInTab1
    Fragment1 -> Fragment2 -> Fragment3
  ActivityInTab2
    Fragment4
  ActivityInTab3
    Fragment5 -> Fragment6

各ActivityInTabは一度に1つのフラグメントのみを保持し、1つのフラグメントを別のフラグメントに置き換える方法を知っています(ActvityGroupとほぼ同じです)。この方法でタブごとに個別のバックスタックを作成するのは非常に簡単です。

各ActivityInTabの機能はまったく同じでした。あるフラグメントから別のフラグメントに移動してバックスタックを維持する方法を知っているので、それを基本クラスに配置します。単にActivityInTabと呼びましょう:

abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_in_tab);
    }

    /**
     * Navigates to a new fragment, which is added in the fragment container
     * view.
     * 
     * @param newFragment
     */
    protected void navigateTo(Fragment newFragment) {
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();

        ft.replace(R.id.content, newFragment);

        // Add this transaction to the back stack, so when the user presses back,
        // it rollbacks.
        ft.addToBackStack(null);
        ft.commit();
    }

}

activity_in_tab.xmlは次のとおりです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:isScrollContainer="true">
</RelativeLayout>

ご覧のとおり、各タブのビューレイアウトは同じでした。これは、各フラグメントを保持するのがコンテンツと呼ばれるFrameLayoutにすぎないためです。フラグメントは、各画面のビューを持つフラグメントです。

ボーナスポイントとして、ユーザーが[戻る]を押したときに確認ダイアログを表示するためのコードを少し追加しました。前に戻るフラグメントはありません。

// In ActivityInTab.java...
@Override
public void onBackPressed() {
    FragmentManager manager = getSupportFragmentManager();
    if (manager.getBackStackEntryCount() > 0) {
        // If there are back-stack entries, leave the FragmentActivity
        // implementation take care of them.
        super.onBackPressed();
    } else {
        // Otherwise, ask user if he wants to leave :)
        showExitDialog();
    }
}

それはほとんどセットアップです。ご覧のとおり、各FragmentActivity(または単にAndroid> 3のActivity)は、独自のFragmentManagerですべてのバックスタッキングを処理しています。

ActivityInTab1のようなアクティビティは非常に単純です。最初のフラグメント(画面)を表示するだけです。

public class ActivityInTab1 extends ActivityInTab {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        navigateTo(new Fragment1());
    }
}

次に、フラグメントが別のフラグメントに移動する必要がある場合、少し厄介なキャストを行う必要があります...しかし、それほど悪くはありません。

// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());

これでほぼ終わりです。これは非常に標準的な(そして大抵はあまり良くない)ソリューションではないので、この機能を実現するためのより良いアプローチは何ですか?完了」というメッセージが表示された場合はリンクまたは資料のいずれかを説明していただければ幸いです。 Androidがこれにアプローチする方法を(タブ、タブ内のネストされた画面など)を。コメントでこの回答を切り離してください:)

このソリューションがあまり良くないことを示す兆候として、最近、アプリケーションにいくつかのナビゲーション機能を追加する必要がありました。ユーザーを1つのタブから別のタブに移動し、ネストされた画面に移動するいくつかの奇妙なボタン。フラグメントとアクティビティが実際にインスタンス化および初期化されるときに、誰が知っているか、誰が問題を処理しているかを処理するため、プログラムでそれを行うのはお尻の苦痛でした。これらの画面とタブがすべて本当にビューだったら、もっと簡単だったと思います。


最後に、向きの変化を乗り切る必要がある場合は、フラグメントをsetArguments / getArgumentsを使用して作成することが重要です。フラグメントのコンストラクターでインスタンス変数を設定すると、ねじ込まれます。しかし、幸いなことに、これは非常に簡単に修正できます。コンストラクタのsetArgumentsにすべてを保存し、onCreateのgetArgumentsでそれらを取得して使用します。


13
すばらしい答えですが、これを目にする人はほとんどいないと思います。私はまったく同じパスを選択しましたが(前の回答の会話からわかるように)、あなたと同じように満足していません。このAPIは主要なユースケースをカバーしていないので、Googleは本当にこの断片を台無しにしたと思います。あなたが遭遇する可能性のある別の問題は、フラグメントを別のフラグメントに埋め込むことができないことです。
ドミトリーリヤドネンコ、2011

コメントボルダーをありがとう。ええ、私はフラグメントAPIについてこれ以上同意できませんでした。私はすでにネストされたフラグメントの問題に遭遇しました(それが、「1つのフラグメントを別のフラグメントで置き換える」アプローチを彼が行った理由です)。
エピデミアン2011

1
私はすべての活動を通じてこれを実装しました。私は得たものが気に入らなかったので、フラグメントを試してみるつもりです。それはあなたの経験の逆です!各タブの子ビューのライフサイクルを処理し、独自の[戻る]ボタンを実装するためのアクティビティには、多くの実装があります。また、すべてのビューへの参照を保持することはできません。そうしないと、メモリが爆発します。フラグメントが:1)メモリの明確な分離でフラグメントのライフサイクルをサポートし、2)戻るボタン機能に加えて、このプロセスにフラグメントを使用すると、タブレットでの実行が容易になりますか?
gregm 2011

ユーザーがタブを切り替えるとどうなりますか?Fragmentバックスタックは削除されますか?バックスタックが残っていることを確認するにはどうすればよいですか?
gregm '21 / 09/21

1
@gregm私がしたように1つのタブ<-> 1つのアクティビティに行った場合、アクティビティが実際に維持されるため、タブが切り替えられたときに各タブのバックスタックが残ります。一時停止して再開するだけです。TabActivityでタブが切り替えられたときに、アクティビティを破棄して再作成する方法があるかどうかはわかりません。ただし、アクティビティ内のフラグメントを私が提案したように置き換えると、それら破棄されます(バックスタックがポップされると再作成されます)。したがって、いつでもタブごとに最大1つのフラグメントが有効になります。
エピデミアン


6

フラグメントへの強い参照を格納することは正しい方法ではありません。

FragmentManagerはputFragment(Bundle, String, Fragment)およびを提供しsaveFragmentInstanceState(Fragment)ます。

どちらでも、バックスタックを実装するには十分です。


を使用しputFragmentて、フラグメントを置き換えるのではなく、古いフラグメントを切り離して新しいフラグメントを追加します。これは、フレームワークがバックスタックに追加された置換トランザクションに対して行うことです。putFragmentアクティブなフラグメントの現在のリストへのインデックスを保存します。これらのフラグメントは、向きの変更中にフレームワークによって保存されます。

2番目の方法は、を使用してsaveFragmentInstanceState、フラグメントの状態全体をバンドルに保存し、切り離すのではなく、実際に削除できるようにします。このアプローチを使用すると、必要なときにいつでもフラグメントをポップできるため、バックスタックの操作が簡単になります。


このユースケースでは2番目の方法を使用しました。

SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
               \                          /
                \------------------------/

ユーザーが3番目の画面から[戻る]ボタンを押して[サインアップ]画面に戻らないようにします。また、それらの間でアニメーションを反転します(onCreateAnimation)ので、少なくともユーザーが何かが正しくないことにはっきりと気づかなければ、ハッキーソリューションは機能しません。

これはカスタムバックスタックの有効な使用例であり、ユーザーが期待することを行います...

private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";

private MyBackStack mBackStack;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    if (state == null) {
        mBackStack = new MyBackStack();

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.add(R.id.act_base_frg_container, new SignInFragment());
        tr.commit();
    } else {
        mBackStack = state.getParcelable(STATE_BACKSTACK);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(STATE_BACKSTACK, mBackStack);
}

private void showFragment(Fragment frg, boolean addOldToBackStack) {
    final FragmentManager fm = getSupportFragmentManager();
    final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);

    FragmentTransaction tr = fm.beginTransaction();
    tr.replace(R.id.act_base_frg_container, frg);
    // This is async, the fragment will only be removed after this returns
    tr.commit();

    if (addOldToBackStack) {
        mBackStack.push(fm, oldFrg);
    }
}

@Override
public void onBackPressed() {
    MyBackStackEntry entry;
    if ((entry = mBackStack.pop()) != null) {
        Fragment frg = entry.recreate(this);

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.replace(R.id.act_base_frg_container, frg);
        tr.commit();

        // Pop it now, like the framework implementation.
        fm.executePendingTransactions();
    } else {
        super.onBackPressed();
    }
}

public class MyBackStack implements Parcelable {

    private final List<MyBackStackEntry> mList;

    public MyBackStack() {
        mList = new ArrayList<MyBackStackEntry>(4);
    }

    public void push(FragmentManager fm, Fragment frg) {
        push(MyBackStackEntry.newEntry(fm, frg);
    }

    public void push(MyBackStackEntry entry) {
        if (entry == null) {
            throw new NullPointerException();
        }
        mList.add(entry);
    }

    public MyBackStackEntry pop() {
        int idx = mList.size() - 1;
        return (idx != -1) ? mList.remove(idx) : null;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        final int len = mList.size();
        dest.writeInt(len);
        for (int i = 0; i < len; i++) {
            // MyBackStackEntry's class is final, theres no
            // need to use writeParcelable
            mList.get(i).writeToParcel(dest, flags);
        }
    }

    protected MyBackStack(Parcel in) {
        int len = in.readInt();
        List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
        for (int i = 0; i < len; i++) {
            list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
        }
        mList = list;
    }

    public static final Parcelable.Creator<MyBackStack> CREATOR =
        new Parcelable.Creator<MyBackStack>() {

            @Override
            public MyBackStack createFromParcel(Parcel in) {
                return new MyBackStack(in);
            }

            @Override
            public MyBackStack[] newArray(int size) {
                return new MyBackStack[size];
            }
    };
}

public final class MyBackStackEntry implements Parcelable {

    public final String fname;
    public final Fragment.SavedState state;
    public final Bundle arguments;

    public MyBackStackEntry(String clazz, 
            Fragment.SavedState state,
            Bundle args) {
        this.fname = clazz;
        this.state = state;
        this.arguments = args;
    }

    public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
        final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
        final String name = frg.getClass().getName();
        final Bundle args = frg.getArguments();
        return new MyBackStackEntry(name, state, args);
    }

    public Fragment recreate(Context ctx) {
        Fragment frg = Fragment.instantiate(ctx, fname);
        frg.setInitialSavedState(state);
        frg.setArguments(arguments);
        return frg;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(fname);
        dest.writeBundle(arguments);

        if (state == null) {
            dest.writeInt(-1);
        } else if (state.getClass() == Fragment.SavedState.class) {
            dest.writeInt(0);
            state.writeToParcel(dest, flags);
        } else {
            dest.writeInt(1);
            dest.writeParcelable(state, flags);
        }
    }

    protected MyBackStackEntry(Parcel in) {
        final ClassLoader loader = getClass().getClassLoader();
        fname = in.readString();
        arguments = in.readBundle(loader);

        switch (in.readInt()) {
            case -1:
                state = null;
                break;
            case 0:
                state = Fragment.SavedState.CREATOR.createFromParcel(in);
                break;
            case 1:
                state = in.readParcelable(loader);
                break;
            default:
                throw new IllegalStateException();
        }
    }

    public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
        new Parcelable.Creator<MyBackStackEntry>() {

            @Override
            public MyBackStackEntry createFromParcel(Parcel in) {
                return new MyBackStackEntry(in);
            }

            @Override
            public MyBackStackEntry[] newArray(int size) {
                return new MyBackStackEntry[size];
            }
    };
}

2

免責事項:


これは、かなり標準的なAndroidのものと思われる同様のタイプの問題について私が取り組んだ関連ソリューションを投稿するのに最適な場所だと思います。それは誰にとっても問題を解決するつもりはありませんが、それは一部を助けるかもしれません。


フラグメント間の主な違いがそれらをバックアップするデータのみである場合(つまり、大きなレイアウトの大きな違いではない場合)、フラグメントを実際に置き換える必要はなく、単に基になるデータを交換してビューを更新するだけです。

このアプローチの考えられる例の1つを次に示します。

ListViewsを使用するアプリがあります。リストの各項目は、いくつかの子を持つ親です。アイテムをタップすると、元のリストと同じActionBarタブ内で、新しいリストをそれらの子で開く必要があります。これらのネストされたリストのレイアウトは非常に似ていますが(場合によっては、条件付きの微調整がいくつかあります)、データは異なります。

このアプリは、最初の親リストの下に子孫の複数のレイヤーがあり、ユーザーが最初の親リストを超えて特定の深さにアクセスしようとしたときに、サーバーからのデータがある場合とない場合があります。リストはデータベースカーソルから構築され、フラグメントはカーソルローダーとカーソルアダプターを使用してリストビューにリスト項目を設定するため、クリックが登録されたときに発生する必要があるのは次のとおりです。

1)適切な「to」フィールドと「from」フィールドを使用して新しいアダプターを作成します。これは、リストに追加される新しいアイテムビューと、新しいカーソルによって返される列に一致します。

2)このアダプターをListViewの新しいアダプターとして設定します。

3)クリックされたアイテムに基づいて新しいURIを作成し、新しいURI(およびプロジェクション)でカーソルローダーを再起動します。この例では、URIは特定のクエリにマップされ、選択引数はUIから渡されます。

4)URIから新しいデータがロードされたら、アダプターに関連付けられているカーソルを新しいカーソルに交換すると、リストが更新されます。

トランザクションを使用していないため、これに関連するバックスタックはありません。したがって、独自にビルドするか、階層からバックアウトするときにクエリを逆に再生する必要があります。これを試したとき、クエリは十分高速で、階層の最上位に到達するまで、oNBackPressed()で再度実行するだけで、フレームワークは再び[戻る]ボタンを引き継ぎます。

同様の状況に陥った場合は、次のドキュメントを必ずお読みください:http : //developer.android.com/guide/topics/ui/layout/listview.html

http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html

これが誰かに役立つことを願っています!


誰かがこれを行っており、SectionIndexer(AlphabetIndexerなど)も使用している場合、アダプタを交換した後、高速スクロールが機能しないことに気付く場合があります。一種の不幸なバグですが、新しいインデクサーでアダプターを交換しても、FastScrollで使用されるセクションのリストは更新されません。回避策があります。次を参照してください:問題の説明回避策
裁判所、

2

私はまったく同じ問題を抱えており、スタックされたタブ、バックアップ、ナビゲーションをカバーするオープンソースのgithubプロジェクトを実装しており、十分にテストおよび文書化されています。

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

これは、ナビゲーションタブとフラグメントの切り替え、上下のナビゲーションの処理のためのシンプルで小さなフレームワークです。各タブには、独自のフラグメントのスタックがあります。ActionBarSherlockを使用し、APIレベル8と互換性があります。


2

Androidは1つのバックスタックしか処理しないため、これは複雑な問題ですが、これは可能です。各タブのフラグメント履歴という、まさに求めていることを実行するタブスタッカーと呼ばれるライブラリを作成するのに数日かかりました。これはオープンソースで完全に文書化されており、gradleに簡単に組み込むことができます。ライブラリはgithubにあります:https : //github.com/smart-fun/TabStacker

サンプルアプリをダウンロードして、動作がニーズに対応していることを確認することもできます。

https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp

ご不明な点がございましたら、お気軽にメールをお送りください。


2

誰かが探していて、彼/彼女のニーズに最適なものを試してみたいと思っている場合に備えて、私自身の解決策を提案したいと思います。

https://github.com/drusak/tabactivity

ライブラリを作成する目的はかなり当たり前です-iPhoneのように実装してください。

主な利点:

  • TabLayoutでandroid.support.designライブラリを使用します。
  • 各タブには、(フラグメントの参照を保存せずに)FragmentManagerを使用して独自のスタックがあります。
  • ディープリンクのサポート(特定のタブとその中の特定のフラグメントのレベルを開く必要がある場合);
  • タブの状態の保存/復元;
  • タブ内のフラグメントの適応ライフサイクルメソッド。
  • ニーズに合わせて簡単に実装できます。

ありがとう、これは非常に役に立ちました。ListFragmentsに加えてFragments を使用する必要があるため、BaseTabFragment.javaをBaseTabListFragment.javaに複製し、ListFragmentを拡張しました。次に、常にBaseTabFragmentを想定していると想定されるコードのさまざまな部分を変更する必要がありました。もっと良い方法はありますか?
Primehalo 2016年

残念ながら、ListFragmentについては考えていませんでした。技術的には正しいソリューションですが、TabFragmentとそのinstanceOf BaseTabListFragmentの追加チェックが必要になります。内部でListViewを使用してFragmentを使用する別のアプローチ(実装されたListFragmentとまったく同じ)。考え直します。私にそれを指摘してくれてありがとう!
kasurd 2016年

1

簡単な解決策:

タブ/ルートビューの呼び出しを変更するたびに:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

BackStackをクリアします。ルートフラグメントを変更する前に、必ずこれを呼び出してください。

そして、これでフラグメントを追加します:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();

.addToBackStack(null)とは、transaction.addたとえばで変更できることに注意してくださいtransaction.replace


-1

このスレッドはとても面白くて役に立ちました。
説明とコードを提供してくれたKrishnabhadraに感謝します。私はあなたのコードを使用して少し改善し、スタックやcurrentTabなどを構成の変更(主に回転)から永続化できるようにしました。
実際の4.0.4および2.3.6デバイスでテスト済み、エミュレーターではテストされていません

「AppMainTabActivity.java」でコードのこの部分を変更しましたが、残りは同じままです。多分クリシュナバドラは彼のコードにこれを追加するでしょう。

作成時にデータを回復:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.app_main_tab_fragment_layout);

    /*  
     *  Navigation stacks for each tab gets created..
     *  tab identifier is used as key to get respective stack for each tab
     */

  //if we are recreating this activity...
    if (savedInstanceState!=null) {
         mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
         mCurrentTab = savedInstanceState.getString("currentTab");
    }
    else {
    mStacks = new HashMap<String, Stack<Fragment>>();
    mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
    mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());

    }

    mTabHost = (TabHost)findViewById(android.R.id.tabhost);
    mTabHost.setup();

    initializeTabs();

  //set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
    mTabHost.setOnTabChangedListener(listener);
}

変数を保存して、バンドルに入れます。

 //Save variables while recreating
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable("stack", mStacks);
    outState.putString("currentTab", mCurrentTab);
    //outState.putInt("tabHost",mTabHost);
}

以前のCurrentTabが存在する場合はこれを設定し、そうでない場合は新しいTab_Aを作成します。

public void initializeTabs(){
    /* Setup your tab icons and content views.. Nothing special in this..*/
    TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);

    spec.setContent(new TabHost.TabContentFactory() {
        public View createTabContent(String tag) {
            return findViewById(R.id.realtabcontent);
        }
    });
    spec.setIndicator(createTabView(R.drawable.tab_a_state_btn));
    mTabHost.addTab(spec);


    spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
    spec.setContent(new TabHost.TabContentFactory() {
        public View createTabContent(String tag) {
            return findViewById(R.id.realtabcontent);
        }
    });
    spec.setIndicator(createTabView(R.drawable.tab_b_state_btn));
    mTabHost.addTab(spec);

//if we have non default Tab as current, change it
    if (mCurrentTab!=null) {
        mTabHost.setCurrentTabByTag(mCurrentTab);
    } else {
        mCurrentTab=AppConstants.TAB_A;
        pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
    }
}

これが他の人の役に立つことを願っています。


これは間違っています。BundleでonCreateを呼び出すと、それらのフラグメントは画面に表示されるフラグメントとは異なり、setRetainInstanceを使用しない限り、古いフラグメントがリークします。また、ActivityManagerがアクティビティを「保存」すると、フラグメントはシリアル化もパーセルもできないため、ユーザーがアクティビティに戻るとクラッシュします。
sergio91pt 2014

-1

HashMapに基づくバックスタックを使用しないことをお勧めします>「アクティビティを保持しない」モードには多くのバグがあります。フラグメントのスタックを深く調べた場合、状態は正しく復元されません。また、ネストされたマップフラグメントで断片化されます(例外:フラグメント:IDのビューが見つかりません)。Coz HashMap> background \ foregroundアプリの後はnullになります

上記のコードをフラグメントのバックスタックでの作業用に最適化します

下TabViewです

主な活動クラス

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;

import com.strikersoft.nida.R;
import com.strikersoft.nida.abstractActivity.BaseActivity;
import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;

public class TagsActivity extends BaseActivity {
    public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
    private TabHost mTabHost;
    private String mCurrentTab;

    public static final String TAB_TAGS = "TAB_TAGS";
    public static final String TAB_MAP = "TAB_MAP";
    public static final String TAB_SETTINGS = "TAB_SETTINGS";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
        getActionBar().hide();
        setContentView(R.layout.tags_activity);

        mTabHost = (TabHost) findViewById(android.R.id.tabhost);

        mTabHost.setup();

        if (savedInstanceState != null) {
            mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
            initializeTabs();
            mTabHost.setCurrentTabByTag(mCurrentTab);
            /*
            when resume state it's important to set listener after initializeTabs
            */
            mTabHost.setOnTabChangedListener(listener);
        } else {
            mTabHost.setOnTabChangedListener(listener);
            initializeTabs();
        }
    }

    private View createTabView(final int id, final String text) {
        View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
        ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
        imageView.setImageDrawable(getResources().getDrawable(id));
        TextView textView = (TextView) view.findViewById(R.id.tab_text);
        textView.setText(text);
        return view;
    }

    /*
    create 3 tabs with name and image
    and add it to TabHost
     */
    public void initializeTabs() {

        TabHost.TabSpec spec;

        spec = mTabHost.newTabSpec(TAB_TAGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
        mTabHost.addTab(spec);

        spec = mTabHost.newTabSpec(TAB_MAP);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
        mTabHost.addTab(spec);


        spec = mTabHost.newTabSpec(TAB_SETTINGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
        mTabHost.addTab(spec);

    }

    /*
    first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
    for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
    */
    TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
        public void onTabChanged(String tabId) {

            mCurrentTab = tabId;

            if (tabId.equals(TAB_TAGS)) {
                pushFragments(SearchFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_MAP)) {
                pushFragments(MapContainerFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_SETTINGS)) {
                pushFragments(SettingsFragment.getInstance(), false,
                        false, null);
            }

        }
    };

/*
Example of starting nested fragment from another fragment:

Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
                TagsActivity tAct = (TagsActivity)getActivity();
                tAct.pushFragments(newFragment, true, true, null);
 */
    public void pushFragments(Fragment fragment,
                              boolean shouldAnimate, boolean shouldAdd, String tag) {
        FragmentManager manager = getFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        if (shouldAnimate) {
            ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
                    R.animator.fragment_slide_left_exit,
                    R.animator.fragment_slide_right_enter,
                    R.animator.fragment_slide_right_exit);
        }
        ft.replace(R.id.realtabcontent, fragment, tag);

        if (shouldAdd) {
            /*
            here you can create named backstack for realize another logic.
            ft.addToBackStack("name of your backstack");
             */
            ft.addToBackStack(null);
        } else {
            /*
            and remove named backstack:
            manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
            or remove whole:
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
             */
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        ft.commit();
    }

    /*
    If you want to start this activity from another
     */
    public static void startUrself(Activity context) {
        Intent newActivity = new Intent(context, TagsActivity.class);
        newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(newActivity);
        context.finish();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putString(M_CURRENT_TAB, mCurrentTab);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onBackPressed(){
        super.onBackPressed();
    }
}

tags_activity.xml

<

?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>
        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:background="@drawable/bg_main_app_gradient"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
        <TabWidget
            android:id="@android:id/tabs"
            android:background="#EAE7E1"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>
    </LinearLayout>
</TabHost>

tags_icon.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tabsLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/bg_tab_gradient"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="contentDescription" >

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_marginTop="4dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView 
        android:id="@+id/tab_text"
        android:layout_marginBottom="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/tab_text_color"/>

</LinearLayout>

ここに画像の説明を入力してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.