fragment的切換問題一直都比較麻煩绽诚,好在官方最新的jetpack提供了簡(jiǎn)單的解決方案监署,但也是不能拿來直接用,在此記錄下一些解決方法寄啼。
1.創(chuàng)建Android studio默認(rèn)ButtomNavigationView
MainActivity的布局很簡(jiǎn)單逮光,一個(gè)fragment一個(gè)BottomNavigationView,注意到ConstraintLayout有個(gè)paddingTop="?attr/actionBarSize"這就比較奇怪墩划,貌似如果父布局有toolbar時(shí)才有用涕刚。fragment的類是NavHostFragment,這就是jetpack的組件了走诞。注意app:menu指定了NavigationView的menu副女,fragment的app:navGraph指定了一個(gè)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蚣旱,有三個(gè)fragment標(biāo)簽碑幅,其父標(biāo)簽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>
每個(gè)item有和navigation中fragment同樣的id沟涨,通過下面MainActivity中onCreate用NavigationUI的方法setupWithNavController的綁定實(shí)現(xiàn)跳轉(zhuǎn)。
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(navView, navController);
2.用ViewModel保存View數(shù)據(jù)
整個(gè)fragment代碼非常簡(jiǎn)潔异吻,簡(jiǎn)直是新手的福音裹赴,但我后來發(fā)現(xiàn)一個(gè)崩潰的事,每次切換fragment诀浪,之前的fragment已經(jīng)完全Destroy了棋返,后退時(shí)完全重走了一遍Fragment的onAttach()->onCreate()->onCreateView()...的過程,運(yùn)存倒是省了雷猪,可是需要聯(lián)網(wǎng)顯示的或滾動(dòng)界面的fragment無法恢復(fù)狀態(tài)是不可接受的睛竣。我當(dāng)然想到了項(xiàng)目默認(rèn)創(chuàng)建的ViewModel,可是依賴fragment的ViewModel在fragment destroy之后生命周期也結(jié)束了求摇。最后用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;
}
}
使用方法,其實(shí)拿到的是同一個(gè)mainViewModel實(shí)例:
//在MainActivity中使用
MainViewModel mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
//在HomeFragment中使用,在onViewCreated()中或之后調(diào)用
MainViewModel mainViewModel = new ViewModelProvider(getActivity()).get(MainViewModel.class);
以HomeFragment為例与境,我們不使用ViewModel的觀察者方法验夯,直接在onDestroyView調(diào)用時(shí)保存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;
}
然而這有一個(gè)問題摔刁,root在這種情況下可能會(huì)指定多個(gè)parent導(dǎo)致報(bào)錯(cuò)挥转,加入去除parent的代碼:
@Override
public void onDestroyView() {
super.onDestroyView();
//其實(shí)以下代碼不一定寫在onDestroyView(),可以獲取指定后root的地方都可以
ViewGroup parent = (ViewGroup) root.getParent();
if(parent!=null){
parent.removeView(root);
}
mainViewModel.setView(root);
}
3.取消煩人的fragment重建
到這里為止還有一個(gè)非常不爽的地方,就是我們單擊已經(jīng)選中的BottomNavigationView的底部Tag绑谣,居然會(huì)再次新建當(dāng)前fragment准潭,同時(shí)把舊fragment的視圖設(shè)為不可見后銷毀,這會(huì)導(dǎo)致我們重用視圖的fragment閃爍域仇、空白,于是我們要取消當(dāng)前Tag的單擊動(dòng)作寺擂,觀察NavigationUI的setupWithNavController方法源碼:發(fā)現(xiàn)只給navView設(shè)置了一個(gè)監(jiān)聽暇务,于是我們可以重新設(shè)置監(jiān)聽:
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()){
//...其他動(dòng)作
return true;//單擊事件被消耗
}
return onNavDestinationSelected(item, navController);
}
});
這樣的結(jié)果是單擊當(dāng)前Tag不再有動(dòng)作。
希望大家能提供更好的辦法怔软。