แยก Back Stack สำหรับแต่ละแท็บใน Android โดยใช้ Fragments


158

ฉันพยายามใช้แท็บเพื่อนำทางในแอพ Android เนื่องจาก TabActivity และ ActivityGroup เลิกใช้แล้วฉันต้องการใช้งานโดยใช้ Fragments แทน

ฉันรู้วิธีตั้งค่าหนึ่งส่วนสำหรับแต่ละแท็บแล้วสลับส่วนเมื่อมีการคลิกแท็บ แต่ฉันจะมีสแต็กหลังแยกต่างหากสำหรับแต่ละแท็บได้อย่างไร

ตัวอย่างเช่น Fragment A และ B จะอยู่ภายใต้ Tab 1 และ Fragment C และ D ภายใต้ Tab 2 เมื่อแอพเริ่มทำงาน Fragment A จะปรากฏขึ้นและเลือก Tab 1 จากนั้นแฟรกเมนต์ A อาจถูกแทนที่ด้วย Fragment B เมื่อแท็บ 2 ถูกเลือกแฟรกเมนต์ C ควรถูกแสดง หากแท็บ 1 ถูกเลือกแล้ว Fragment B ควรจะปรากฏขึ้นอีกครั้ง ณ จุดนี้คุณควรใช้ปุ่มย้อนกลับเพื่อแสดง Fragment A

นอกจากนี้สิ่งสำคัญคือต้องรักษาสถานะของแต่ละแท็บเมื่อหมุนอุปกรณ์

มาร์ติน BR

คำตอบ:


23

เฟรมเวิร์กจะไม่ทำสิ่งนี้ให้คุณโดยอัตโนมัติ คุณจะต้องสร้างและจัดการกองหลังของคุณเองสำหรับแต่ละแท็บ

ความจริงแล้วนี่เป็นเรื่องที่น่าสงสัยจริงๆ ฉันไม่สามารถจินตนาการได้ว่ามันจะส่งผลให้มี UI ที่เหมาะสม - ถ้าปุ่มย้อนกลับจะทำสิ่งต่าง ๆ ขึ้นอยู่กับแท็บที่ฉันเป็นโดยเฉพาะอย่างยิ่งถ้าปุ่มย้อนกลับยังมีพฤติกรรมปกติของการปิดกิจกรรมทั้งหมดเมื่ออยู่ด้านบน กอง ... ฟังดูน่ารังเกียจ

หากคุณกำลังพยายามสร้างบางสิ่งบางอย่างเช่นเว็บเบราว์เซอร์ UI เพื่อให้ได้ UX ที่เป็นธรรมชาติสำหรับผู้ใช้จะต้องมีการปรับแต่งพฤติกรรมที่ละเอียดอ่อนมากขึ้นอยู่กับบริบทดังนั้นคุณจะต้องทำ back stack ของคุณเอง การจัดการมากกว่าพึ่งพาการใช้งานเริ่มต้นในกรอบ ตัวอย่างเช่นลองสังเกตว่าปุ่มแบ็คโต้ตอบกับเบราว์เซอร์มาตรฐานในรูปแบบต่าง ๆ ที่คุณสามารถเข้าและออกได้อย่างไร (แต่ละหน้าต่าง "" ในเบราว์เซอร์เป็นแท็บ)


7
อย่าทำอย่างนั้น และกรอบก็ไม่มีประโยชน์อะไรเลย มันไม่ได้ให้การสนับสนุนโดยอัตโนมัติสำหรับสิ่งนี้ซึ่งฉันบอกว่าฉันไม่สามารถจินตนาการได้ว่าจะส่งผลให้ผู้ใช้ได้รับประสบการณ์ที่ดียกเว้นในสถานการณ์พิเศษที่คุณจะต้องควบคุมพฤติกรรมหลังอย่างระมัดระวัง
hackbod

9
การนำทางประเภทนี้แล้วคุณมีแท็บและลำดับชั้นของหน้าในแต่ละแท็บนั้นเป็นเรื่องธรรมดามากสำหรับแอปพลิเคชั่น iPhone (คุณสามารถตรวจสอบแอพของ App Store และ iPod) ฉันพบว่าประสบการณ์การใช้งานของพวกเขาค่อนข้างดี
Dmitry Ryadnenko

