Лучшие практики для вложенных фрагментов в Android 4.0, 4.1 (<4.2) без использования библиотеки поддержки

Я пишу приложение для планшетов 4.0 и 4.1, для которого я не хочу использовать библиотеки поддержки (если не нужны), но только для 4.x api.

Так что моя целевая платформа очень хорошо определена как: >= 4.0 и <= 4.1

Приложение имеет многопанельный макет (два фрагмента, один маленький слева, один фрагмент контента справа) и панель действий с вкладками.

Похоже на это:

Нажатие вкладки на панели действий изменяет "внешний" фрагмент, и внутренний фрагмент затем становится фрагментом с двумя вложенными фрагментами (1. маленький фрагмент списка слева, 2. фрагмент широкого содержимого).

Теперь мне интересно, как лучше заменить фрагменты и особенно вложенные фрагменты. ViewPager является частью библиотеки поддержки, для этого класса нет собственной альтернативы 4.x. Похоже, "устарела" в моем смысле. - http://developer.android.com/reference/android/support/v4/view/ViewPager.html

Затем я прочитал заметки о выпуске для Android 4.2, касающиеся ChildFragmentManager, что было бы хорошо, но я нацеливаюсь на 4.0 и 4.1, так что это тоже нельзя использовать.

ChildFragmentManager доступно только в 4.2

К сожалению, вряд ли найдутся хорошие примеры, демонстрирующие лучшие практики использования фрагментов без библиотеки поддержки, даже во всех руководствах разработчиков Android; и особенно ничего о вложенных фрагментах.

Поэтому мне интересно: неужели просто невозможно написать приложения 4.1 с вложенными фрагментами без использования библиотеки поддержки и всего, что с ней идет? (необходимо использовать FragmentActivity вместо Fragment и т. д.?) Или что было бы лучше?


Проблема, которую я сейчас испытываю при разработке, заключается именно в этом утверждении:

Библиотека поддержки Android также теперь поддерживает вложенные фрагменты, так что вы можете реализовать проекты с вложенными фрагментами на Android 1.6 и выше.

Примечание. Вы не можете раздувать макет на фрагмент, если этот макет содержит <fragment>, Вложенные фрагменты поддерживаются только при динамическом добавлении к фрагменту.

Потому что я положил определение вложенных фрагментов в XML, что, по-видимому, вызывает ошибку, такую ​​как:

Caused by: java.lang.IllegalArgumentException: Binary XML file line #15: Duplicate id 0x7f090009, tag frgCustomerList, or parent id 0x7f090008 with another fragment for de.xyz.is.android.fragment.CustomerListFragment_

На данный момент я делаю выводы для себя: даже на 4.1, когда я даже не хочу ориентироваться на платформу 2.x, вложенные фрагменты, как показано на скриншоте, невозможны без библиотеки поддержки.

(На самом деле это может быть больше вики, чем вопрос, но, возможно, кто-то еще справился с этим раньше).

Обновить:

Полезный ответ: фрагмент внутри фрагмента

5 ответов

Решение

Ограничения

Таким образом, размещение фрагментов внутри другого фрагмента невозможно с помощью XML независимо от того, какая версия FragmentManager ты используешь.

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

Так что вложение без использования getChildFragmentManger? Суть позади childFragmentManager является то, что он откладывает загрузку до завершения предыдущей транзакции фрагмента. И, конечно, это только естественно поддерживалось в 4.2 или в библиотеке поддержки.

Вложение без ChildManager - решение

Решение, конечно! Я делаю это в течение долгого времени, (так как ViewPager было объявлено).

Увидеть ниже; Это Fragment что откладывает загрузку, так Fragments могут быть загружены внутри него.

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

// Remember this is an example, you will need to modify to work with your code
private final Handler handler = new Handler();
private Runnable runPager;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    return inflater.inflate(R.layout.frag_layout, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState)
{
    super.onActivityCreated(savedInstanceState);
    runPager = new Runnable() {

        @Override
        public void run()
        {
          getFragmentManager().beginTransaction().addFragment(R.id.frag_container, MyFragment.newInstance()).commit();
        }
    };
    handler.post(runPager);
}

