Material Design 实战(二):侧滑菜单 DrawerLayout 与 NavigationView 详解

223 阅读5分钟

前言

书接上回...

滑动菜单也是 Material Design 中最常见的效果之一。这个功能看似挺复杂,但借助 Google 提供的工具,我们可以很轻松地实现滑动菜单的效果。

DrawerLayout 与 Toolbar 的联动

滑动菜单的核心是一个容器布局,它能将一部分内容(菜单)隐藏在屏幕边缘,通过滑动的方式将其展示出来。如果要我们自己去实现的话,难度有点大。但 Google 为我们提供了 DrawerLayout 控件,来专门实现这个效果。

DrawerLayout 是一个布局,它内部允许放置两个直接子控件,第一个子控件是主屏幕中显示的内容,第二个子控件则是隐藏的滑动菜单。

我们修改 activity_main.xml 布局文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </FrameLayout>

    <TextView
        android:layout_width="320dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#FFF"
        android:text="This is menu"
        android:textSize="30sp" />

</androidx.drawerlayout.widget.DrawerLayout>

这里最外层是 DrawerLayout,它的第二个子控件 TextView 就是我们的滑动菜单,其 layout_gravity 属性决定了菜单从哪一侧滑出。

left 表示从左侧滑出,right 表示从右侧滑出。start 则是根据系统语言来判断,如果是中文、英文等语言,就会从左侧滑出,如果是阿拉伯语等从右往左书写的语言,就会从右侧滑出。

现在运行程序,你会发现你根本无法拉出菜单,因为现在的手机大多使用全屏手势导航,当你从屏幕边缘向内滑动时,会触发返回操作,而不是拉出滑动菜单的操作。

为此,我们在 Toolbar 的最左侧添加一个导航按钮,通过点击这个按钮来打开菜单。MainActivity 中的代码如下:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setSupportActionBar(binding.toolbar)

        // 获取 ActionBar 实例
        supportActionBar?.let {
            // 显示导航按钮
            it.setDisplayHomeAsUpEnabled(true)
            // 设置导航按钮图标
            it.setHomeAsUpIndicator(R.drawable.ic_menu)
        }

    }

    ...
    // 处理所有菜单项的点击事件
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            // 点击该导航按钮后,展示滑动菜单
            android.R.id.home -> binding.drawerLayout.openDrawer(GravityCompat.START)
            ...
        }
        return true
    }
}

实际上,Toolbar 最左侧的按钮叫做 Home 按钮,默认图标为返回的箭头,用于返回上一个 Activity。这里我们把它的默认的图标和作用都改变了。

现在运行程序,界面效果如下:

image.png

点击左上角的导航按钮,就会出现滑动菜单:

image.png

使用 NavigationView 展示丰富菜单

虽然我们实现了滑动菜单,但它有些太单调了,只使用了一个 TextView 显示了一段文字内容。

当然,我们可以对滑动菜单页面定制布局,但 Google 给我们提供了 NavigationView 控件,它可以很轻松地实现符合 Material Design 规范的菜单页面。

一个标准的导航菜单通常包含顶部的头部布局,显示用户头像和信息,和下方的菜单项列表。我们来分别准备这两个部分。

  1. 创建菜单项(menu):res/menu 文件夹中,新建一个 nav_menu.xml 文件,代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <group android:checkableBehavior="single">
            <item
                android:id="@+id/navCall"
                android:icon="@drawable/nav_call"
                android:title="Call" />
            <item
                android:id="@+id/navFriends"
                android:icon="@drawable/nav_friends"
                android:title="Friends" />
            <item
                android:id="@+id/navLocation"
                android:icon="@drawable/nav_location"
                android:title="Location" />
            <item
                android:id="@+id/navMail"
                android:icon="@drawable/nav_mail"
                android:title="Mail" />
            <item
                android:id="@+id/navTask"
                android:icon="@drawable/nav_task"
                android:title="Tasks" />
        </group>
    </menu>
    

    我们使用 <group> 标签包裹了所有 <item> 菜单项,指定了其 checkableBehavior 属性值为 single,让菜单项具有单选的效果。

  2. 创建头布局(headerLayout):res/layout 文件夹中,新建一个 nav_header.xml 文件,代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:background="?attr/colorPrimary"
        android:padding="10dp">
    
        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/iconImage"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:layout_centerInParent="true"
            android:src="@drawable/avatar"
            app:shapeAppearanceOverlay="@style/ShapeAppearance.App.CircleImageView" />
    
        <TextView
            android:id="@+id/mailText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:text="liangyu.chen719@gmail.com"
            android:textColor="#FFF"
            android:textSize="14sp" />
    
        <TextView
            android:id="@+id/userText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@id/mailText"
            android:text="Snow"
            android:textColor="#FFF"
            android:textSize="14sp" />
    </RelativeLayout>
    

    为了让 ShapeableImageView 变为圆形,我们需要在 res/values/styles.xml 文件中定义一个 style

    <style name="ShapeAppearance.App.CircleImageView" parent="">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSize">50%</item>
    </style>
    

现在,我们就可以使用 NavigationView 了,回到 activity_main.xml 中,将之前的 TextView 换成 NavigationView。代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />
    </FrameLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_menu" />

</androidx.drawerlayout.widget.DrawerLayout>

我们通过 headerLayoutmenu 属性,指向了我们之前定义的头布局和菜单。

虽然现在样子有了,我们再来完成它的菜单项点击事件,在 MainActivity 中添加如下代码:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        binding.navView.apply {
            // 设置默认选中项
            setCheckedItem(R.id.navCall)
            // 设置菜单项点击监听
            setNavigationItemSelectedListener { menuItem ->
                // 根据点击的菜单项id执行不同操作
                when (menuItem.itemId) {
                    R.id.navCall -> Toast.makeText(
                        applicationContext,
                        "You clicked Call",
                        Toast.LENGTH_SHORT
                    ).show()

                    R.id.navFriends -> Toast.makeText(
                        applicationContext,
                        "You clicked Friends",
                        Toast.LENGTH_SHORT
                    ).show()

                    R.id.navLocation -> Toast.makeText(
                        applicationContext,
                        "You clicked Location",
                        Toast.LENGTH_SHORT
                    ).show()
                }

                // 关闭指定的抽屉
                binding.drawerLayout.closeDrawer(GravityCompat.START)
                // 返回 true,表示该菜单项已被处理,并会显示为选中状态
                true
            }
        }
        

    }

    ...
}

我们设置了 Call 菜单项为默认选中项。然后设置了每个菜单项的点击事件都是弹出 Toast 提示,然后关闭滑动菜单,并且最后返回 true,会让 NavigationView 将当前点击的项保持高亮选中状态。

现在,再次运行程序,点击导航按钮,你会看到:

image.png

未完待续...