13
นี่มันบ้าไปแล้ว iPhone ไม่มีปุ่มย้อนกลับ มีการสาธิต API แสดงรหัสที่ง่ายมากในการใช้แฟรกเมนต์ในแท็บ คำถามที่ถามคือการมีแบ็คสแต็คที่แตกต่างกันสำหรับแต่ละแท็บและการตอบสนองของฉันคือเฟรมเวิร์กไม่ได้ให้สิ่งนี้โดยอัตโนมัติเพราะความหมายของปุ่มย้อนกลับที่มันน่าจะเป็นประสบการณ์ผู้ใช้ที่เลวร้ายที่สุด คุณสามารถนำอรรถศาสตร์กลับมาใช้ได้ง่ายถ้าคุณต้องการ
hackbod

4
อีกครั้งที่ iPhone ไม่มีปุ่มย้อนกลับดังนั้นมันจึงไม่มีพฤติกรรมการสแต็กกลับเช่น Android นอกจากนี้ "ดีกว่าที่จะเพียงแค่ยึดกิจกรรมและช่วยตัวเองเป็นจำนวนมากเวลา" ไม่สมเหตุสมผลเลยเพราะกิจกรรมไม่อนุญาตให้คุณใส่แท็บบำรุงรักษาใน UI ด้วยสแต็คหลังที่แตกต่างกัน ในความเป็นจริงกลับการจัดการสแต็คของกิจกรรมมีความยืดหยุ่นน้อยกว่าสิ่งที่ให้ไว้โดยกรอบส่วน
hackbod

22
@hackbod ฉันพยายามติดตามคะแนนของคุณ แต่มีปัญหาในการใช้พฤติกรรมแบ็คสแต็คที่กำหนดเอง ฉันรู้ว่าการมีส่วนร่วมในการออกแบบสิ่งนี้คุณจะต้องเข้าใจอย่างถ่องแท้ว่ามันอาจจะง่ายแค่ไหน เป็นไปได้ไหมที่มีแอพตัวอย่างสำหรับกรณีการใช้งานของ OP เนื่องจากเป็นสถานการณ์ที่พบได้บ่อยโดยเฉพาะอย่างยิ่งสำหรับพวกเราที่ต้องเขียน & แอพ iOS สำหรับลูกค้าที่ทำการร้องขอเหล่านี้ .... การจัดการแยกต่างหาก แฟรกเมนต์ backstacks ภายในแต่ละ FragmentActivity
Richard Le Mesurier

138

ฉันมาช้ามากกับคำถามนี้ แต่เนื่องจากหัวข้อนี้มีข้อมูลมากและเป็นประโยชน์กับฉันฉันคิดว่าฉันดีกว่าโพสต์เพนนีที่นี่

ฉันต้องการโฟลว์หน้าจอเช่นนี้ (การออกแบบที่เรียบง่ายด้วย 2 แท็บและ 2 มุมมองในแต่ละแท็บ)

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

ฉันมีข้อกำหนดเดียวกันในอดีตและฉันก็ใช้TabActivityGroup(ซึ่งเลิกใช้ในเวลานั้นด้วย) และกิจกรรม ครั้งนี้ฉันต้องการใช้ชิ้นส่วน

นี่คือสิ่งที่ฉันทำ

1. สร้างคลาส Fragment Class

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. ตกลงกิจกรรมแท็บหลัก - โปรดอ่านข้อคิดเห็นในโค้ด ..

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, simliar สำหรับแท็บทั้งหมด)

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

3
@omegatai เห็นด้วยกับคุณอย่างสมบูรณ์ .. ปัญหาทั้งหมดเกิดขึ้นเนื่องจาก Android ไม่ได้จัดการกองซ้อนสำหรับเรา ( ซึ่ง iOS ทำและเปลี่ยนการวางแนวหรือแท็บที่มีหลายส่วนเป็นเรื่องง่าย ) และนำเรากลับไปสู่การสนทนาดั้งเดิมใน Q / ด้าย ไม่ดีที่จะย้อนกลับไปในตอนนี้ ..
ฤษณะภัทรา

