Android: основной / подробный поток (двойная панель) с использованием 1 действия

Как указано в руководстве по Android, двойная панель может быть достигнуто двумя способами:

  1. Несколько фрагментов, одно действие
  2. Несколько фрагментов, несколько действий

Я использую первый случай (руководство по Android объясняет только второй случай).

Вот что происходит на 7-дюймовых планшетах:

  • поворот из альбомной в книжную: воссоздается только фрагмент, состоящий из одной панели
  • при повороте из книжной в альбомную: воссоздаются все 3 фрагмента (одинарная, двойная-мастерская, двойная-детальная)

Вопрос: почему однопанельный фрагмент (который я создаю программно, но использую FrameLayout, определенный в макете в качестве контейнера) воссоздается на двойной панели?

Я сообщаю ниже своей реализации:

/layout/activity_main.xml:

<FrameLayout
    android:id="@+id/single_pane"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment class="com.example.MasterFragment"
        android:id="@+id/master_dual"
        android:tag="MASTER_FRAGMENT_DUAL_PANE"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <fragment class="com.example.DetailFragment"
        android:id="@+id/detail_dual"
        android:tag="DETAIL_FRAGMENT_DUAL_PANE"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Это onCreate в основном действии:

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

    mDualPane = findViewById(R.id.dual_pane)!=null;

    FragmentManager fm = getFragmentManager();
    if (savedInstanceState==null) {
        // this is a non-UI fragment I am using for data processing purposes
        fm.beginTransaction().add(new NonUiFragment(), DATA_FRAGMENT).commit();
    }
    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        fm.beginTransaction().add(R.id.single_pane, new MasterFragment(), MASTER_FRAGMENT_SINGLE_PANE).commit();
    }
}

person Daniele B    schedule 21.08.2014    source источник


Ответы (2)


Я обнаружил, что гораздо лучше добавлять фрагменты в код также для двойной панели.

Таким образом, вместо использования <fragment> также используйте <FrameLayout> для двухпанельного XML.

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/master_dual"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <FrameLayout
        android:id="@+id/detail_dual"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Таким образом, вы можете использовать только один экземпляр masterFragment и DetailFragment, чтобы не столкнуться с проблемой наличия нескольких экземпляров одного и того же фрагмента.

Для этого в OnCreate нужно добавить фрагменты в контейнер, отсоединившись от старого контейнера:

    mDualPane = findViewById(R.id.dual_pane)!=null;

    if (savedInstanceState!=null) {
        mLastSinglePaneFragment = savedInstanceState.getString("lastSinglePaneFragment");
    }

    FragmentManager fm = getSupportFragmentManager();

    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(false);
        fm.beginTransaction().add(R.id.single_pane, masterFragment, MASTER_FRAGMENT).commit();
        if (mLastSinglePaneFragment==DETAIL_FRAGMENT) {
            openSinglePaneDetailFragment();
        }
    }
    if (mDualPane && fm.findFragmentById(R.id.master_dual)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(true);
        fm.beginTransaction().add(R.id.master_dual, masterFragment, MASTER_FRAGMENT).commit();
    }
    if (mDualPane && fm.findFragmentById(R.id.detail_dual)==null) {
        DetailFragment detailFragment = getDetatchedDetailFragment();
        fm.beginTransaction().add(R.id.detail_dual, detailFragment, DETAIL_FRAGMENT).commit();
    }

используя эти функции:

public static final String MASTER_FRAGMENT = "MASTER_FRAGMENT";
public static final String DETAIL_FRAGMENT = "DETAIL_FRAGMENT";

private MasterFragment getDetatchedMasterFragment(boolean popBackStack) {
    FragmentManager fm = getSupportFragmentManager();
    MasterFragment masterFragment = getSupportFragmentManager().findFragmentByTag(MASTER_FRAGMENT);
    if (masterFragment == null) {
        masterFragment = new MasterFragment();
    } else {
        if (popBackStack) {
            fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        fm.beginTransaction().remove(masterFragment).commit();
        fm.executePendingTransactions();
    }
    return masterFragment;
}

private DetailFragment getDetatchedDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    DetailFragment detailFragment = getSupportFragmentManager().findFragmentByTag(DETAIL_FRAGMENT);
    if (detailFragment == null) {
        detailFragment = new DetailFragment();
    } else {
        fm.beginTransaction().remove(detailFragment).commit();
        fm.executePendingTransactions();
    }
    return detailFragment;
}

