Окремий стек для кожної вкладки в Android за допомогою фрагментів


158

Я намагаюся реалізувати вкладки для навігації в додатку для Android. Оскільки TabActivity та ActivityGroup застаріли, я хотів би реалізувати його, використовуючи фрагменти.

Я знаю, як налаштувати по одному фрагменту для кожної вкладки, а потім переключити фрагменти, коли натиснути вкладку. Але як я можу мати окремий задній стек для кожної вкладки?

Наприклад, фрагменти A і B будуть розташовані під вкладкою 1, а фрагмент C і D під вкладкою 2. Коли програма запущена, відображається фрагмент A і вибирається закладка 1. Тоді Фрагмент А може бути замінений на Фрагмент В. Коли вибрано Tab 2, Фрагмент C повинен відображатися. Якщо вибрано Tab 1, фрагмент B повинен бути відображений ще раз. У цей момент, щоб показати фрагмент А., слід використовувати кнопку "назад".

Також важливо, щоб стан кожної вкладки підтримувався при обертанні пристрою.

BR Martin

Відповіді:


23

Рамка наразі не зробить це автоматично для вас. Вам потрібно буде створити та керувати власними задніми стеками для кожної вкладки.

Якщо чесно, це здається дійсно сумнівною справою. Я не можу уявити, що це призведе до пристойного інтерфейсу користувача - якщо клавіша "назад" буде робити різні речі залежно від вкладки, на якій я перебуваю, особливо якщо клавіша "назад" також має звичайну поведінку закриття всієї активності, коли знаходиться вгорі стек ... звучить неприємно.

Якщо ви намагаєтеся створити щось на зразок інтерфейсу веб-браузера, для отримання природного для користувача UX буде задіяно багато тонких налаштувань поведінки залежно від контексту, тому вам обов'язково потрібно буде зробити свій власний задній стек а не покладатися на деяку реалізацію за замовчуванням у рамках. Для прикладу спробуйте звернути увагу на те, як задня клавіша взаємодіє зі стандартним браузером різними способами входу та виходу з неї. (Кожне "вікно" в браузері по суті є вкладкою.)


7
Не робіть цього. І рамки навряд чи марні. Це не дає вам автоматичної підтримки подібних речей, що, як я вже сказав, я не можу уявити, що це призведе до гідного користувальницького досвіду, за винятком дуже спеціалізованих ситуацій, коли вам потрібно буде ретельно контролювати поведінку спини.
хакбод

9
Цей тип навігації, тоді ви маєте вкладки та ієрархію сторінок на кожній вкладці, дуже поширені для додатків iPhone, наприклад (ви можете перевірити додатки App Store та iPod). Я вважаю, що їх досвід роботи досить пристойний.
Дмитро Рядненко

13
Це божевільно. У iPhone навіть немає кнопки повернення. Існують демо-версії API, які показують дуже простий код для реалізації фрагментів у вкладках. Питання, що задається, стосується того, що для кожної вкладки є різні зворотні стеки, і моя відповідь полягає в тому, що рамки не постачають це автоматично, оскільки семантично для того, що робить кнопка "назад", це, швидше за все, буде шаленим користувачем. Ви можете досить легко реалізувати назад семантику, якщо хочете.
хакбод

4
Знову ж таки, у iPhone немає кнопки "назад", тому вона не має семантичної поведінки, як Android. Крім того, «краще просто дотримуватися діяльностей і врятувати себе багато часу» не має ніякого сенсу тут, тому що діяльність не дозволяє ставити підтримувати вкладки в призначеному для користувача інтерфейсі зі своїм різним заднім штабелем; насправді управління операціями із зворотним стеком є ​​менш гнучким, ніж те, що передбачено рамкою Fragment.
хакбод

22
@hackbod Я намагаюся дотримуватися ваших моментів, але у мене виникли проблеми з реалізацією власної поведінки в режимі резервного стека. Я усвідомлюю, що, долучившись до розробки цього питання, ви мали б чітке уявлення про те, наскільки це легко. Можливо, є демо-додаток для випадку використання ОП, оскільки це насправді дуже поширена ситуація, особливо для тих із нас, хто повинен писати та переносити програми iOS для клієнтів, які роблять ці запити .... управління окремими фрагменти backstacks у межах кожної FragmentActivity.
Річард Ле Месюр'є

138

Я страшенно запізнююся на це питання. Але оскільки ця тема була дуже інформативною та корисною для мене, я подумав, що краще розмістити свої два пенси тут.

Мені потрібен був такий потік екрана (мінімалістичний дизайн з 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. Створіть кілька ідентифікаторів Tab, доступних скрізь у проекті

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, подібний для всіх вкладок)

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
Зберігання даних для кожного фрагмента, відтворення кожного з них, відновлення стеків ... так багато роботи для простої зміни орієнтації.
Майкл Ейлерс Сміт

