BottomNavigationView 與 Navigation使用遇見的一個小坑
先說結(jié)論吧抗楔,只是一個小問題,自己記錄一下。如果大家是BottomNavigationView與NavController結(jié)合使用實現(xiàn)fragment的切換,發(fā)現(xiàn)無法切換,記得檢查一下你創(chuàng)建的menu文件與navigation文件中的id是否是一一對應(yīng)苹熏。
最近在開發(fā)一個項目,因為項目的UI比較少相對也比較簡單币喧,考慮用一單Activity的實現(xiàn)方式轨域,使用Jetpack的Navigation進行頁面導(dǎo)航,主界面的tab實現(xiàn)使用谷歌的BottomNavigationView控件杀餐。因為之前寫過Demo干发,所以以為可以信手拈來。
創(chuàng)建主界面的布局史翘,Menu文件枉长,以及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">
<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:labelVisibilityMode="labeled"
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/main_tab_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Menu
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_cruise"
android:icon="@mipmap/main_tab_icon_cruise"
android:title="@string/main_tab_title_cruise" />
<item
android:id="@+id/navigation_video"
android:icon="@mipmap/main_tab_icon_video"
android:title="@string/main_tab_title_video" />
<item
android:id="@+id/navigation_warning"
android:icon="@mipmap/main_tab_icon_warning"
android:title="@string/main_tab_title_warning" />
<item
android:id="@+id/navigation_profile"
android:icon="@mipmap/main_tab_icon_profile"
android:title="@string/main_tab_title_profile" />
</menu>
- navigation
<?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/main_tab_navigation"
app:startDestination="@id/navigation_cruise">
<fragment
android:id="@+id/cruiseFragment"
android:name="com.gzwx.securityrobot.ui.main.cruise.CruiseFragment"
android:label="CruiseFragment"
tools:layout="@layout/fragment_cruise" />
<fragment
android:id="@+id/videoFragment"
android:name="com.gzwx.securityrobot.ui.main.video.VideoFragment"
android:label="VideoFragment"
tools:layout="@layout/fragment_video" />
<fragment
android:id="@+id/warnigFragment"
android:name="com.gzwx.securityrobot.ui.main.warning.WarningFragment"
android:label="WarningFragment"
tools:layout="@layout/fragment_warning" />
<fragment
android:id="@+id/profileFragment"
android:name="com.gzwx.securityrobot.ui.main.profile.ProfileFragment"
android:label="ProfileFragment"
tools:layout="@layout/fragment_profile" />
</navigation>
- MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initNavigation()
}
private fun initNavigation() {
val navController = findNavController(R.id.nav_host_fragment)
nav_view.setupWithNavController(navController)
adjustNavigationIconSize(nav_view)
}
}
大致的代碼結(jié)構(gòu)如上冀续,我以為這樣就可以實現(xiàn)切換了,之前的demo差不多也是這么個創(chuàng)建流程和使用方式必峰,可是洪唐,tab終未能如約的切換。最開始我以為是我自己哪里寫錯了吼蚁,上網(wǎng)搜了一圈各種教程桐罕,發(fā)現(xiàn)寫的都大差不差,也沒發(fā)現(xiàn)什么明顯的錯誤桂敛,后來debug了一下,發(fā)現(xiàn)在切換的時候拋出了異常:
java.lang.IllegalArgumentException: Navigation action/destination [package]:id/navigation_video cannot be found from the current destination Destination( [package]:id/cruiseFragment) label=CruiseFragment class= [package].ui.main.cruise.CruiseFragment
然后發(fā)現(xiàn)拋出異常的地方在NavigationUI的onNavDestinationSelected方法中
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
//具體報錯在這一行
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
繼續(xù)跟蹤在NavController里面
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
看到這里溅潜,大家應(yīng)該明白术唬,在NavController里面,NavDestination會根據(jù)destId查找下一個跳轉(zhuǎn)的dest滚澜,而這個destId就是傳進來的menuItem的Id粗仓。我們在主界面的布局文件中用來展示Fragment的宿主控件是"androidx.navigation.fragment.NavHostFragment"
<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/main_tab_navigation" />
這里有個屬性 app:navGraph,具體查看這個NavHostFragment的源碼设捐,我們會發(fā)現(xiàn)它會基于這個navGraph指向的xml文件進行一個導(dǎo)航圖表的創(chuàng)建借浊,因為在navigation的每一個fragment中我們都設(shè)置了一個id,可以想象的是創(chuàng)建的這個導(dǎo)航圖萝招,肯定與這個id相關(guān)蚂斤,那么navController調(diào)用navigate的時候如果傳入一個導(dǎo)航圖中不存在的id,肯定是無法找到合適的路由進行跳轉(zhuǎn)的槐沼,所以這里曙蒸,我們要保證Menu中的每一個Item的Id與navigation.xml中的每一個fragment的Id保持一致。最終修改的menu與navigation的文件如下
- Menu
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_cruise"
android:icon="@mipmap/main_tab_icon_cruise"
android:title="@string/main_tab_title_cruise" />
<item
android:id="@+id/navigation_video"
android:icon="@mipmap/main_tab_icon_video"
android:title="@string/main_tab_title_video" />
<item
android:id="@+id/navigation_warning"
android:icon="@mipmap/main_tab_icon_warning"
android:title="@string/main_tab_title_warning" />
<item
android:id="@+id/navigation_profile"
android:icon="@mipmap/main_tab_icon_profile"
android:title="@string/main_tab_title_profile" />
</menu>
- naviagtion
<?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/main_tab_navigation"
app:startDestination="@id/navigation_cruise">
<fragment
android:id="@+id/navigation_cruise"
android:name="com.gzwx.securityrobot.ui.main.cruise.CruiseFragment"
android:label="CruiseFragment"
tools:layout="@layout/fragment_cruise" />
<fragment
android:id="@+id/navigation_video"
android:name="com.gzwx.securityrobot.ui.main.video.VideoFragment"
android:label="VideoFragment"
tools:layout="@layout/fragment_video" />
<fragment
android:id="@+id/navigation_warning"
android:name="com.gzwx.securityrobot.ui.main.warning.WarningFragment"
android:label="WarningFragment"
tools:layout="@layout/fragment_warning" />
<fragment
android:id="@+id/navigation_profile"
android:name="com.gzwx.securityrobot.ui.main.profile.ProfileFragment"
android:label="ProfileFragment"
tools:layout="@layout/fragment_profile" />
</navigation>
當然岗钩,這里的id我個人覺得其實可以在values中創(chuàng)建一個ids的文件纽窟,創(chuàng)建對應(yīng)的Id,然后在這兩個布局文件中直接@id指向即可兼吓。