private void openSinglePaneDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    DetailFragment detailFragment = getDetatchedDetailFragment();
    FragmentTransaction fragmentTransaction = fm.beginTransaction();
    fragmentTransaction.replace(R.id.single_pane, detailFragment, DETAIL_FRAGMENT);
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();
}
person Daniele B    schedule 17.05.2015

При повороте текущие активные фрагменты будут сохранены FragmentManager и использованы для автоматического воссоздания фрагментов при создании нового Activity. Вы можете предотвратить воссоздание, не передавая savedInstanceState методу super. Например. super.onCreate(null);.

В качестве альтернативы, если вам нужно восстановить состояние с помощью метода FragmentActivity.onCreate (savedInstanceState) (который вызывает FragmentManager.restoreAllState (), см. https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/FragmentManager.java#L1759) , вы можете найти свой тег фрагмента и удалить его вручную в файле onCreate. Это так, поскольку у вас есть фрагмент, отличный от пользовательского интерфейса, который вы хотите восстановить. Восстановление сохраненных фрагментов также зависит от вызова FragmentActivity.onCreate (savedInstanceState) с saveInstanceState! = Null.

Воссоздание происходит потому, что обычно вы хотите сохранить активный фрагмент (и, возможно, добавить вторую панель деталей в случае планшетов).

if (mDualPane) {
    Fragment singlePane = getFragmentManager().findFragmen‌​tByTag(MASTER_FRAGMENT_SINGLE_PANE);
    if (singlePane != null)
        getFragmentManager().beginTransaction().remove(fragment).commit(); 
}
person Pierre-Antoine LaFayette    schedule 21.08.2014
comment
Привет, Пьер-Антуан, я понял, что все 3 фрагмента хранятся менеджером фрагментов. Но я не понимаю, почему фрагмент с одной панелью должен быть воссоздан в наземном пространстве (несмотря на то, что его контейнер не находится в макете), в то время как вместо этого фрагменты с двумя панелями правильно не воссоздаются на портрете (поскольку они не находятся в макет). - person Daniele B; 21.08.2014
comment
Я не могу найти способ реализовать то, что вы говорите. Я определяю, является ли это двойной или одинарной панелью на основе макета R.id.dual_pane. У меня есть эта информация только после setContentView(), что, я думаю, должно быть после super.OnCreate(). - person Daniele B; 21.08.2014
comment
Это не имеет ничего общего с тем, существует ли контейнер в вашем представлении содержимого. FragmentManager создает новые экземпляры всех фрагментов, которые были активными после того, как изменение ориентации привело к уничтожению фрагментов. В FragmentActivity.onCreate () есть вызов mFragments.restoreAllState (), который создает экземпляры любых ранее активных фрагментов в пакете. См. github.com/android/platform_frameworks_base / blob / master / core /. Почему вы не можете сохранить свой код таким, какой он есть, и заменить вызов в первой строке на super.onCreate (null)? - person Pierre-Antoine LaFayette; 21.08.2014
comment
Вы не используете savedInstanceState и заменяете активный фрагмент (-ы) при каждом изменении ориентации, поэтому нет состояния, которое вам нужно восстанавливать с помощью родительского Activity. - person Pierre-Antoine LaFayette; 21.08.2014
comment
На самом деле я также использую фрагмент, не связанный с пользовательским интерфейсом. Я просто добавил это в код вопроса. Думаю, для этого мне нужно восстановить состояние активности, верно? - person Daniele B; 21.08.2014
comment
Я собирался сказать, что вы можете вызвать setRetainInstance (true) для фрагмента без пользовательского интерфейса, но похоже, что это также зависит от вызова restoreAllState. Поэтому в этом случае вам может потребоваться вручную удалить фрагмент при обнаружении двойной панели, просмотрев тег фрагмента и выполнив транзакцию remove (). - person Pierre-Antoine LaFayette; 21.08.2014
comment
Вы говорите, что FragmentManager создает новые экземпляры всех фрагментов, которые были активными после того, как изменение ориентации привело к уничтожению фрагментов. Так почему же при повороте из альбомной в портретную, onCreate не вызывается для фрагментов с двумя панелями? - person Daniele B; 22.08.2014
comment
Я только что понял, что использую onSaveInstanceState(Bundle outState) в MasterFragment для восстановления некоторых значений. Если я установлю super.onCreate(null) в Activity, я думаю, что outState не может быть получен :-( Мне нужно было бы переместить эти данные в не-UI-фрагмент? - person Daniele B; 22.08.2014
comment
Лучше всего сейчас просто вызвать super.onCreate (saveInstanceState), а затем вызвать getFragmentManager().beginTransaction().remove(getFragmentManager().findFragmentByTag(MASTER_FRAGMENT_SINGLE_PANE)).commit(); I.e. позвольте фрагменту одной панели создать экземпляр, а затем удалите его, когда вы обнаружите, что находитесь в режиме двух панелей. - person Pierre-Antoine LaFayette; 22.08.2014
comment
Разве это не означает, что outState в MasterFragment не будет восстановлен даже на смартфонах? - person Daniele B; 22.08.2014
comment
Смотрите мой обновленный ответ. В вашем случае я не рекомендую вызывать super.onCreate (null), так как вам нужно восстановить состояние. Вам просто нужно найти фрагмент одной панели, если вы переключаетесь на двойную панель, и удалить его. - person Pierre-Antoine LaFayette; 22.08.2014
comment
Я пытался сделать то, что ты сказал. Удаление (и фиксация!) Единственного фрагмента панели после setContentView(), но фрагмент, кажется, все еще существует даже после фиксации внутри Activity.onCreate(). И даже Fragment.OnCreate() вызывается. Я проверил, все ли сделано правильно. Кажется, что транзакции фрагментов фиксируются только после OnCreate (). - person Daniele B; 22.08.2014
comment
В идеале я ищу, чтобы поведение фрагмента с одной панелью было похоже на поведение фрагментов с двумя панелями. То есть: всегда оставаться в диспетчере фрагментов, без необходимости явного удаления, но в то же время не воссоздавать, когда это не нужно. Этого невозможно добиться? Почему фрагменты двойной панели (которые не удаляются!) Не воссоздаются на одной панели, а фрагмент одной панели воссоздается на двойной панели? - person Daniele B; 22.08.2014
comment
Это может быть интересно, потому что у вас есть фрагменты, определенные в ваших макетах для режима двойной панели. Я обычно не определяю такие фрагменты, поэтому не знаю наверняка. Вы пробовали определять однопанельный режим в файле макета портретного режима, а не в коде? - person Pierre-Antoine LaFayette; 22.08.2014
comment
Я не могу определить фрагмент с одной панелью в файле макета, потому что он должен быть заменяемым (как я уже говорил ранее в вопросе, я использую несколько фрагментов, подход с одним действием). Android не позволяет заменять определенные в макете фрагменты. Вот почему я определяю только контейнер FrameLayout в макете. - person Daniele B; 22.08.2014
comment
Тогда я думаю, вы застряли в том, чтобы позволить FragmentManager воссоздать экземпляр и удалить его. Иногда FragmentManager пытается быть слишком умным и не дает вам достаточно точного контроля зернистости. Надеюсь, они решат эту проблему в будущем выпуске (по аналогии с ListView и новой реализацией RecyclerView). - person Pierre-Antoine LaFayette; 22.08.2014
comment
На данный момент я не уверен, что есть смысл его удалять. Я думаю всегда сохранять этот фрагмент, состоящий из одной панели. Это может даже иметь некоторую выгоду, как если бы вы вращались назад и вперед между однопанельным и двухпанельным, однопанельное окно запомнило последний фрагмент. Я пытаюсь понять, какой в ​​этом может быть недостаток. На данный момент не нашел. Думаю, это зависит от вашего общего дизайна. В моем случае до сих пор я не испытывал серьезных проблем, позволяя фрагменту одной панели воссоздавать себя. - person Daniele B; 22.08.2014
comment
Кстати, спасибо Пьеру-Антуану за ваши мысли. - person Daniele B; 22.08.2014