导航体系结构组件-如何使用导航控制器设置/更改自定义返回或汉堡包图标?

时间:2018-10-30 09:34:05

标签: android android-layout navigation android-architecture-components android-jetpack

我正在尝试实现Navigation Architecture Component附带的新引入的Jetpack。到目前为止,它对于管理应用程序的导航流程非常有用,而且非常有用。

我已经使用MainActivity中的工具栏设置了包括抽屉布局在内的基本导航,如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navController = Navigation.findNavController(this, R.id.mainNavFragment)

        // Set up ActionBar
        setSupportActionBar(toolbar)
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)

        // Set up navigation menu
        navigationView.setupWithNavController(navController)
    }

    override fun onSupportNavigateUp(): Boolean {
        return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.mainNavFragment), drawerLayout)
    }
}

采用这种布局:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            app:navigationIcon="@drawable/ic_home_black_24dp"/>

    </android.support.design.widget.AppBarLayout>

    <fragment
        android:id="@+id/mainNavFragment"
        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>

它可以工作在鳍上。但是,真正的问题是,何时为应用提供定制设计,

  

如何设置汉堡包的自定义图标或后退图标?

到现在为止,由NavigatoinController本身处理。

enter image description here

我已经尝试过下面的选项,但是不起作用:

app:navigationIcon="@drawable/ic_home_black_24dp" //1
supportActionBar.setHomeAsUpIndicator(R.drawable.ic_android_black_24dp) //2

谢谢!

8 个答案:

答案 0 :(得分:6)

导航组件使用DrawerArrowDrawable作为基础实现,该实现实际上在汉堡包图标和后退箭头图标之间进行动画处理。可以通过样式在一定程度上修改两个图标的外观,请参见

How to style the DrawerArrowToggle from Android appcompat v7 21 library

对于我们的应用,我们使用以下属性来获取细长的箭头:

public class MyProcess
{
    private Process _process;
    public event EventHandler OnFinish;
    public MyProcess(string pathToExe)
    {
         _process = new Process();
         _process.StartInfo.FileName = pathToExe;
    }
    public int Start()
    {
         _process.Exited += _processExited;
         _process.Start();
         return _process.Id;
    }
    public void Stop()
    {
         _process.Stop();
    }
    private void _processExited(object sender, EventArgs e)
    {
         OnFinish?.Invoke();
    }
}

public class MyProgram
{
    private static int stoppedProcs = 0;

    public static void Main(string[] args)
    {
         var proc = new MyProcess("foo.exe");
         proc.OnFinish += proc_OnFinish;
         proc.Start();
         proc.Stop();
         //Wait here to display 1 instead of 0
         Console.WriteLine(stoppedProcs);
    }

    private static void proc_OnFinish(object sender, EventArgs e)
    {
         stoppedProcs++;
    }
}

必须在应用的主题中设置此样式,如下所示:

    <style name="ToolbarArrow" parent="Widget.AppCompat.DrawerArrowToggle">
        <item name="color">@color/primary</item>
        <item name="drawableSize">32dp</item>
        <item name="arrowShaftLength">22dp</item>
        <item name="thickness">1.7dp</item>
        <item name="arrowHeadLength">8dp</item>
    </style>

答案 1 :(得分:2)

导航版本1.0.0-alpha08遇到相同的问题。我已经通过重新实现setupActionBarWithNavController来提供我需要的自定义行为来解决它。

setDisplayShowTitleEnabled(false) //Disable the default title

container.findNavController().addOnDestinationChangedListener { _, destination: NavDestination, _ ->
    //Set the toolbar text (I've placed a TextView within the AppBar, which is being referenced here)
    toolbar_text.text = destination.label

    //Set home icon
    supportActionBar?.apply {
        setDisplayHomeAsUpEnabled((destination.id in NO_HOME_ICON_FRAGMENTS).not())
        setHomeAsUpIndicator(if (destination.id in TOP_LEVEL_FRAGMENTS) R.drawable.app_close_ic else 0)
    }
}

您需要做的就是将一个onDestinationChangedListener添加到导航控制器,并为每个目的地设置标题和图标。

此代码应放置在包含导航图的活动的onCreate()方法内,并且NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)应该从代码中删除。

请注意,这将删除导航抽屉的默认功能。

我希望这会有所帮助!

答案 2 :(得分:2)

