navigation结合DrawerLayout和toolbar时不使toolbar显示返回箭头

本文探讨了在Android中使用navigation、DrawerLayout和toolbar时遇到的问题,即打开抽屉菜单跳转fragment后,toolbar显示返回箭头而非抽屉图标。通过分析`setupWithNavController()`、`ToolbarOnDestinationChangedListener`和`navigateUp()`的源码,提出了两种解决方案:自定义toolbar的点击事件和OnDestinationChangedListener,或者将所有fragment设为顶级目的地。最终选择后者,并展示了如何通过AppBarConfiguration.Builder添加顶级目的地,以保持抽屉在任何页面下都能正常工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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()一个控制抽屉的展开,一个控制导航向上一级导航。
通过判断条件可以看到其是通过导航到的目的地是否最上层目的地。
那么整个官方实现的代码已经看完,解决方案暂时想到两种:

  1. 直接自己实现toolbar的Navigation点击事件及OnDestinationChangedListener
  2. 第二种便是将其余的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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值