Лучшие практики для вложенных фрагментов в 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
- http://developer.android.com/about/versions/android-4.2.html
- http://developer.android.com/reference/android/app/Fragment.html
К сожалению, вряд ли найдутся хорошие примеры, демонстрирующие лучшие практики использования фрагментов без библиотеки поддержки, даже во всех руководствах разработчиков 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
что откладывает загрузку, так 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
Лучший способ сделать это в 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 рекомендует это, и оно сделает ваше приложение доступным для самой широкой аудитории.
В своем более полном ответе здесь я сделал пример, демонстрирующий, как использовать вложенные фрагменты с библиотекой поддержки.