从零自学Andriod(二):BottomNavigationView的应用与问题

2022-07-29,,

上一篇介绍了Res资源管理目录的大概结构,相信大部分人还是会搞不明白这其中的关系。没事,接下来我们就要结合实际代码来理解了。在上一篇我创建了带有底部导航菜单的项目,可以看到运行出来的效果已经有个App的基本框架了,起到关键作用的就是底部的导航菜单了。实现导航菜单的方式有很多种,我这只说说App里自带的BottomNavigationView

一、BottomNavigationView

直接看代码,打开Java>com.example.myapp>MainActivity,这里要注意一下,使用BottomNavigationView需要import相关依赖,如果选择了创建带BottomNavigationView的项目,那么这个也会默认import。

import com.google.android.material.bottomnavigation.BottomNavigationView;

有一个被重写的onCreate方法。在方法中可以看到上一篇讲过的读取资源内容的方式:R.layout.activity_main和R.id.nav_view以及其他的R.id.xxx。

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);//设置页面布局
        BottomNavigationView navView = findViewById(R.id.nav_view); //找到导航菜单组件
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView, navController);
    }

super.onCreate(savedInstanceState);就不说了大家都懂,看第二句:

setContentView(R.layout.activity_main);

我的理解是将res/layout/activity_main.html这个布局文件设置为整个app的主布局。这个文件下有两个子节点,代表底部的导航菜单(BottomNavigationView)和中间可显示的内容(fragment)。这些看属性名就挺好理解的,给节点设置id就可以通过R.id.xx的方式获取,而其他的也能看懂就是控件位置大小之类的。

<?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"
    android:paddingTop="?attr/actionBarSize">

    <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: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/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

在BottomNavigationView中比较重要的属性是app:menu="@menu/bottom_nav_menu",这句话是将底部导航与菜单项关联起来。菜单项的xml文件中写了3个item,它们分别就是文章开头那张图上的3个菜单按钮home、dashboard和notifications。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>
android:icon = 菜单的icon,指向了一个图片型的资源文件
android:title = 菜单的标题,指向了一个string型的资源文件

这两个资源文件就不细说了,打开就能看明白。

到这一步导航菜单的UI其实就已经出来了,但要实现导航的功能,就要回到activity_main.xml的第二个节点fragment。我对这个fragment的理解是容器,类似于html的frameset,替换容器的内容从而实现导航的效果。而将内容页面与布局关联起来的属性是app:navGraph="@navigation/mobile_navigation" ,其中通过app:startDestination="@+id/navigation_home"来设置打开App时默认显示的fragment,每个fragment中的andriod:name都指向其Java实现,存放在/Java/ui/的目录下。

<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.example.myapp.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.example.myapp.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.example.myapp.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

以home举例,HomeFragment继承了fragment类,并在onCreateView中关联继承自ViewModel的HomeViewModel。然后给fragment_home布局中的文本标签TextView赋值。

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        homeViewModel =
                ViewModelProviders.of(this).get(HomeViewModel.class);
        View root = inflater.inflate(R.layout.fragment_home, container, false);
        final TextView textView = root.findViewById(R.id.text_home);
        homeViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                textView.setText(s);
            }
        });
        return root;
    }

 赋值的内容是HomeViewModel的构造函数中setValue的值。

    public HomeViewModel() {
        mText = new MutableLiveData<>();
        mText.setValue("This is home fragment");
    }

不知道为什么,我刚新建的一个项目会提示ViewModelProviders已弃用,之前建的项目就不会。查找了一下资料,我们需要将ViewModelProviders.of()改成new ViewModelProvider()的形式。

homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);

到这里BottomNavigationView组件的基本使用方法已经解读完了,第一次看可能会觉得有点绕,只要把这几个xml的关系梳理梳理就清楚了。

二、NavigationUI

回头看MainActivity时,会发现有几句代码好像没有说到,我的理解是NavigationUI将页面切换与App Bar关联起来了,也就是用来实现切换菜单时App Bar的标题会同时跟着变动的效果。后续查了一下资料,果不其然NavigationUI是用来管理页面与App Bar的。

        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView, navController);

三、应用与问题解决

App虽然已经为我们生成好了主要的功能,但想要变成自己的东西还是需要一番调整的,在改动的过程中我遇到了一些问题并将它们记录了下来:

Q:当航菜单有3个项以上时,默认不显示未选中项的文字。

A1:在引用导航控件的xml(activity_main.xml)文件里增加属性:app:labelVisibilityMode="labeled"

    <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:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu"
        app:labelVisibilityMode="labeled"/>

A2:在activity的构造函数中(onCreate)设置:

navView.setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED);

这需要

