navigation结合DrawerLayout和toolbar代码
activity_main.xml中
<layout 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">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawerlayout_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_main"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:theme="@style/ThemeOverlay.AppCompat.Dark" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_Main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
</LinearLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/naview_header"
android:fitsSystemWindows="true"
app:menu="@menu/main_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
<data>
</data>
</layout>
nav_main.xml中
<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/nav_main"
app:startDestination="@id/AFragment"
>
<fragment
android:id="@+id/AFragment"
android:name="com.example.dp.myshow.fragment.AFragment"
android:label="a_fragment"
tools:layout="@layout/a_fragment" />
<fragment
android:id="@+id/BFragment"
android:name="com.example.dp.myshow.fragment.BFragment"
android:label="b_fragment"
tools:layout="@layout/b_fragment" />
</navigation>
main_menu.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/AFragment"
android:title="@string/a" />
<item
android:id="@+id/BFragment"
android:title="@string/b" />
</menu>
activity中
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_Main) as NavHostFragment
val navController = navHostFragment.findNavController()
val configuration = AppBarConfiguration(navController.graph, binding.drawerlayoutMain)
binding.navView.setupWithNavController(navController)
binding.toolbarMain.setupWithNavController(navController,configuration)
问题
使用该方式会导致使用抽屉菜单跳转到别的fragment时,toolbar中的导航按钮变成了返回图标。而很多时候我们使用抽屉时需要的是在任何页面下点击toolbar的导航按钮时显示抽屉而不是返回。那么要想解决该问题,可以看下navigation是如何将toolbar和DrawerLayout结合起来的。
setupWithNavController()
首先看这段代码 binding.toolbarMain.setupWithNavController(navController,configuration)
,代码实现如下:
fun Toolbar.setupWithNavController(
navController: NavController,
configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
) {
NavigationUI.setupWithNavController(this, navController, configuration)
}
很明显这里使用了kotlin的扩展函数,实际调用的还是NavigationUI中的函数:
public static void setupWithNavController(@NonNull Toolbar toolbar,
@NonNull final NavController navController,
@NonNull final AppBarConfiguration configuration) {
navController.addOnDestinationChangedListener(
new ToolbarOnDestinationChangedListener(toolbar, configuration));
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
navigateUp(navController, configuration);
}
});
}
这段代码很简单,1是设置navController的目的地改变时的listener,2是设置toolbar的导航按钮的点击
很明显关于图标的改变是在OnDestinationChangedListene
中,而导航按钮的点击行为是在navigateUp()
中
ToolbarOnDestinationChangedListener
ToolbarOnDestinationChangedListener的onDestinationChanged代码如下:
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
Toolbar toolbar = mToolbarWeakReference.get();
if (toolbar == null) {
controller.removeOnDestinationChangedListener(this);
return;
}
super.onDestinationChanged(controller, destination, arguments);
}
很明显这里没有做什么关键操作,主要还是在其父类中:
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
//省略前面部分代码
boolean isTopLevelDestination = NavigationUI.matchDestinations(destination,
mTopLevelDestinations);
if (openableLayout == null && isTopLevelDestination) {
setNavigationIcon(null, 0);
} else {
setActionBarUpIndicator(openableLayout != null && isTopLevelDestination);
}
}
由于设置DrawerLayout所以if (openableLayout == null && isTopLevelDestination)
肯定为false那么关键代码便是在setActionBarUpIndicator()
中了:
private void setActionBarUpIndicator(boolean showAsDrawerIndicator) {
boolean animate = true;
if (mArrowDrawable == null) {
mArrowDrawable = new DrawerArrowDrawable(mContext);
// We're setting the initial state, so skip the animation
animate = false;
}
setNavigationIcon(mArrowDrawable, showAsDrawerIndicator
? R.string.nav_app_bar_open_drawer_description
: R.string.nav_app_bar_navigate_up_description);
float endValue = showAsDrawerIndicator ? 0f : 1f;
if (animate) {
float startValue = mArrowDrawable.getProgress();
if (mAnimator != null) {
mAnimator.cancel();
}
mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress",
startValue, endValue);
mAnimator.start();
} else {
mArrowDrawable.setProgress(endValue);
}
}
}
从代码中不难看出当前目的地不是最上层时,对于动画的endvalue是1f,目的地是最上层时动画endvalue为0f,而DrawerArrowDrawable
就是控制toolbar中导航图标由三线转换成返回的。
toolbar图标转换的代码已经找到,现在去看控制toolbar按键行为的代码
navigateUp()
public static boolean navigateUp(@NonNull NavController navController,
@NonNull AppBarConfiguration configuration) {
Openable openableLayout = configuration.getOpenableLayout();
NavDestination currentDestination = navController.getCurrentDestination();
Set<Integer> topLevelDestinations = configuration.getTopLevelDestinations();
if (openableLayout != null && currentDestination != null
&& matchDestinations(currentDestination, topLevelDestinations)) {
openableLayout.open();
return true;
} else {
if (navController.navigateUp()) {
return true;
} else if (configuration.getFallbackOnNavigateUpListener() != null) {
return configuration.getFallbackOnNavigateUpListener().onNavigateUp();
} else {
return false;
}
}
}
关键代码就两处openableLayout.open();
和navController.navigateUp()
一个控制抽屉的展开,一个控制导航向上一级导航。
通过判断条件可以看到其是通过导航到的目的地是否最上层目的地。
那么整个官方实现的代码已经看完,解决方案暂时想到两种:
- 直接自己实现toolbar的Navigation点击事件及OnDestinationChangedListener
- 第二种便是将其余的fragment全部添加为最顶级目的地。
使用第一种方式需要自己写很多代码以及几乎无法利用官方封装好的代码,现采用第二种方案
解决方案
从官方封装的代码中看到其mTopLevelDestinations
为一个integer的set集合,那么官方原来设计之初便是考虑到可以有多个顶级目的地的情况那么我们只需要将menu中的fragment添加到TopLevelDestinations中即可.
添加代码:
configuration.topLevelDestinations.add()
添加的integer为NavDestination的id字段,对于NavDestination可以通过navController.graph.findNode(@IdRes int resid)来获得,其中resid为nav.xml中对应fragment使用的 android:id="@+id/AFragment"
修改后代码:
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_Main) as NavHostFragment
val navController = navHostFragment.findNavController()
val configuration = AppBarConfiguration(navController.graph, binding.drawerlayoutMain)
binding.navView.setupWithNavController(navController)
configuration.topLevelDestinations.add( navController.graph.findNode(R.id.AFragment)?.id)
configuration.topLevelDestinations.add( navController.graph.findNode(R.id.BFragment)?.id)
binding.toolbarMain.setupWithNavController(navController, configuration)
由于navigation组件的更新,官方将AppBarConfiguration中topLevelDestinations改为 Set而不是MutableSet无法通过之前的方式直接add了,可以通过AppBarConfiguration的Builder方法自行构建:
val configuration =
AppBarConfiguration.Builder(navView.menu)
.setOpenableLayout(drawerlayoutMain)
.setFallbackOnNavigateUpListener { false }
.build()
其原先的AppBarConfiguration(navController.graph, binding.drawerlayoutMain)
也是使用的这种方式:
public inline fun AppBarConfiguration(
navGraph: NavGraph,
drawerLayout: Openable? = null,
noinline fallbackOnNavigateUpListener: () -> Boolean = { false }
): AppBarConfiguration = AppBarConfiguration.Builder(navGraph)
.setOpenableLayout(drawerLayout)
.setFallbackOnNavigateUpListener(fallbackOnNavigateUpListener)
.build()