fragment的切换问题一直都比较麻烦,好在官方最新的jetpack提供了简单的解决方案,但也是不能拿来直接用,在此记录下一些解决方法。
1.创建Android studio默认ButtomNavigationView
MainActivity的布局很简单,一个fragment一个BottomNavigationView,注意到ConstraintLayout有个paddingTop="?attr/actionBarSize"这就比较奇怪,貌似如果父布局有toolbar时才有用。fragment的类是NavHostFragment,这就是jetpack的组件了。注意app:menu指定了NavigationView的menu,fragment的app:navGraph指定了一个navigation文件。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
我们打开res/navigation/mobile_navigation,有三个fragment标签,其父标签navigation通过app:startDestination指定了起始fragment。
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.latlaj.buttomnavigationtest.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.latlaj.buttomnavigationtest.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.latlaj.buttomnavigationtest.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
</navigation>
打开src/menu/bottom_nav_menu。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
</menu>
每个item有和navigation中fragment同样的id,通过下面MainActivity中onCreate用NavigationUI的方法setupWithNavController的绑定实现跳转。
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(navView, navController);
2.用ViewModel保存View数据
整个fragment代码非常简洁,简直是新手的福音,但我后来发现一个崩溃的事,每次切换fragment,之前的fragment已经完全Destroy了,后退时完全重走了一遍Fragment的onAttach()->onCreate()->onCreateView()...的过程,运存倒是省了,可是需要联网显示的或滚动界面的fragment无法恢复状态是不可接受的。我当然想到了项目默认创建的ViewModel,可是依赖fragment的ViewModel在fragment destroy之后生命周期也结束了。
最后用Activity的ViewModel解决问题。新建MainViewModel.java:
package com.latlaj.buttomnavigationtest;
import android.view.View;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
private MutableLiveData<View> mDashboardView;
private MutableLiveData<View> mHomeView;
private MutableLiveData<View> mNotificationsView;
public MainViewModel() {
mDashboardView = new MutableLiveData<>();
mHomeView = new MutableLiveData<>();
mNotificationsView = new MutableLiveData<>();
}
public void setDashboardView(View v) {
mDashboardView.setValue(v);
}
public void setHomeView(View v) {
mHomeView.setValue(v);
}
public void setNotificationsView(View v) {
mNotificationsView.setValue(v);
}
public LiveData<View> getDashboardView() {
return mDashboardView;
}
public LiveData<View> getHomeView() {
return mHomeView;
}
public LiveData<View> getNotificationsView() {
return mNotificationsView;
}
}
使用方法,其实拿到的是同一个mainViewModel实例:
//在MainActivity中使用
MainViewModel mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
//在HomeFragment中使用,在onViewCreated()中或之后调用
MainViewModel mainViewModel = new ViewModelProvider(getActivity()).get(MainViewModel.class);
以HomeFragment为例,我们不使用ViewModel的观察者方法,直接在onDestroyView调用时保存View。
@Override
public void onDestroyView() {
super.onDestroyView();
mainViewModel.setView(root);
}
private View root;
private MainViewModel mainViewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
mainViewModel=new ViewModelProvider(getActivity())
.get(MainViewModel.class);
root=mainViewModel.getHomeView().getValue();
if(root==null){
//正常初始化View
root = inflater.inflate(R.layout.fragment_home, container, false);
//...
}
//...
return root;
}
然而这有一个问题,root在这种情况下可能会指定多个parent导致报错,加入去除parent的代码:
@Override
public void onDestroyView() {
super.onDestroyView();
//其实以下代码不一定写在onDestroyView(),可以获取指定后root的地方都可以
ViewGroup parent = (ViewGroup) root.getParent();
if(parent!=null){
parent.removeView(root);
}
mainViewModel.setView(root);
}
3.取消烦人的fragment重建
到这里为止还有一个非常不爽的地方,就是我们单击已经选中的BottomNavigationView的底部Tag,居然会再次新建当前fragment,同时把旧fragment的视图设为不可见后销毁,这会导致我们重用视图的fragment闪烁、空白,于是我们要取消当前Tag的单击动作,观察NavigationUI的setupWithNavController方法源码:
发现只给navView设置了一个监听,于是我们可以重新设置监听:
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(navView, navController);
navView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
if (item.isChecked()){
//...其他动作
return true;//单击事件被消耗
}
return onNavDestinationSelected(item, navController);
}
});
这样的结果是单击当前Tag不再有动作。
希望大家能提供更好的办法。
网友评论