import com.google.android.material.bottomnavigation.LabelVisibilityMode;

Q:NavigationUI与override的OnNavigationItemSelectedListener监听冲突。

A:原本想实现点击菜单更换图片的效果,就重写了菜单的OnNavigationItemSelectedListener。却发现事件监听一直没有触发,控制台信息没有输出,断点也进不去。尝试了多种方式未果,怀疑问题出在NavigationUI上,遂将NavigationUI那几句代码注释,果然成功了。于是跳到 NavigationUI.setupWithNavController(navView, navController)这个方法去看,果然里面是重写了监听的。如果真的有重写的需要,就在setupWithNavController里面修改吧。

    public static void setupWithNavController(
            @NonNull final BottomNavigationView bottomNavigationView,
            @NonNull final NavController navController) {
        bottomNavigationView.setOnNavigationItemSelectedListener(
                new BottomNavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                        return onNavDestinationSelected(item, navController);
                    }
                });
        final WeakReference<BottomNavigationView> weakReference =
                new WeakReference<>(bottomNavigationView);
        navController.addOnDestinationChangedListener(
                new NavController.OnDestinationChangedListener() {
                    @Override
                    public void onDestinationChanged(@NonNull NavController controller,
                            @NonNull NavDestination destination, @Nullable Bundle arguments) {
                        BottomNavigationView view = weakReference.get();
                        if (view == null) {
                            navController.removeOnDestinationChangedListener(this);
                            return;
                        }
                        Menu menu = view.getMenu();
                        for (int h = 0, size = menu.size(); h < size; h++) {
                            MenuItem item = menu.getItem(h);
                            if (matchDestination(destination, item.getItemId())) {
                                item.setChecked(true);
                            }
                        }
                    }
                });
    }

Q3:如何实现点击菜单更换图片的效果。

A:上一条说了监听已经被NavigationUI重写,有没有办法在不修改setupWithNavController方法的情况下实现这个效果呢?答案是肯定的,这涉及到另一个概念选择器(selector)。不得不说,自从发现有selector后,我可太喜欢了,selector为一些动效的实现省了很多事。

具体怎么使用的呢,首先在res/drawable/下新建一个selector,我给它起名home_selector

创建好了之后是这样,selector是由若干个item子节点组成的。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

</selector>

接下来我给它加上两行代码

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_home_black_24dp" android:state_checked="true" />
    <item android:drawable="@drawable/ic_dashboard_black_24dp" android:state_checkable="false" />
</selector>

 android:state_checked="true" 与 android:state_checked="false" 很好理解:选中和未选中状态。

所以就是,选中状态显示图片@drawable/ic_home_black_24dp,未选中显示图片@drawable/ic_dashboard_black_24dp

然后将之前提到的res/menu/bottom_nav_menu.xml中的android:icon改成创建的这个选择器就OK。

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/home_selecor"
        android:title="@string/title_home" />

跑起来看看效果,home菜单在没有选中的时候换了个图片~

selector还有其他很多判断用法,网上都有统计,有兴趣的可以找找看。 

Q4:为什么我换了新的Icon,显示的还是默认颜色?

A:需要在MainActivity的onCreate方法中加上navView.setItemIconTintList(null); 

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BottomNavigationView navView = findViewById(R.id.nav_view);
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        navView.setItemIconTintList(null);  //去掉不显示图片默认颜色
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_parlour, R.id.navigation_study, R.id.navigation_kitchen,R.id.navigation_balcony,R.id.navigation_bedroom )
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView, navController);
    }

Q5:如果修改菜单文字颜色 

A:这也是我整了半天都没改成功的效果,查了半天资料才发现,不能直接修改菜单的文本颜色,要在App主题那边修改。找到res/values/style.xml,可以看到在style下里面有3个节点分别指向了3个颜色值(颜色值存放在res/values/color.xml中)。

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

这三个颜色值分别表示 

        <!-- 应用的主要色调,actionBar默认使用该颜色,Toolbar导航栏的底色 -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <!-- 应用的主要暗色调,statusBarColor 默认使用该颜色 -->
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <!-- 一般控件的选中效果默认采用该颜色,如 CheckBox,RadioButton,SwitchCompat,ProcessBar等-->
        <item name="colorAccent">@color/colorAccent</item>

所以想要修改菜单标题颜色就将相应的颜色值替换掉,但要注意同时也会将其他用到这个颜色值的地方一并修改。style还有很多其他的属性值可以修改,有需要的可以查找资料。最后看一下我修改后的效果,这篇就到这里啦。

本文地址:https://blog.csdn.net/freebazzi/article/details/108710319

《从零自学Andriod(二):BottomNavigationView的应用与问题.doc》

下载本文的Word格式文档,以方便收藏与打印。