目前是否有一种方法可以在 MVVMCross 中进行选项卡式导航,例如 YouTube 应用程序,每个选项卡都有一个子导航,还是必须由我自己实现?那么如何才是一种干净的方式呢?我应该调整演示者吗?
一个例子: 我有三个选项卡,我可以从一个选项卡导航到另一个选项卡。如果我在第一个选项卡中选择一个元素,它将导航到新的 ViewModel 并替换第一个选项卡,同时我仍然可以切换到其他选项卡。如果我导航回第一个选项卡,我将返回到旧的第一个选项卡。
最佳答案
解决方案架构:
Activity
TabLayout
ViewPager
RootFragment (1 per tab, created/deleted by the viewpager depdending of the selected tab)
|-- ChildFragmentManager of the RootFragment (= independant navigation stack)
| Tab1Fragment1
| Tab1Fragment2
| Tab1Fragment3
- 主视图是一个 mvvmcross Activity “HomeContainerActivity”
- 对于选项卡按钮,我使用了 TabLayout。
- 内容是链接到选项卡布局的 ViewPager。当选择选项卡时,viewpager 会切换显示的 fragment 。
- ViewPager 的适配器配有自定义 MvxCachingFragmentPagerAdapter,该适配器实例化 RootTabFragment,而不是指定 MvxViewPagerFragmentInfo 的 fragment ,并使用将成为堆栈根的 fragment 对其进行初始化
- RootTabFragment 的布局只有一个内容,其中放置了上述 fragment 。
仅此而已。导航到 View 模型会在当前顶部 fragment 中打开它。这意味着标准导航将按预期工作,但后退按钮在您的主要 Activity 中需要一些代码。此外,如果没有自定义 View 呈现器,您就无法通过代码更改选项卡。
主要 Activity 布局:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/chrome"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<!-- fragment content for popups -->
<FrameLayout
android:id="@+id/contentPopup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="5dp"/>
<!-- action/tool bar -->
<!-- must be below all contents -->
<include layout="@layout/activity_home_toolbar" />
<!-- tab bar -->
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:layout_gravity="bottom"
>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:tabIndicator="@null"
/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
使用 TabLayout 设置 ViewPager:
var fragments = new List<MvxViewPagerFragmentInfo>
{
new MvxViewPagerFragmentInfo("Tab1", typeof(Tab1Fragment).FragmentJavaName(), typeof(Tab1Fragment), typeof(Tab1ViewModel)),
new MvxViewPagerFragmentInfo("Tab2", typeof(Tab2Fragment).FragmentJavaName(), typeof(Tab2Fragment), typeof(Tab2ViewModel)),
};
viewPager = FindViewById<ViewPager>(Resource.Id.viewpager);
viewPager.Adapter = new MvxCachingFragmentStatePagerAdapter2(this, SupportFragmentManager, fragments);
tabLayout = FindViewById<TabLayout>(Resource.Id.tabs);
tabLayout.SetupWithViewPager(viewPager);
自定义 fragment 寻呼机适配器:
[Register("vapolia.MvxCachingFragmentStatePagerAdapter2")]
public class MvxCachingFragmentStatePagerAdapter2 : MvxCachingFragmentPagerAdapter
{
private readonly Context context;
[Preserve(Conditional=true)]
protected MvxCachingFragmentStatePagerAdapter2(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) {}
public MvxCachingFragmentStatePagerAdapter2(Context context, FragmentManager fragmentManager, List<MvxViewPagerFragmentInfo> fragmentsInfo) : base(fragmentManager)
{
this.context = context;
FragmentsInfo = fragmentsInfo;
}
public override int Count => FragmentsInfo?.Count() ?? 0;
public List<MvxViewPagerFragmentInfo> FragmentsInfo { get; }
public override Fragment GetItem(int position, Fragment.SavedState fragmentSavedState = null)
{
var fragment = (RootTabFragment)Fragment.Instantiate(context, typeof(RootTabFragment).FragmentJavaName());
fragment.FragmentInfo = FragmentsInfo[position];
return fragment;
}
public override int GetItemPosition(Java.Lang.Object @object) => PagerAdapter.PositionNone;
public override ICharSequence GetPageTitleFormatted(int position) => new Java.Lang.String(FragmentsInfo.ElementAt(position).Title);
protected override string GetTag(int position) => FragmentsInfo[position].Tag;
public override void DestroyItem(ViewGroup container, int position, Object objectValue) {} //Disable destroy
}
根选项卡 fragment :
/// <summary>
/// Special fragment used as the root content for every tab items.
/// This enables a specific navigation stack for each tab item (using ChildFragmentManager).
///
/// Contains only one subfragment
/// </summary>
public class RootTabFragment : Fragment
{
public MvxViewPagerFragmentInfo FragmentInfo { get; set; }
[Android.Runtime.Preserve(Conditional=true)]
protected RootTabFragment(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) {}
[Android.Runtime.Preserve(Conditional=true)]
public RootTabFragment()
{
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
return inflater.Inflate(Resource.Layout.home_common_root, container, false);
}
public override void OnViewCreated(View view, Bundle savedInstanceState)
{
base.OnViewCreated(view, savedInstanceState);
var fm = ChildFragmentManager;
var isCacheable = FragmentInfo.FragmentType.IsFragmentCacheable(Mvx.IoCProvider.Resolve<IMvxAndroidCurrentTopActivity>().Activity.GetType());
var firstFragment = fm.FindFragmentByTag(FragmentInfo.Tag) ?? Fragment.Instantiate(Context, FragmentInfo.FragmentType.FragmentJavaName());
// ReSharper disable once SuspiciousTypeConversion.Global
if (firstFragment is IMvxFragmentView mvxFragment)
{
if (savedInstanceState == null || !isCacheable)
{
var viewModel = FragmentInfo.ViewModel ?? CreateViewModel();
mvxFragment.ViewModel = viewModel;
}
}
var ft = fm.BeginTransaction();
ft.SetReorderingAllowed(true);
ft.Replace(Resource.Id.content, firstFragment, FragmentInfo.Tag);
ft.CommitAllowingStateLoss();
}
private IMvxViewModel CreateViewModel()
{
MvxBundle mvxBundle = null;
if (FragmentInfo.ParameterValuesObject != null)
mvxBundle = new MvxBundle(FragmentInfo.ParameterValuesObject.ToSimplePropertyDictionary());
var request = new MvxViewModelRequest(FragmentInfo.ViewModelType, mvxBundle, null);
return Mvx.IoCProvider.Resolve<IMvxViewModelLoader>().LoadViewModel(request, null);
}
}
根选项卡布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
>
<!-- fragment content for navigation -->
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</FrameLayout>
</FrameLayout>
将进入根选项卡 fragment 的示例 fragment :
[MvxFragmentPresentation(FragmentContentId = Resource.Id.content, AddToBackStack = false, ActivityHostViewModelType = typeof(HomeContainerActivity), IsCacheableFragment = true)]
public class SettingsFragment : BaseFragment<SettingsViewModel>, IFragmentWithTitle
{
protected override int FragmentLayoutId => Resource.Layout.home_settings;
public string TitleText => "Settings";
}
修复了 Activity 中的 onbackpressed:
public override void OnBackPressed()
{
var n = SupportFragmentManager.BackStackEntryCount;
if (n >= 1)
{
base.OnBackPressed();
}
else
{
//Try to use the current selected tab's backstack
var currentFragment = (Fragment)viewPager.Adapter.InstantiateItem(null, viewPager.CurrentItem); //Returns the existing fragment
var fm = currentFragment.ChildFragmentManager;
n = fm.BackStackEntryCount;
if (n >= 1)
{
//Must use immediate, so SetCorrectTitle can use the updated backstack to set the correct title
fm.PopBackStackImmediate();
if (n == 1)
SupportActionBar.SetDisplayHomeAsUpEnabled(false); //Set to true to display back button
//SetCorrectTitle();
}
else
{
base.OnBackPressed();
}
}
}
希望有帮助。
关于android - MVVM带 Backstack 的跨选项卡导航,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49974255/