BottomNavigationView 与 Navigation使用遇见的一个小坑
先说结论吧,只是一个小问题,自己记录一下。如果大家是BottomNavigationView与NavController结合使用实现fragment的切换,发现无法切换,记得检查一下你创建的menu文件与navigation文件中的id是否是一一对应。
最近在开发一个项目,因为项目的UI比较少相对也比较简单,考虑用一单Activity的实现方式,使用Jetpack的Navigation进行页面导航,主界面的tab实现使用谷歌的BottomNavigationView控件。因为之前写过Demo,所以以为可以信手拈来。
创建主界面的布局,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)
}
}
大致的代码结构如上,我以为这样就可以实现切换了,之前的demo差不多也是这么个创建流程和使用方式,可是,tab终未能如约的切换。最开始我以为是我自己哪里写错了,上网搜了一圈各种教程,发现写的都大差不差,也没发现什么明显的错误,后来debug了一下,发现在切换的时候抛出了异常:
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
然后发现抛出异常的地方在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;
}
}
继续跟踪在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);
}
看到这里,大家应该明白,在NavController里面,NavDestination会根据destId查找下一个跳转的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的源码,我们会发现它会基于这个navGraph指向的xml文件进行一个导航图表的创建,因为在navigation的每一个fragment中我们都设置了一个id,可以想象的是创建的这个导航图,肯定与这个id相关,那么navController调用navigate的时候如果传入一个导航图中不存在的id,肯定是无法找到合适的路由进行跳转的,所以这里,我们要保证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中创建一个ids的文件,创建对应的Id,然后在这两个布局文件中直接@id指向即可。