由于样式AppTheme将Widget.AppCompat.DrawerArrowToggle样式用于navigationUI汉堡包和后退按钮图标,因此您需要通过继承Widget.AppCompat.DrawerArrowToggle来创建自定义样式。

我将尽力所有您可能拥有的选项以及每个选项的作用:

<style name="CustomNavigationIcons" parent="Widget.AppCompat.DrawerArrowToggle">
        <!-- The drawing color for the bars -->
        <item name="color">@color/colorGrey</item>
        <!-- The total size of the drawable -->
        <item name="drawableSize">64dp</item>

        <!-- The thickness (stroke size) for the bar paint -->
        <item name="thickness">3dp</item>
        <!-- Whether bars should rotate or not during transition -->
        <item name="spinBars">true</item>
        <!-- The max gap between the bars when they are parallel to each other -->
        <item name="gapBetweenBars">4dp</item>
        <!-- The length of the bars when they are parallel to each other -->
        <item name="barLength">27dp</item>

        <!-- The length of the shaft when formed to make an arrow -->
        <item name="arrowShaftLength">23dp</item>
        <!-- The length of the arrow head when formed to make an arrow -->
        <item name="arrowHeadLength">7dp</item>
</style>

最后,您必须确保AppTheme现在使用自定义样式作为DrawerArrowToggle,如下所示:

<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <!-- Other styles -->
        <item name="drawerArrowStyle">@style/CustomNavigationIcons</item>

</style>

答案 3 :(得分:2)

实际上,我不知道这是 Google 的功能还是错误。

在给出我更改自定义后退图标的解决方案之前。让我们看看 Google 是如何实施的。

一切从我们调用开始:

val navController = findNavController(R.id.nav_host_fragment)
        appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.homeFragment,
                R.id.categoryFragment,
                R.id.cartFragment,
                R.id.myAccountFragment,
                R.id.moreFragment
            ),
        )
 toolbar.setupWithNavController(navController, appBarConfiguration)

setupWithNavController() 是 Toolbar 的扩展。

fun Toolbar.setupWithNavController(
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
) {
    NavigationUI.setupWithNavController(this, navController, configuration)
}

在NavigationUI中,你可以看到Google只是监听目的地的变化,并在用户点击后退按钮时设置一个点击事件。

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);
            }
        });
    }

再详细一点,你可以看到这个函数:

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);
        }
    }

这里是 setNavigationIcon:

@Override
    protected void setNavigationIcon(Drawable icon,
            @StringRes int contentDescription) {
        Toolbar toolbar = mToolbarWeakReference.get();
        if (toolbar != null) {
            boolean useTransition = icon == null && toolbar.getNavigationIcon() != null;
            toolbar.setNavigationIcon(icon);
            toolbar.setNavigationContentDescription(contentDescription);
            if (useTransition) {
                TransitionManager.beginDelayedTransition(toolbar);
            }
        }
    }

我刚刚看到每次目标更改时,目标不是根页面。谷歌创建了一个 DrawerArrowDrawable 对象,然后谷歌将这个图标设置为导航图标。我认为这就是工具栏不显示我们自定义图标的原因。

在这一点上,我认为我们有一些解决方案来处理它。 一种简单的解决方案是在 Activity 的 onCreate() 方法中添加 addOnDestinationChangedListener() 方法,每当用户更改到新页面(目的地)时,我们将检查并决定是否显示自定义后退图标。像这样:

val navController = findNavController(R.id.nav_host_fragment)
        navController.addOnDestinationChangedListener { _, destination, _ ->
          (appBarConfiguration.topLevelDestinations.contains(destination.id)) {
                    toolbar.navigationIcon = null

                } else {
                    toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white)

                }
        }

答案 4 :(得分:1)

使用导航版本1.0.0-alpha07时遇到相同的问题。我发现以下解决方法:

在我的活动中,我将工具栏视图设置为支持操作栏并使用导航控制器:

val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
toolbar.setupWithNavController(navController, appBarConfiguration)

然后在我要替换箭头图标的片段的onViewCreated中,设置图标并将其设置为启用状态(可能在onAttached中进行此操作):

(requireActivity() as AppCompatActivity).supportActionBar?.apply {
  setHomeAsUpIndicator(R.drawable.ic_close_24dp)
  setDisplayHomeAsUpEnabled(true)
}