1
@Renjith นี่เป็นเพราะแฟรกเมนต์ถูกสร้างใหม่ทุกครั้งเมื่อคุณสลับแท็บอย่าคิดแม้แต่ครั้งเดียวแฟรกเมนต์ของคุณจะถูกนำกลับมาใช้ใหม่ผ่านสวิตช์แท็บ เมื่อฉันเปลี่ยนจากแท็บ A เป็น B แท็บ A จะถูกปลดปล่อยจากหน่วยความจำ ดังนั้นบันทึกข้อมูลของคุณในกิจกรรมและทุกครั้งที่ตรวจสอบว่ากิจกรรมมีข้อมูลก่อนที่จะพยายามรับจากเซิร์ฟเวอร์
Krishnabhadra

2
@ Krishnabhadra Ok เสียงดีกว่ามาก ให้ฉันแก้ไขในกรณีที่ฉันผิด ตามตัวอย่างของคุณมีเพียงหนึ่งกิจกรรมเท่านั้นดังนั้นจึงมีหนึ่งชุด สร้างอินสแตนซ์ของอะแดปเตอร์ใน BaseFragment (อ้างอิงโครงการของคุณ) และบันทึกข้อมูลในนั้น ใช้พวกเขาเมื่อใดก็ตามที่จะสร้างมุมมอง
Renjith

1
เตรียมพร้อมที่จะทำงาน ขอบคุณมาก. การอัปโหลดโครงการทั้งหมดเป็นความคิดที่ดี! :-)
Vinay W

96

เราต้องใช้พฤติกรรมเช่นเดียวกับที่คุณอธิบายสำหรับแอปเมื่อเร็ว ๆ นี้ หน้าจอและโฟลว์รวมของแอปพลิเคชั่นได้ถูกกำหนดไว้แล้วดังนั้นเราจึงต้องยึดติดกับมัน โชคดีที่เราจัดการเพื่อกำจัดปุ่มด้านหลังบนหน้าจอ :)

เราแฮ็คโซลูชันโดยใช้ส่วนผสมของ TabActivity, FragmentActivities (เราใช้ห้องสมุดสนับสนุนสำหรับชิ้นส่วน) และ Fragments ในการหวนกลับฉันค่อนข้างแน่ใจว่ามันไม่ใช่การตัดสินใจทางสถาปัตยกรรมที่ดีที่สุด แต่เราจัดการเพื่อให้มันทำงานได้ หากฉันต้องทำมันอีกครั้งฉันอาจลองทำตามกิจกรรมเพิ่มเติม (ไม่มีแฟรกเมนต์) หรือลองและมีเพียงกิจกรรมเดียวสำหรับแท็บและให้ส่วนที่เหลือทั้งหมดเป็นมุมมอง (ซึ่งฉันพบว่ามีอีกมาก นำมาใช้ซ้ำได้มากกว่ากิจกรรมโดยรวม)

ดังนั้นข้อกำหนดจะต้องมีแท็บและหน้าจอที่ซ้อนได้ในแต่ละแท็บ

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 มีเพียงหนึ่งแฟรกเมนต์ต่อครั้งและรู้วิธีแทนที่แฟรกเมนต์หนึ่งรายการสำหรับอีกอันหนึ่ง (ค่อนข้างคล้ายกับ 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) กำลังดูแล back-stacking ทั้งหมดด้วย FragmentManager ของตัวเอง

กิจกรรมอย่างเช่น ActivityInTab1 นั้นจะง่ายมากมันจะแสดงให้เห็นว่ามันเป็นส่วนแรก (หน้าจอ ie):

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 ที่มีประสบการณ์ว่าอะไรจะเป็นวิธีที่ดีกว่าในการทำให้ฟังก์ชั่นนี้ดีขึ้นและหากนี่ไม่ใช่ ทำ" ใน Android ของฉันขอบคุณถ้าคุณสามารถชี้ให้ฉันเชื่อมโยงหรือสาระบางอย่างที่อธิบายซึ่งเป็นวิธี Android ที่จะเข้าใกล้นี้ (แท็บหน้าจอที่ซ้อนกันในแท็บ ฯลฯ ) รู้สึกอิสระที่จะฉีกคำตอบนี้ในความคิดเห็น :)