3
@omegatai повністю згоден з вами. Вся проблема виникає, оскільки Android не управляє стеком для нас ( що робить iOS і зміна орієнтації чи вкладка з декількома фрагментами - вітер ), і це повертає нас до початкової дискусії в цьому запитанні / Нитка. Немає нічого хорошого, щоб повернутися до цього зараз ..
Крішнабхадра

1
@Renjith Це тому, що фрагмент відтворюється кожен раз, коли ви перемикаєте вкладку. Не думайте навіть про те, що ваш фрагмент повторно використовується через перемикач вкладки. при переході з вкладки "А" на "В" вкладка "" звільняється від пам'яті. Тому зберігайте свої дані в активності та кожен раз перевіряйте, чи є у діяльності дані, перш ніж намагатися отримати їх із сервера.
Крішнабхадра

2
@Krishnabhadra Добре, це звучить набагато краще. Дозвольте виправити, якщо я помиляюся. Згідно з вашим прикладом, є лише одна діяльність і, отже, один комплект. Створіть екземпляри адаптера в BaseFragment (оновлення проекту) і збережіть там дані. Використовуйте їх, коли має бути побудований вид.
Ренджіт

1
Почав працювати. Дуже дякую. Завантаження всього проекту було гарною ідеєю! :-)
Vinay W

96

Нам довелося реалізувати саме таку поведінку, яку ви описували для програми нещодавно. Екрани та загальний потік програми були вже визначені, тому нам довелося дотримуватися цього (це клон додатків для iOS ...). На щастя, нам вдалося позбутися екранних кнопок повернення :)

Ми зламали рішення, використовуючи суміш 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) піклується про всі бек-стеки за допомогою власного 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, що було б краще підходити до використання цієї функціональності, і якщо це не так "як це зроблено »в Android, я був би вдячний , якщо ви могли б вказати мені на яку - то посилання або матеріал , який пояснює , що Android спосіб наблизитися до цього (вкладки, вкладені екрани в закладках і т.д.). Сміливо відривайте цю відповідь у коментарях :)

Як знак того, що це рішення не дуже добре, це те, що останнім часом мені довелося додати деяку функціонал навігації у додаток. Якась химерна кнопка, яка має перенести користувача з однієї вкладки в іншу та вкладений екран. Це робити програмно було болем у попці, через те, хто-хто-хто знає проблеми та вирішувати, коли фрагменти та діяльність насправді створені та ініціалізовані. Я думаю, було б набагато простіше, якби ці екрани та вкладки були справді просто Переглядами.


Нарешті, якщо вам потрібно пережити зміни орієнтації, важливо, щоб ваші фрагменти були створені за допомогою setArguments / getArguments. Якщо ви встановите змінні екземпляра в конструкторах ваших фрагментів, ви будете накручені. Але, на щастя, це виправити дуже просто: просто збережіть усе у setArgument у конструкторі, а потім отримайте ті речі за допомогою getArguments in onCreate, щоб використовувати їх.


13
Чудова відповідь, але я думаю, що мало хто це побачить. Я вибрав абсолютно такий самий шлях (як ви бачите з розмови в попередній відповіді), і я не задоволений ним, як і ви. Я думаю, що Google справді перекрутив ці фрагменти, оскільки цей API не охоплює основних випадків використання. Інша проблема, з якою ви можете зіткнутися, - це неможливість вбудовування фрагмента в інший фрагмент.
Дмитро Рядненко

Дякуємо за коментар валун. Так, я не міг домовитися більше про API фрагментів. Я вже зіткнувся з проблемою вкладених фрагментів (саме тому ми пішли за підхід «замінити один фрагмент іншим»).
епідемія

1
Я реалізував це через ВСІ заходи. Мені не сподобалося те, що я отримав, і я збираюся спробувати фрагменти. Це навпаки вашого досвіду! Існує велика реалізація заходів, спрямованих на реалізацію життєвого циклу дитячих переглядів у кожній вкладці, а також на реалізацію власної кнопки "назад". Крім того, ви не можете просто зберегти посилання на всі погляди, або ви підірвете пам'ять. Я сподіваюся, що фрагменти будуть: 1) підтримувати життєвий цикл фрагментів з чітким розділенням пам’яті та 2) допоможе реалізувати функціональність кнопки «Назад» Плюс, якщо ви використовуєте фрагменти для цього процесу, чи не буде простішим запуском планшетів?
gregm

Що відбувається, коли користувач перемикає вкладки? Чи видаляється backstack "Фрагмент"? Як переконатися, що рюкзак залишився?
gregm

1
@gregm Якщо ви перейдете на 1 вкладку <-> 1 активність, як я робив, backstack для кожної вкладки залишатиметься при переключенні вкладок, оскільки дії фактично зберігаються в живих; вони лише призупиняються та відновлюються. Я не знаю, чи є спосіб знищити та відновити діяльність при переключенні вкладок у TabActivity. Тим НЕ менше, якщо ви зробите фрагменти всередині діяльності будуть замінені , як я припустив, що вони будуть знищені (і відтвореної при backstack виштовхується). Тож у будь-який час ви матимете щонайменше один фрагмент на вкладці.
епідемія