当我这样做时,导航组件似乎仍然可以正确处理后退堆栈,动画,并删除起始片段上的图标。希望在离开Alpha之前,导航组件可以支持自定义图标,而不必诉诸旧式操作栏API。

答案 5 :(得分:1)

我遇到了同样的问题。我只是重写了SDK中的某些类:

class ToolbarOnDestinationChangedListener(toolbar: Toolbar) : OnDestinationChangedListener {
    private val toolbarWeakReference = WeakReference(toolbar)

    override fun onDestinationChanged(
        controller: NavController,
        destination: NavDestination,
        arguments: Bundle?
    ) {
        if (toolbarWeakReference.get() == null) {
            controller.removeOnDestinationChangedListener(this)
            return
        }

        if (destination is FloatingWindow) {
            return
        }

        destination.label?.let {
            val title = pickTitleFromArgs(it, arguments)
            setTitle(title)
        }
    }

    private fun pickTitleFromArgs(label: CharSequence, arguments: Bundle?): StringBuffer {
        val title = StringBuffer()
        val matcher = Pattern.compile("\\{(.+?)\\}").matcher(label)
        while (matcher.find()) {
            val argName = matcher.group(1)
            if (arguments != null && arguments.containsKey(argName)) {
                matcher.appendReplacement(title, "")
                title.append(arguments[argName].toString())
            } else {
                error("Could not find $argName in $arguments to fill label $label")
            }
        }
        matcher.appendTail(title)
        return title
    }

    private fun setTitle(title: CharSequence) {
        toolbarWeakReference.get()?.let {
            it.title = title
        }
    }
}

以及以下扩展名:

fun Toolbar.setNavController(navController: NavController) {
    navController.addOnDestinationChangedListener(ToolbarOnDestinationChangedListener(this))
    toolbar.setNavigationOnClickListener { navController.navigateUp() }
}

使用:

//Fragment
toolbar.setNavController(findNavController())

此代码不控制图标。但它会考虑标签,参数,目标更改。您应该使用XML设置图标。

答案 6 :(得分:0)

我最近也遇到了同样的问题。我想将默认设置与NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)一起使用,也不想在每个片段中都编写逻辑。这就是在项目中具有导航组件的用例。在挖掘导航组件的源代码之后,我想出了以下解决方案:

findNavController(R.id.nav_host_fragment)
        .addOnDestinationChangedListener { _, destination, _ ->
            when (destination.id) {

                R.id.nav_home,
                R.id.nav_gallery -> {
                    drawerToggleDelegate!!
                        .setActionBarUpIndicator(
                            ContextCompat.getDrawable(
                                this,
                                R.drawable.custom_hamburger
                            ),
                            androidx.navigation.ui.R.string.nav_app_bar_open_drawer_description
                        )
                }

                else -> {
                    drawerToggleDelegate!!
                        .setActionBarUpIndicator(
                            ContextCompat.getDrawable(
                                this,
                                R.drawable.custom_back
                            ),
                            androidx.navigation.ui.R.string.nav_app_bar_navigate_up_description
                        )
                }
            }

        }

我已将DestinationChangedListener添加到navController并使用了drawerToggleDelegate的{​​{1}}属性来更改图标。这里AppCompatActivityR.id.nav_home是我的顶级目的地;为此,将显示自定义汉堡包,对于其他汉堡,将显示自定义后退图标。
这种方法的唯一缺点是您将丢失默认动画。

但是您可以在此处轻松实现逻辑以获得动画效果。

答案 7 :(得分:0)

我不知道什么时候可以这样做,但我刚刚发现,您可以在调用 toolbar.setNavigationIcon(R.drawable.your_icon) 后调用 toolbar.setupWithNavController(findNavController())。这将删除默认的 DrawerArrowDrawable 并立即用您的自定义 drawable 覆盖它。无需重写任何 sdk 代码或做一些疯狂的其他事情

带有默认 findViewById 的代码

fun Fragment.initToolbarWithCustomDrawable(@DrawableRes resId: Int) {
    val toolbar = requireView().findViewById<MaterialToolbar>(R.id.your_toolbar)
    with(toolbar) {
        setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
        setNavigationIcon(resId)
    }  
}

没有 findViewById 的代码

fun Fragment.initToolbarWithCustomDrawable(
     toolbar: Toolbar
     @DrawableRes resId: Int
) {
   with(toolbar) {
        setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
        setNavigationIcon(resId)
   }
}