ในฐานะที่เป็นสัญญาณว่าโซลูชันนี้ไม่ดีมากเมื่อเร็ว ๆ นี้ฉันต้องเพิ่มฟังก์ชั่นการนำทางไปยังแอปพลิเคชัน ปุ่มแปลกประหลาดบางอย่างที่ควรนำผู้ใช้จากแท็บหนึ่งไปยังอีกแท็บหนึ่งและเข้าสู่หน้าจอซ้อน การทำแบบนั้นโดยทางโปรแกรมนั้นเป็นความเจ็บปวดในก้นเพราะใครจะรู้ว่ามีปัญหาอะไรและจัดการกับเมื่อใดที่ชิ้นส่วนและกิจกรรมถูกยกตัวอย่างและเริ่มต้นจริง ๆ ฉันคิดว่าคงจะง่ายกว่านี้ถ้าหน้าจอและแท็บเหล่านั้นเป็นเพียงแค่การดูจริงๆ


สุดท้ายถ้าคุณต้องการเอาชีวิตรอดจากการเปลี่ยนแปลงการวางแนวสิ่งสำคัญคือการสร้างแฟรกเมนต์ของคุณโดยใช้ setArguments / getArguments หากคุณตั้งค่าตัวแปรอินสแตนซ์ในตัวสร้างแฟรกเมนต์ของคุณคุณจะถูกเมา แต่โชคดีที่มันง่ายต่อการแก้ไข: เพียงบันทึกทุกอย่างใน setArguments ในตัวสร้างแล้วเรียกสิ่งเหล่านั้นด้วย getArguments ใน onCreate เพื่อใช้งาน


13
คำตอบที่ดี แต่ฉันคิดว่าน้อยมากที่จะเห็นสิ่งนี้ ฉันเลือกเส้นทางเดียวกันทุกประการ (อย่างที่คุณเห็นจากการสนทนาในคำตอบก่อนหน้า) และฉันไม่พอใจกับมันเหมือนคุณ ฉันคิดว่า Google รู้สึกผิดกับชิ้นส่วนนี้มากเนื่องจาก API นี้ไม่ครอบคลุมกรณีการใช้งานที่สำคัญ ปัญหาอื่นที่คุณอาจพบคือความเป็นไปไม่ได้ที่จะฝังส่วนย่อยลงในส่วนอื่น
Dmitry Ryadnenko

ขอบคุณสำหรับความคิดเห็นโบลเดอ ใช่ฉันไม่เห็นด้วยเพิ่มเติมเกี่ยวกับ API ของแฟรกเมนต์ ฉันได้พบปัญหาของชิ้นส่วนซ้อนกัน (นั่นคือเหตุผลที่เราไปสำหรับ "แทนที่หนึ่งชิ้นส่วนด้วยวิธีอื่น" hehe)
epidemian

1
ฉันใช้สิ่งนี้ผ่านกิจกรรมทั้งหมด ฉันไม่ชอบสิ่งที่ฉันได้รับและฉันจะลองใช้แฟรกเมนต์ นั่นเป็นสิ่งที่ตรงกันข้ามกับประสบการณ์ของคุณ! มีการใช้งานกับกิจกรรมมากมายในการจัดการวงจรชีวิตของมุมมองลูกในแต่ละแท็บและใช้ปุ่มย้อนกลับของคุณเอง นอกจากนี้คุณไม่สามารถอ้างอิงถึงทุกมุมมองได้ไม่เช่นนั้นคุณจะระเบิดความจำ ฉันหวังว่าแฟรกเมนต์จะ: 1) สนับสนุนวงจรชีวิตของแฟรกเมนต์ที่มีการแยกหน่วยความจำอย่างชัดเจนและ 2) ช่วยในการใช้งานฟังก์ชั่นปุ่มย้อนกลับรวมถึงถ้าคุณใช้แฟรกเมนต์สำหรับกระบวนการนี้
gregm

จะเกิดอะไรขึ้นเมื่อผู้ใช้สลับแท็บ Fragment backstack จะถูกลบหรือไม่? จะมั่นใจได้อย่างไรว่า backstack ยังคงอยู่?
gregm