6

Збереження чітких посилань на фрагменти - не правильний спосіб.

FragmentManager забезпечує putFragment(Bundle, String, Fragment)іsaveFragmentInstanceState(Fragment) .

Або одного достатньо, щоб реалізувати backstack.


Використовуючи putFragment, замість того, щоб замінювати фрагмент, ви від'єднуєте старий і додаєте новий. Це те, що рамки роблять для транзакції на заміну, яка додається до backstack.putFragmentзберігає індекс до поточного списку активних фрагментів, і ті фрагменти зберігаються рамкою під час змін орієнтації.

Другий спосіб, використовуючи saveFragmentInstanceState, зберігає весь стан фрагмента до розшарування, дозволяючи реально видалити його, а не від'єднувати його. Використання цього підходу полегшує маніпулювання заднім стеком, оскільки ви можете виводити фрагмент, коли захочете.


Я використовував другий метод для цієї справи:

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

Відмова від відповідальності:


Я вважаю, що це найкраще місце для розміщення пов'язаного рішення, над яким я працював над подібним типом проблеми, яка, здається, є досить стандартною інформацією про Android. Це не вирішить проблему для всіх, але це може допомогти деяким.


Якщо первинна відмінність ваших фрагментів - це лише резервне копіювання даних (тобто не велика різниця у макеті), можливо, вам не потрібно буде фактично замінювати фрагмент, а просто поміняти основні дані та оновити подання.

Ось один із можливих прикладів такого підходу:

У мене є додаток, який використовує ListViews. Кожен елемент у списку є батьком з деякою кількістю дітей. Коли ви торкаєтесь цього пункту, потрібно відкрити новий список із цими дітьми на тій же вкладці ActionBar, що і вихідний список. Ці вкладені списки мають дуже схожий макет (можливо, деякі умовні зміни тут і там можливо), але дані різні.

У цього додатка є кілька шарів нащадків під початковим списком батьків, і ми можемо мати або не мати даних із сервера до моменту, коли користувач намагається отримати доступ до будь-якої певної глибини за межі першого. Оскільки список побудований з курсору бази даних, а фрагменти використовують завантажувач курсора та адаптер курсору для заповнення списку списку елементами списку, все, що повинно відбутися при реєстрації кліку, це:

1) Створіть новий адаптер з відповідними полями «до» та «від», які відповідатимуть новим переглядам елементів, доданим до списку, та стовпцям, повернутим новим курсором.

2) Встановіть цей адаптер як новий адаптер для ListView.

3) Створіть новий URI на основі елемента, на який було натиснуто, та перезапустіть завантажувач курсорів новим URI (та проекцією). У цьому прикладі URI відображається на конкретні запити з аргументами вибору, переданими з інтерфейсу користувача.

4) Коли нові дані завантажуються з URI, поміняйте курсор, пов'язаний з адаптером, на новий курсор, і список оновиться.

З цим немає ніякого backstack, оскільки ми не використовуємо транзакції, тому вам доведеться або створити власні, або зіграти запити в зворотному порядку, коли виходите з ієрархії. Коли я спробував це, запити були досить швидкими, що я просто виконую їх знову в oNBackPress () до тих пір, поки я не стану на вершині ієрархії, і в цей момент рамка знову переймає кнопку назад.

Якщо ви опинилися в подібній ситуації, обов'язково прочитайте документи: 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 і сумісний назад до рівня 8 API.


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. Ще один підхід до використання фрагмента з 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.addможе, наприклад, змінити transaction.replace.


-1

Ця нитка була дуже дуже цікавою та корисною.
Дякую Кришнабхадрі за ваше пояснення та код, я використовую ваш код і трохи покращився, дозволяючи зберігати стеки, currentTab тощо тощо від зміни конфігурації (в основному обертається).
Тестовано на реальних пристроях 4.0.4 та 2.3.6, не перевірених на емуляторі

Я змінюю цю частину коду на "AppMainTabActivity.java", решта залишаюсь такою ж. Можливо, Крішнабхадра додасть це до свого коду.

Відновлення даних onCreate:

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

Я сподіваюся, що це допомагає іншим людям.


Це неправильно. Коли onCreate викликається з Bundle, ці фрагменти не будуть тими ж, які будуть показані на екрані, і ви протікаєте старі, якщо ви не використовуєте setRetainInstance. І якщо ActivityManager "збереже" вашу активність, оскільки фрагмент не є серіалізаційним і не розбірним, коли користувач повернеться до вашої активності, він вийде з ладу.
sergio91pt

-1

Я б рекомендував не використовувати backstack на основі HashMap> в режимі "не тримати діяльність" є багато помилок. Він не буде належним чином відновити стан, якщо ви знаходитесь в стеці фрагмента. А також буде розміщено у вкладеному фрагменті карти (з винятком: Фрагмент для 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>

введіть тут опис зображення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.