12.3--滑动菜单

滑动菜单可以说是 Material Design 中最常见的效果之一了,在许多著名的应用(如 Gmail、 Google Photo等)中,都有滑动菜单的功能。虽说这个功能看上去好像挺复杂的,不过借助Google提供的各种工具,我们可以很轻松地实现非常炫醋的滑动菜单效果,那么我们马上开始吧。

 

12.3.1 DrawerLayout

所谓的滑动菜单就是将一些萊单选项隐藏起来,而不是放置在主屏幕上,然后可以通过滑动的方式将菜单显示出来。这种方式既节省了屏幕空间,又实现了非常好的动画效果,是 Material Design 中推荐的做法。

不过如果我们全靠自己去实现上述功能的话,难度恐怕就很大了。幸运的是,Google 在AndroidX 库中提供了 一个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:tools="http://schemas.android.com/tools"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".MainActivity">

    <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="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            />

    </FrameLayout>
    <TextView
        android:layout_width="match_parent"
        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,DrawerLayout 中放置了两个直接子控件;

第一个子控件是 FrameLayout,用于作为主屏暮中显示的内容,当然里面还有我们刚刚定义的 Toolbar。

第二个子控件这里使用了一个 TextView,用于作为滑动菜单中显示的内容,其实使用什么都可以,DrawerLayout 并没有限制只能使用固定的控件。

但是关于第二个子控件有一点需要注意,Layout_gravity 这个属性是必须指定的,因为我们需要告诉 DrawerLayout 滑动菜单是在屏幕的左边还是右边,指定 left 表示滑动菜单在左边,指定 right 表示滑动菜单在右边。这里我指定了 start,表示会根据系统语言进行判断,如果系统语言是从左往右的,比如英语、汉语,滑动菜单就在左边,如果系统语言是从右往左的,比如阿拉伯语,滑动菜单就在右边。

没错,只需要改动这么多就可以了,现在重新运行一下程序,然后在屏幕的左侧边缘向右拖动,就可以让滑动菜单显示出来了,如图所示。

然后向左滑动菜单,或者点击一下菜单以外的区域,都可以让滑动菜单关闭,从而回到主界面。无论是展示还是隐藏滑动菜单,都是有非常流畅的动画过渡的。

可以看到,我们只是稍微改动了一下布局文件,就能实现如此炫醋的效果,是不是觉得挺激动呢?不过现在的滑动菜单还有点问题,因为只有在屏幕的左侧边缘进行拖动时才能将菜单拖出来,而很多用户可能根本就不知道有这个功能,那么该怎么提示他们呢?

Material Design 建议的做法是在 Toolbar 的最左边加入一个导航按钮,点击了按钮也会将滑动菜单的内容展示出来。这样就相当于给用户提供了两种打开滑动菜单的方式,防止一些用户不知道屏幕的左侧边缘是可以拖动的。

下面我们开始来实现这个功能。首先我准备了一张导航按钮的图标 ic_menu.png,将它放在了 drawable-xxhdpi 目录下。然后修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.let {
            it.setDisplayHomeAsUpEnabled(true)
            it.setHomeAsUpIndicator(R.drawable.ic_menu)
        }
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.toolbar,menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            ...
            android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
        }
        return true
    }
}

这里我们并没有改动多少代码,首先调用getSupportActionBar() 方法得到了ActionBar的实例,虽然这个ActionBar 的具体实现是由Toolbar 来完成的。接着在ActionBar 不为空的情况下调用setDisplayHomeAsUpEnabled() 方法让导航按钮显示出来,调用setHomeAsUpIndicator() 方法来设置一个导航按钮图标。实际上,Toolbar 最左侧的这个按钮就叫做Home 按钮,它默认的图标是一个返回箭头,含义是返回上一个Activity。很明显,这里我们将它默认的样式和作用都进行了修改

接下来,在onOptionsItemSelected() 方法中对 Home 按钮的点击事件进行处理,Home 按钮的id 永远是android.R.id.home。然后调用DrawerLayout 的openDrawer() 方法将滑动菜单展示出来,注意,openDrawer() 方法要求传入一个Gravity 参数,为了保证这里的行为和XML 中定义的一致,我们传入了GravityCompat.START。

现在重新运行一下程序,效果如图所示。

可以看到,在 Toolbar 的最左边出现了一个导航按钮,用户看到这个按钮就知道这肯定是可以点击的。现在点击一下这个按钮,滑动菜单界面就会再次展示出来了。

 

12.3.2 NavigationView

目前我们已经成功实现了滑动菜单功能,其中滑动功能已经做得非常好了,但是菜单却还很丑,毕竟菜单页面仅仅使用了一个 TextView,非常单调。有对比才会有落差,我们看一下Play Store 的滑动菜单页面是长什么样的,如图所示。

 

类似这样的,但凡是黑白图片都是从第二版 的PDF 中截取的。

经过对比之后是不是党得我们的滑动菜单页面更丑了?不过没关系,优化滑动菜单页面,这就是我们本小节的全部目标。

事实上,你可以在滑动菜单页面定制任意的布局,不过Google给我们提供了一种更好的方法 ——使用 NavigationView。NavigationView 是 Design Support 库中提供的一个控件,它不仅是严格按照 Material Design 的要求来进行设计的,而且还可以将滑动菜单页面的实现变得非常简单。接下来我们就学习ー下 NavigationView 的用法。