/**
 * @see android.support.v4.app.Fragment#onPause()
 */
@Override
public void onPause()
{
    super.onPause();
    handler.removeCallbacks(runPager);
}

Я бы не стал считать это "лучшей практикой", но у меня есть живые приложения, использующие этот хак, и у меня еще не возникло проблем с ним.

Я также использую этот метод для встраивания видовых пейджеров - https://gist.github.com/chrisjenx/3405429

Лучший способ сделать это в pre-API 17 - вообще не делать этого. Попытка реализовать это поведение вызовет проблемы. Однако это не означает, что он не может быть убедительно подделан с использованием текущего API 14. Я сделал следующее:

1 - посмотреть на связь между фрагментами http://developer.android.com/training/basics/fragments/communicating.html

2 - переместите макет xml FrameLayout из существующего фрагмента в макет "Активность" и скройте его, задав высоту 0:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent">
<FrameLayout android:id="@+id/content"
          android:layout_width="300dp"
          android:layout_height="match_parent" />


<FrameLayout android:id="@+id/lstResults"
             android:layout_width="300dp"
             android:layout_height="0dp"
             android:layout_below="@+id/content"
             tools:layout="@layout/treeview_list_content"/>


<FrameLayout android:id="@+id/anomalies_fragment"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
        android:layout_toRightOf="@+id/content" />

3 - Реализовать интерфейс в родительском фрагменте

    OnListener mCallback;

// Container Activity must implement this interface
public interface OnListener 
{
    public void onDoSomethingToInitChildFrame(/*parameters*/);
    public void showResults();
    public void hideResults();
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);

    // This makes sure that the container activity has implemented
    // the callback interface. If not, it throws an exception
    try {
        mCallback = (OnFilterAppliedListener) activity;
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString()
                + " must implement OnListener");
    }
}

@Override
public void onActivityCreated(Bundle savedInstanceState) 
{
    super.onActivityCreated(savedInstanceState);

    mCallback.showResults();
}

@Override
public void onPause()
{
    super.onPause();

    mCallback.hideResults();
}

public void onClickButton(View view)
{
    // do click action here

    mCallback.onDoSomethingToInitChildFrame(/*parameters*/);
}

4 - Реализовать интерфейс в родительском Activity

открытый класс YourActivity расширяет Activity, реализует yourParentFragment.OnListener {

public void onDoSomethingToInitChildFrame(/*parameters*/)
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment == null)
    {
        childFragment = new yourChildFragment(/*parameters*/);
        ft.add(R.id.lstResults, childFragment, "Results");
    }
    else
    {
        ft.detach(childFragment);

        ((yourChildFragment)childFragment).ResetContent(/*parameters*/);

        ft.attach(childFragment);
    }
    ft.commit();

    showResultsPane();
}

public void showResults()
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.attach(childFragment);
    ft.commit();

    showResultsPane();
}

public void showResultsPane()
{
    //resize the elements to show the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
    findViewById(R.id.lstResults).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
}

public void hideResults()
{
    //resize the elements to hide the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
    findViewById(R.id.lstResults).getLayoutParams().height = 0;

    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.detach(childFragment);
    ft.commit();
}

}

5 - наслаждайтесь, с этим методом вы получаете ту же функциональную функциональность, что и с функцией getChildFragmentManager() в среде до API 17. Как вы, возможно, заметили, дочерний фрагмент больше не является дочерним по отношению к родительскому фрагменту, но теперь является дочерним по отношению к действию, этого действительно нельзя избежать.

Мне пришлось решать именно эту проблему из-за сочетания NavigationDrawer, TabHost и ViewPager, что имело сложности с использованием вспомогательной библиотеки из-за TabHost. И затем мне также пришлось поддерживать минимальный API JellyBean 4.1, поэтому использование вложенных фрагментов с getChildFragmentManager не было возможным.