1
@ รวมถ้าคุณไป 1 แท็บ <-> กิจกรรม 1 อย่างที่ฉันทำแบ็คสแต็คสำหรับแต่ละแท็บจะยังคงอยู่เมื่อมีการสลับแท็บเพราะกิจกรรมนั้นยังมีชีวิตอยู่จริง ๆ พวกเขาถูกหยุดชั่วคราวและดำเนินการต่อเท่านั้น ฉันไม่ทราบว่ามีวิธีที่จะทำให้กิจกรรมถูกทำลายและสร้างขึ้นใหม่เมื่อมีการสลับแท็บใน TabActivity หรือไม่ อย่างไรก็ตามหากคุณทำให้ชิ้นส่วนภายในกิจกรรมถูกแทนที่อย่างที่ฉันแนะนำพวกมันจะถูกทำลาย (และจะถูกสร้างขึ้นใหม่เมื่อแบ็คสแต็คถูกตอก) ดังนั้นคุณจะมีแฟรกเมนต์ไม่เกินหนึ่งรายการต่อแท็บเมื่อใดก็ได้
23904 epidemic


6

การจัดเก็บการอ้างอิงที่รัดกุมเกี่ยวกับแฟรกเมนต์ไม่ใช่วิธีที่ถูกต้อง

FragmentManager ให้และputFragment(Bundle, String, Fragment)saveFragmentInstanceState(Fragment)

อย่างใดอย่างหนึ่งก็เพียงพอที่จะใช้ backstack


เมื่อใช้putFragmentแทนการแทนที่ชิ้นส่วนคุณจะถอดชิ้นส่วนเก่าออกและเพิ่มชิ้นส่วนใหม่ นี่คือสิ่งที่เฟรมเวิร์กทำกับธุรกรรมแทนที่ที่ถูกเพิ่มเข้ากับแบ็คสแต็ค putFragmentจัดเก็บดัชนีไปยังรายการปัจจุบันของแฟรกเมนต์ที่ใช้งานอยู่และแฟรกเมนต์เหล่านั้นจะถูกบันทึกโดยกรอบงานระหว่างการเปลี่ยนแปลงการวางแนว

วิธีที่สองใช้saveFragmentInstanceStateบันทึกสถานะของแฟรกเมนต์ทั้งหมดเป็น Bundle ซึ่งช่วยให้คุณสามารถลบออกได้จริงแทนที่จะลบออก การใช้วิธีการนี้จะช่วยให้การจัดการกองหลังทำได้ง่ายขึ้นเนื่องจากคุณสามารถเปิดส่วนย่อยได้ทุกเมื่อที่คุณต้องการ


ฉันใช้วิธีที่สองสำหรับ usecase นี้:

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

ฉันไม่ต้องการให้ผู้ใช้กลับไปที่หน้าจอลงทะเบียนจากที่สามโดยกดปุ่มย้อนกลับ ฉันยังทำภาพเคลื่อนไหวพลิกระหว่างพวกเขา (โดยใช้onCreateAnimation) ดังนั้นการแก้ปัญหาแฮ็คจะไม่ทำงานอย่างน้อยที่สุดหากไม่มีผู้ใช้จะสังเกตเห็นว่ามีบางอย่างไม่ถูกต้อง

นี่เป็นกรณีการใช้งานที่ถูกต้องสำหรับ backstack ที่กำหนดเองทำในสิ่งที่ผู้ใช้คาดหวัง ...

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

Disclaimer:


ฉันรู้สึกว่านี่เป็นสถานที่ที่ดีที่สุดในการโพสต์วิธีแก้ปัญหาที่เกี่ยวข้องที่ฉันทำงานเพื่อแก้ไขปัญหาที่คล้ายกันซึ่งดูเหมือนว่าจะเป็นอุปกรณ์มาตรฐานของ Android มันจะไม่แก้ปัญหาสำหรับทุกคน แต่อาจช่วยได้บ้าง


หากความแตกต่างหลักระหว่างแฟรกเมนต์ของคุณเป็นเพียงข้อมูลที่สำรองไว้ (เช่นไม่ใช่ความแตกต่างของเลย์เอาต์ขนาดใหญ่) ดังนั้นคุณอาจไม่จำเป็นต้องแทนที่แฟรกเมนต์จริงๆ แต่เพียงสลับข้อมูลพื้นฐานและรีเฟรชมุมมอง

ต่อไปนี้เป็นคำอธิบายตัวอย่างที่เป็นไปได้สำหรับวิธีนี้:

ฉันมีแอพที่ใช้ ListViews แต่ละรายการในรายการเป็นรายการหลักที่มีลูกจำนวนหนึ่ง เมื่อคุณแตะที่รายการรายการใหม่จะต้องเปิดพร้อมกับลูก ๆ เหล่านั้นภายในแท็บ ActionBar เดียวกันกับรายการเดิม รายการที่ซ้อนกันเหล่านี้มีเลย์เอาต์ที่คล้ายกันมาก (อาจมีการปรับแต่งตามเงื่อนไขที่นี่และที่นั่น) แต่ข้อมูลนั้นแตกต่างกัน

แอพนี้มีลูกหลานหลายเลเยอร์ภายใต้รายการพาเรนต์เริ่มต้นและเราอาจหรือไม่อาจมีข้อมูลจากเซิร์ฟเวอร์ตามเวลาที่ผู้ใช้พยายามเข้าถึงความลึกบางอย่างเกินกว่าครั้งแรก เนื่องจากรายการถูกสร้างขึ้นจากเคอร์เซอร์ฐานข้อมูลและแฟรกเมนต์ใช้ตัวโหลดเคอร์เซอร์และอะแดปเตอร์เคอร์เซอร์เพื่อเติมมุมมองรายการด้วยไอเท็มรายการทั้งหมดที่ต้องเกิดขึ้นเมื่อคลิกลงทะเบียนคือ:

1) สร้างอะแดปเตอร์ใหม่ที่มีฟิลด์ 'ถึง' และ 'จาก' ที่เหมาะสมซึ่งจะตรงกับมุมมองไอเท็มใหม่ที่ถูกเพิ่มในรายการและคอลัมน์ที่เคอร์เซอร์ใหม่ส่งคืน

2) ตั้งค่าอะแดปเตอร์นี้เป็นอะแดปเตอร์ใหม่สำหรับ ListView

3) สร้าง URI ใหม่ตามรายการที่คลิกและรีสตาร์ทเคอร์เซอร์โหลดเดอร์ด้วย URI ใหม่ (และการฉายภาพ) ในตัวอย่างนี้ URI ถูกแมปกับเคียวรีเฉพาะที่มีการเลือก args ที่ส่งผ่านจาก UI

4) เมื่อโหลดข้อมูลใหม่จาก URI ให้เปลี่ยนเคอร์เซอร์ที่เชื่อมโยงกับอะแดปเตอร์ไปเป็นเคอร์เซอร์ใหม่จากนั้นรายการจะรีเฟรช

ไม่มี backstack ที่เกี่ยวข้องกับสิ่งนี้เนื่องจากเราไม่ได้ใช้ธุรกรรมดังนั้นคุณจะต้องสร้างของคุณเองหรือเล่นแบบสอบถามในสิ่งที่ตรงกันข้ามเมื่อสำรองข้อมูลจากลำดับชั้น เมื่อฉันลองสิ่งนี้เคียวรีนั้นเร็วพอที่ฉันจะดำเนินการอีกครั้งใน 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 ตัวเท่านั้น แต่เป็นไปได้ ฉันใช้เวลาหลายวันในการสร้างห้องสมุดที่เรียกว่า Tab Stacker ซึ่งทำสิ่งที่คุณต้องการอย่างแท้จริง: ประวัติแยกส่วนสำหรับแต่ละแท็บ มันเป็นโอเพนซอร์ซและมีเอกสารครบถ้วนและสามารถรวมเข้ากับ gradle ได้อย่างง่ายดาย คุณสามารถค้นหาห้องสมุดได้ที่ github: https://github.com/smart-fun/TabStacker

คุณยังสามารถดาวน์โหลดแอปตัวอย่างเพื่อดูว่าพฤติกรรมนั้นตรงกับความต้องการของคุณ:

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

หากคุณมีคำถามใด ๆ อย่าลังเลที่จะวางอีเมล


2

ฉันต้องการแนะนำวิธีแก้ปัญหาของฉันเองในกรณีที่มีใครบางคนกำลังมองหาและต้องการลองและเลือกวิธีที่ดีที่สุดสำหรับความต้องการของเขา / เธอ

https://github.com/drusak/tabactivity

จุดประสงค์ในการสร้างห้องสมุดนั้นค่อนข้างซ้ำซาก - ปรับใช้เหมือนกับ iPhone

