前言
书接上回...
滑动菜单也是 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。这里我们把它的默认的图标和作用都改变了。
现在运行程序,界面效果如下:
点击左上角的导航按钮,就会出现滑动菜单:
使用 NavigationView 展示丰富菜单
虽然我们实现了滑动菜单,但它有些太单调了,只使用了一个 TextView 显示了一段文字内容。
当然,我们可以对滑动菜单页面定制布局,但 Google 给我们提供了 NavigationView 控件,它可以很轻松地实现符合 Material Design 规范的菜单页面。
一个标准的导航菜单通常包含顶部的头部布局,显示用户头像和信息,和下方的菜单项列表。我们来分别准备这两个部分。
-
创建菜单项(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,让菜单项具有单选的效果。 -
创建头布局(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>
我们通过 headerLayout 和 menu 属性,指向了我们之前定义的头布局和菜单。
虽然现在样子有了,我们再来完成它的菜单项点击事件,在 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 将当前点击的项保持高亮选中状态。
现在,再次运行程序,点击导航按钮,你会看到:
未完待续...