Так что моя проблема может быть решена до...

 TabHost (для верхнего уровня)
+ ViewPager (только для одного из фрагментов с вкладками верхнего уровня)
= потребность во вложенных фрагментах (которые JellyBean 4.1 не будет поддерживать) 

Моим решением было создать иллюзию вложенных фрагментов без фактического вложения фрагментов. Я сделал это, используя основное действие, используя TabHost AND ViewPager для управления двумя одноуровневыми представлениями, видимость которых управляется переключением layout_weight между 0 и 1.

//Hide the fragment used by TabHost by setting height and weight to 0
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 0);
mTabHostedView.setLayoutParams(lp);
//Show the fragment used by ViewPager by setting height to 0 but weight to 1
lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
mPagedView.setLayoutParams(lp);

Это фактически позволило моему фальшивому "вложенному фрагменту" работать как независимое представление, пока я вручную управлял соответствующими весами макета.

Вот мой Activity_main.xml:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.ringofblades.stackru.app.MainActivity">

    <TabHost
        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:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"/>
            <android.support.v4.view.ViewPager
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/pager"
                android:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"
                tools:context="com.ringofblades.stackru.app.MainActivity">
                <FrameLayout
                    android:id="@+id/container"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
            </android.support.v4.view.ViewPager>
            <TabWidget android:id="@android:id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </TabHost>

    <fragment android:id="@+id/navigation_drawer"
        android:layout_width="@dimen/navigation_drawer_width"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:name="com.ringofblades.stackru.app.NavigationDrawerFragment"
        tools:layout="@layout/fragment_navigation_drawer" />
</android.support.v4.widget.DrawerLayout>

Обратите внимание, что "@+id/pager" и "@+id/container" - это братья и сестры с android:layout_weight="0.5" и android:layout_height="0dp"'. Это так, что я могу видеть его в программе предварительного просмотра для любого размера экрана. В любом случае, их веса будут обрабатываться в коде во время выполнения.

Опираясь на ответ @Chris.Jenkins, это решение, которое хорошо мне подходит, для удаления фрагментов во время событий жизненного цикла (которые имеют тенденцию генерировать исключения IllegalStateExceptions). При этом используется комбинация подхода Handler и проверки Activity.isFinishing() (в противном случае будет выдано сообщение об ошибке "Невозможно выполнить это действие после onSaveInstanceState).

import android.app.Activity;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

public abstract class BaseFragment extends Fragment {
    private final Handler handler = new Handler();

    /**
     * Removes the {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragment The {@link Fragment} to schedule for removal.
     */
    protected void removeFragment(@Nullable final Fragment fragment) {
        if (fragment == null) return;

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    getFragmentManager().beginTransaction()
                            .remove(fragment)
                            .commitAllowingStateLoss();
                }
            }
        });
    }

    /**
     * Removes each {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragments The {@link Fragment}s to schedule for removal.
     */
    protected void removeFragments(final Fragment... fragments) {
        final FragmentManager fragmentManager = getFragmentManager();
        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

        for (Fragment fragment : fragments) {
            if (fragment != null) {
                fragmentTransaction.remove(fragment);
            }
        }

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    fragmentTransaction.commitAllowingStateLoss();
                }
            }
        });
    }
}

Использование:

class MyFragment extends Fragment {
    @Override
    public void onDestroyView() {
        removeFragments(mFragment1, mFragment2, mFragment3);
        super.onDestroyView();
    }
}

Хотя у ОП могут быть особые обстоятельства, которые не позволяют ему использовать Библиотеку поддержки, большинство людей должны использовать ее. Документация по Android рекомендует это, и оно сделает ваше приложение доступным для самой широкой аудитории.

В своем более полном ответе здесь я сделал пример, демонстрирующий, как использовать вложенные фрагменты с библиотекой поддержки.

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

Другие вопросы по тегам