ข้อได้เปรียบหลัก:

  • ใช้ไลบรารี android.support.design ด้วย TabLayout;
  • แต่ละแท็บมีสแต็กของตัวเองโดยใช้ FragmentManager (โดยไม่บันทึกการอ้างอิงชิ้นส่วน)
  • สนับสนุนการเชื่อมโยงลึก (เมื่อคุณจำเป็นต้องเปิดแท็บเฉพาะและระดับของชิ้นส่วนเฉพาะในนั้น);
  • การบันทึก / กู้คืนสถานะของแท็บ;
  • วิธีวงจรชีวิตแบบปรับตัวของชิ้นส่วนในแท็บ;
  • ค่อนข้างง่ายที่จะใช้สำหรับความต้องการของคุณ

ขอบคุณสิ่งนี้มีประโยชน์มาก ฉันจำเป็นต้องใช้ListFragments นอกเหนือจากFragments ดังนั้นฉันจึงทำซ้ำ BaseTabFragment.java ไปที่ BaseTabListFragment.java และถ้ามันขยาย ListFragment จากนั้นฉันต้องเปลี่ยนส่วนต่าง ๆ ในรหัสที่มันคาดหวังเสมอว่า BaseTabFragment มีวิธีที่ดีกว่า?
Primehalo

น่าเสียดายที่ไม่ได้คิดถึง ListFragment ในทางเทคนิคมันเป็นทางออกที่ถูกต้อง แต่มันจะต้องมีการตรวจสอบเพิ่มเติมสำหรับ TabFragment และ instanceOf BaseTabListFragment อีกวิธีหนึ่งในการใช้ Fragment กับ ListView ข้างใน (เหมือนกับ ListFragment ที่ดำเนินการทั้งหมด) ฉันจะคิดมากกว่านั้น ขอบคุณที่ชี้ให้ฉันเห็น!
kasurd

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.addtransaction.replace


-1

หัวข้อนี้น่าสนใจมากและมีประโยชน์
ขอบคุณ Krishnabhadra สำหรับคำอธิบายและรหัสของคุณฉันใช้รหัสของคุณและปรับปรุงให้ดีขึ้นเล็กน้อยเพื่อให้สามารถคงอยู่สแต็คแท็บปัจจุบันและอื่น ๆ ... จากการกำหนดค่าการเปลี่ยนแปลง (หมุนส่วนใหญ่)
ทดสอบกับอุปกรณ์จริง 4.0.4 และ 2.3.6 ไม่ได้ทดสอบกับตัวจำลอง

ฉันเปลี่ยนรหัสส่วนนี้ใน "AppMainTabActivity.java" ส่วนที่เหลือยังคงเหมือนเดิม บางที Krishnabhadra จะเพิ่มรหัสนี้ในรหัสของเขา

กู้คืนข้อมูลบนสร้าง:

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);
}

บันทึกตัวแปรและนำไปที่ Bundle:

 //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);
    }
}

ฉันหวังว่านี่จะช่วยคนอื่นได้


นี่เป็นสิ่งที่ผิด เมื่อ onCreate ถูกเรียกด้วย Bundle ชิ้นส่วนเหล่านั้นจะไม่เหมือนกันที่จะปรากฏบนหน้าจอและคุณกำลังรั่วไหลของเก่ายกเว้นว่าคุณใช้ setRetainInstance และหาก ActivityManager "บันทึก" กิจกรรมของคุณเนื่องจาก Fragment ไม่ใช่ Serializable หรือ Parcelable เมื่อผู้ใช้กลับไปที่กิจกรรมของคุณมันจะหยุดทำงาน
sergio91pt

-1

ฉันจะแนะนำอย่าใช้ backstack ตาม HashMap> มีข้อบกพร่องมากมายในโหมด "ไม่เก็บกิจกรรม" จะไม่สามารถกู้คืนสถานะได้อย่างถูกต้องในกรณีที่คุณอยู่ในสแต็กของชิ้นส่วนอย่างลึกซึ้ง และจะถูกฝังในส่วนแผนที่ซ้อนกัน (ด้วย exeption: Fragment ไม่พบ ID) Coz HashMap> หลังจากแอป background \ foreground จะเป็นโมฆะ

ฉันเพิ่มประสิทธิภาพรหัสข้างต้นสำหรับการทำงานกับ backstack ของชิ้นส่วน

มันคือ 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.