首先,既然这个控件是Material 库中提供的,那么我们就需要将这个库引入项目中才行。打开app/build.gradle 文件,在dependencies 闭包中添加如下内容:

dependencies {
    ...
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'de.hdodenhof:circleimageview:3.0.1'
}

这里添加了两行依赖关系,第一行就是 Material 库,第二行是一个开源项目 CirclelmageView,它可以用来轻松实现图片圆形化的功能,我们待会就会用到它。

在开始使用 NavigationView 之前,我们还需要提前准备好两个东西:menu 和 headerLayout。menu 是用来在 NavigationView 中显示具体的菜单项的,headerLayout 则是用来在 NavigationView 中显示头部布局的。

先来准备 menu,这里我事先找了几张图片来作为按钮的图标,并将它们放在了drawable-xxhdpi 目录下。右击 menu 文件夹→New→ Menu resource file,创建一个 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>

我们首先在<menu> 中嵌套了一个<group>标签,然后将group 的 checkableBahavior 属性指定为single。group 表示一个组,checkableBehavior 指定为single 表示组中的所有菜单项只能单选

下面我们来看一下这些菜单项吧。这里一共定义了5个item,分别使用android:id 属性指定菜单项的id,android:icon 属性指定菜单项的图标,android:title 指定菜单项显示的文字。就是这么简单,现在我们已经把menu 准备好了。

接下来应该准备 headerLayout 了,这是一个可以随意定制的布局,不过我并不想将它做得太复杂。这里简单起见,我们就在 headerLayout 中放置头像、用户名、邮箱地址这 3 项内容吧。

说到头像,那我们还需要再准备一张图片,这里我找了一张宠物图片,并把它放在了 drawable-xxhdpi 目录下。另外这张图片最好是一张正方形图片,因为待会我们会把它圆形化。然后右击 layout 文件夹→New→ Layout resource file,创建一个 nav_header.xml 文件。修改其中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="180dp"
    android:padding="10dp"
    android:background="@color/colorPrimary"
    >

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/iconImage"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@drawable/nav_icon"
        android:layout_centerInParent="true"
        />
    <TextView
        android:id="@+id/mailText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="tonygreendev@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="Tony Green"
        android:textColor="#fff"
        android:textSize="14sp"
        />

</RelativeLayout>

可以看到, 布局文件的最外层是一个RelativeLayout,我们将它的宽度设为match_parent,高度设置为180dp,这是一个NavigationView 比较合适的高度,然后指定它的背景色为colorPrimary。

在 Relative Layout I 中我们放置了 3 个控件,CirclelmageView 是一个用于将图片圆形化的控件它的用法非常简单,基本和 ImageView 是完全一样的,这里给它指定了一张图片作为头像,然后设置为居中显示。另外两个 TextView 分别用于显示用户名和邮箱地址,它们都用到了一些 RelativeLayout 的定位属性,相信肯定难不倒你吧?

现在 menu 和 headerLayout 都准备好了,我们终于可以使用 NavigationView 了。修改 activity_main xml 中的代码,如下所示:

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

    <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="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            />

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

    </com.google.android.material.navigation.NavigationView>



</androidx.drawerlayout.widget.DrawerLayout>

可以看到,我们将之前的 Textview 换成了 NavigationView,这样滑动菜单中显示的内容也就变成 NavigationView 了。这里又通过 app: menu 和 app: headerLayout 属性将我们刚才准备好的 menu 和 headerLayout 设置了进去,这样 NavigationView 就定义完成了。

NavigationView 虽然定义完成了,但是我们还要去处理菜单项的点击事件才行。修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.let {
            it.setDisplayHomeAsUpEnabled(true)
            it.setHomeAsUpIndicator(R.drawable.ic_menu)
        }
        navView.setCheckedItem(R.id.navCall)
        navView.setNavigationItemSelectedListener {
            drawerLayout.closeDrawers()
            true
        }
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.toolbar,menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            R.id.backup -> Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show()
            R.id.delete -> Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show()
            R.id.settings -> Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show()
            android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
        }
        return true
    }
}

代码还是比较简单的,这里我们首先调用了Navigation 的setCheckedItem() 方法将Call 菜单项设置为默认选中。接着调用了setNavigationItemSelectedListener() 方法来设置一个菜单选中事件的监听器,当用户一点击了任意菜单项时,就会回调到传入Lamda 表达式当中,我们可以在这里编写具体的逻辑处理。这里调用了DrawerLayout 的closeDrawers() 方法将滑动菜单关闭,并返回了一个true 表示此事件已经被处理。

现在可以重新运行一下程序了,点击一下 Toolbar 左側的导航按钮,效果如图所示。

怎么样?这样的滑动菜单页面,你无论如何也不能说它丑了吧?Material Design 的魅力就在这里,它真的是一种非常美观的设计理念,只要你按照它的各种规范和建议来设计界面,最终做出来的程序就是特别好看的。

相信你对现在做出来的效果也一定十分满意吧?不过不要满足于现状,后面我们会实现更加炫酷的效果。跟紧脚步,继续学习。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值