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

115

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

Итак, моя целевая платформа очень хорошо определена как:> = 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, вложенные фрагменты, как показано на скриншоте, невозможны без библиотеки поддержки.

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

Обновить:

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

Матиас Конрад
источник
22
У вас есть три варианта: 1. Таргетинг только на 4.2 с собственными вложенными фрагментами. 2. Целевой объект 4.x с вложенными фрагментами из библиотеки поддержки. 3. Не используйте вложенные фрагменты для любых других сценариев целевой платформы. Это должно ответить на ваш вопрос. Кроме того, вы не можете использовать вложенные фрагменты, встроенные в макет xml, все они должны быть добавлены в код. вряд ли есть хорошие примеры, демонстрирующие передовые методы использования фрагментов без библиотеки поддержки - структура фрагментов поддержки копирует родную, поэтому любой пример должен работать в любом случае.
Luksprog
@Luksprog Спасибо за ваши комментарии. Я предпочитаю ваше решение 2, и фрагменты хорошо работают в библиотеке поддержки, но вкладки в ActionBar не работают - черт возьми, мне нужно было бы использовать ActionBarSherlock, но вкладки не будут интегрированы в ActionBar тогда, а только под ним (что не t необходимо для 4.x). И ActionBar.TabListener поддерживает только фрагменты из android.app.Fragment, а не из библиотеки поддержки.
Матиас Конрадт
2
Я не знаком с приложением «Контакты» на вкладке «Galaxy», но имейте в виду, что вы всегда можете столкнуться с пользовательской реализацией ActionBar(встроенной компанией Samsung). Присмотритесь к ActionBarSherlock, у него есть вкладки в ActionBar, если есть место.
Luksprog
4
@Luksprog Я считаю, что вы уже предоставили единственный ответ, который можно дать, не могли бы вы дать его как правильный ответ.
Warpzit
1
@Pork Моя основная причина вопроса: есть ли обходные пути для вложенных фрагментов без использования библиотеки поддержки и всех остальных элементов представления. Это означает, что если я переключусь на библиотеку поддержки, я буду использовать FragmentActivity вместо Fragment. Но я хочу использовать фрагмент, все, что мне нужно, - это замена вложенных фрагментов , но не всех компонентов v4. То есть через другие библиотеки с открытым исходным кодом и т.д. Например, приведенный выше снимок экрана работает на 4.0, и мне интересно, используют ли они ABS, SupportLib или что-то еще.
Mathias Conradt

Ответы:

60

Ограничения

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

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

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

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

Решение, конечно! Я этим давно занимаюсь (с тех пор, как ViewPagerбыл анонсирован).

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

Это довольно просто, 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

Chris.Jenkins
источник
Как вы обрабатываете вложенные фрагменты макета?
паблиско
Единственный способ увидеть, как это работает, - использовать CustomLayoutInflater, когда вы встретите fragmentэлемент, вы переопределите супер-реализацию и попытаетесь самостоятельно разобрать / раздуть его. Но это потребует ОЧЕНЬ больших усилий, что выходит за рамки вопроса StackOverflow.
Chris.Jenkins
Привет, может ли кто-нибудь помочь мне в этом вопросе ?? Я действительно застрял .. stackoverflow.com/questions/32240138/…
Nicks
2

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

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

2 - переместите макет xml FrameLayout из существующего фрагмента в макет Activity и скройте его, указав высоту 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 extends 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. Как вы, возможно, заметили, дочерний фрагмент больше не является дочерним по отношению к родительскому фрагменту, а теперь является дочерним по отношению к активности, этого действительно нельзя избежать.

Alex
источник
1

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

Так что мою проблему можно свести к ...

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

Мое решение состояло в том, чтобы создать иллюзию вложенных фрагментов без фактического вложения фрагментов. Я сделал это, заставив основное действие использовать TabHost И 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.stackoverflow.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.stackoverflow.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.stackoverflow.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"'. Это для того, чтобы я мог видеть это в превью при любом размере экрана. В любом случае их веса будут обрабатываться в коде во время выполнения.

Каин Шин
источник
Привет, мне любопытно, почему вы решили использовать TabHost вместо ActionBar с вкладками? Я сам перешел с TabHost на ActionBar, и мой код стал чище и компактнее ...
Игорь Ганапольский 08
Насколько я помню, одним из недостатков использования вкладок в ActionBar является то, что он решает автоматически отображать их как раскрывающееся меню счетчика (в случае маленького экрана), и это было не очень хорошо для меня. Но я не уверен на 100%.
WindRider
@ Игорь, я где-то читал здесь, SOчто использование ActionBarвкладок с символом a Navigation Drawerне очень хорошо, потому что оно автоматически помещает вкладки поверх вида вашего ящика. Извините, у меня нет ссылки для подтверждения.
Azurespot
1
@NoniA. blog.xamarin.com/android-tips-hello-toolbar-goodbye-action-bar Вы вообще следите за развитием Android?
Игорь Ганапольский
1
Ух, спасибо за ссылку @ Игорь! Я обязательно это проверю. Я все еще новичок, так что мне нужно изучить еще миллион вещей с Android, но этот кажется жемчужиной! Еще раз спасибо.
Azurespot
1

Основываясь на ответе @ 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();
    }
}
HelloImKevo
источник
1

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

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

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

Suragch
источник
Я был ОП. Причина отказа от использования библиотеки поддержки заключалась в том, что это было внутреннее приложение компании, в котором используемое оборудование было четко определено как> = 4.0 и <= 4.1. Не было необходимости выходить на широкую аудиторию, это был внутренний персонал и не было намерения использовать приложение за пределами компании. Единственная причина поддержки библиотеки - быть обратно совместимой - но можно было бы ожидать, что все, что вы можете делать с библиотекой поддержки, вы сможете достичь «изначально» и без нее. Почему «родная» более высокая версия имеет меньше функций, чем библиотека поддержки, единственная цель которой - быть совместимой с предыдущими версиями.
Матиас Конрад
1
Тем не менее, конечно, вы могли бы использовать библиотеку поддержки, и я тоже мог бы. Просто не понимал, почему Google предлагает функции ТОЛЬКО в библиотеке поддержки, но не за ее пределами, или почему они даже называют ее библиотекой поддержки, а не делают ее общим стандартом, если это все равно лучше всего. Вот хорошая статья о библиотеке поддержки: martiancraft.com/blog/2015/06/android-support-library
Матиас Конрад,
@ Матиас, хорошая статья.
Suragch