
3.2 导航抽屉
虽然滑动式抽屉可以在屏幕任意一侧显示,但导航抽屉应该一直位于屏幕左侧,并且导航抽屉的elevation属性值应该比除状态栏和导航栏之外的所有其他视图的值更高。可以将导航抽屉想象成一个大部分时间隐藏在屏幕边缘的常驻固定物,如图3-8所示。

图3-8
在有设计库之前,诸如导航视图之类的组件只能通过其他视图来构建。虽然库极大地简化了这一过程,并帮我们省去了必须手动执行许多Material原则的麻烦,但仍有一些指导准则需要注意。最好的领悟方式是从头开始创建一个导航滑动式抽屉。这会涉及创建布局、应用相关组件比例的Material指导方针,以及用代码将以上内容连接在一起。
3.2.1 抽屉结构
当设置项目时,你应该会注意到Android Studio提供了一个Navigation Drawer Activity(导航抽屉式活动)模板。这个模板创建了大部分我们可能需要的结构,而且可以大幅节省工作量。当我们决定了“三明治制作应用程序”要包含哪些功能后,就会使用该模板。不过,从零开始加入一项功能,学习它是如何工作的,更具指导意义。考虑到这一点,我们将创建一个抽屉布局,通过Asset Studio可以轻松获取所需的图标。
(1)打开一个Android Studio项目,最低SDK版本为21或更高,为其提供自定义的颜色和主题。
(2)将以下代码添加到styles.xml文件中。
<item name="android:statusBarColor"> @android:color/transparent </item>
(3)确保有以下编译依赖。
compile 'com.android.support:design:23.4.0'
(4)如果没有使用上一节的项目,则创建名为toolbar.xml的应用程序栏布局。
(5)打开activity_main,替换为以下代码。
<android.support.v4.widget.DrawerLayout 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/drawer" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include android:id="@+id/toolbar" layout="@layout/toolbar" /> <FrameLayout android:id="@+id/fragment" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> </LinearLayout> <android.support.design.widget.NavigationView android:id="@+id/navigation_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/header" app:menu="@menu/menu_drawer" /> </android.support.v4.widget.DrawerLayout>
如你所见,此处的根布局是支持库中提供的DrawerLayout。注意fitsSystemWindows属性,这是抽屉可以延伸到屏幕顶部状态栏下面的原因。在样式中将statusBarColor设置为android:color/transparent后,透过状态栏可以看到抽屉。
即便使用了AppCompat,在Android版本低于5.0(API 21)的设备上也无法显示出此效果。不仅如此,这种设置还会改变header外观的宽高比,并会对图像进行裁剪。为此,请创建一个不设置fitsSystemWindows属性的styles.xml替代资源。
布局的其余部分由线性布局(LinearLayout)和导航视图(NavigationView)组成。线性布局中包含应用程序栏和空的帧布局(FrameLayout)。帧布局是最简单的布局,只包含一个项,通常用作占位符。在当前情况下,它将包含用户从导航菜单中选择的内容。
从前面的代码可以看出,需要一个header的布局文件和抽屉的菜单文件。header.xml文件应该在layout目录中创建,示例如下所示。
<? 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="header_height" android:background="@drawable/header_background" android:orientation="vertical"> <TextView android:id="@+id/feature" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@+id/details" android:gravity="left" android:paddingBottom="8dp" android:paddingLeft="16dp" android:text="@string/feature" android:textColor="#FFFFFF" android:textSize="14sp" android:textStyle="bold" /> <TextView android:id="@+id/details" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@+id/feature" android:layout_alignParentBottom="true" android:layout_marginBottom="16dp" android:gravity="left" android:paddingLeft="16dp" android:text="@string/details" android:textColor="#FFFFFF" android:textSize="14sp" /> </RelativeLayout>
需要将以下值添加到dimens.xml文件中:
<dimen name="header_height">192dp</dimen>
如你所见,这里的header需要一个图像。此处它被称为header_background,宽高比应为4∶3。
如果在具有不同屏幕密度的设备上测试此布局,就可以看到此宽高比无法保持。通过使用配置限定符(与管理图像资源类似的方式)可以轻松应对此问题。为此,请遵循以下几个简单的步骤。
(1)为每种密度范围创建新目录,例如values-ldpi、values-mdpi,等等,直到values-xxxhdpi。
(2)在每个文件夹中,复制出一份dimens.xml文件。
(3)在每个文件夹中,分别设置匹配该屏幕密度的header_height值。
菜单文件名为menu_drawer.xml,应放在menu目录下,你可能需要创建该目录。每个菜单项都有一个关联的图标,这些图标都可以在Asset Studio中找到。代码如下所示。
<? xml version="1.0" encoding="utf-8"? > <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/drama" android:icon="@drawable/drama"/> <item android:id="@+id/film" android:icon="@drawable/film"/> <item android:id="@+id/sport" android:icon="@drawable/sport"/> <item android:id="@+id/news"> <menu> <item android:id="@+id/national" android:icon="@drawable/news"/> <item android:id="@+id/international" android:icon="@drawable/international"/> </menu> </item> </menu>
多亏了设计库,滑动式抽屉和导航视图的大多数度量(如外边距和文本大小)处理好了,但抽屉header的文本大小、位置和颜色还没有被处理。虽然文本和header共享背景,但文本应当作一个高为56dp,内部内边距为16dp,行间距为8dp的组件。此外,正确的文本颜色、大小和权重都可以从前面的代码中衍生出来。
3.2.2 比例关键设计线
当一个元素(例如滑动式抽屉)填满屏幕的整个高度,且像抽屉一样被划分成垂直的片段时,必须按照比例关键设计线分割header和内容,分割比即元素的宽度与分隔内容距顶部的距离之比。在Material布局中有6种允许的比例,比例定义为宽高比(width:height),如下所示(见图3-9):

图3-9
❑ 16∶9
❑ 3∶2
❑ 4∶3
❑ 1∶1
❑ 3∶4
❑ 2∶3
此处的示例,选择了4∶3的比例,并且抽屉的宽度是256dp。我们也可以生成一个比例为16∶9的header,并将layout_height设置为144dp。
比例关键设计线仅与内含元素距离顶部的距离有关。在其下方放置一个新视图时,不能按照距离顶部16∶9的比例放置。但如果它从上方视图的底部延伸到另一个比例关键线的话,可以在其下方放置另外一个视图,如图3-10所示。

图3-10
3.2.3 激活抽屉
接下来,就是使用Java实现一些代码,使布局工作。当用户与抽屉交互时,这主要是通过使用监听器回调的方法完成的。下面的步骤演示了如何实现(结果如图3-11所示)。

图3-11
(1)打开MainActivity文件并在onCreate()方法中添加以下代码,使工具栏替换操作栏:
toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar);
(2)在此下方,添加以下代码以配置抽屉:
drawerLayout = (DrawerLayout) findViewById(R.id.drawer); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.openDrawer, R.string.closeDrawer) { public void onDrawerOpened(View v) { super.onDrawerOpened(v); } public void onDrawerClosed(View v) { super.onDrawerClosed(v); } }; drawerLayout.setDrawerListener(toggle); toggle.syncState();
(3)最后,添加此代码以设置导航视图:
navigationView = (NavigationView) findViewById(R.id.navigation_view); navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem item) { drawerLayout.closeDrawers(); switch (item.getItemId()) { case R.id.drama: Log.d(DEBUG_TAG, "drama"); return true; case R.id.film: Log.d(DEBUG_TAG, "film"); return true; case R.id.news: Log.d(DEBUG_TAG, "news"); return true; case R.id.sport: Log.d(DEBUG_TAG, "sport"); return true; default: return true; } } });
前面的Java代码可以在设备或模拟器上查看抽屉,但可选择的导航项很少,需要使其导航到应用程序的其他部分。这很容易实现,很快我们就会讲到如何操作。此外,前面的代码中还有一两个需要注意的点。
从ActionBarDrawerToggle开始的代码是打开抽屉的汉堡包图标出现在应用程序栏上的原因,也可以通过从屏幕左侧向内滑动来打开抽屉。两个字符串参数openDrawer和closeDrawer是为了可访问性,为无法清楚看到屏幕的用户读出字符串内容。该部分用户可以说一些类似导航抽屉打开、导航抽屉关闭的话。虽然onDrawerOpened()和onDrawerClosed()两个回调方法是空的,但它们演示了可以截获这些事件的位置。
drawerlayout.closedrawers()的调用是必要的,否则抽屉将保持打开状态。这里,可以使用调试器来测试输出,但理想情况下,希望菜单将我们送往应用程序的其他部分。这不是一项困难的任务,同时也提供了一个很好的机会来介绍SDK中最有用、最通用的类之一——碎片(fragment)。
3.2.4 添加碎片
目前为止,从我们所学的内容来看,可以保险地设想在具有多个功能的应用程序中单独使用活动。虽然这种情况经常发生,但资源和活动的消耗可能会非常昂贵,且它们总是会填满整个屏幕。碎片的运行像迷你活动,既有Java和XML定义,又有许多与活动相同的回调和功能。与活动不同是,碎片不是顶级组件,必须存于宿主活动中,好处是每个屏幕可以存在多个碎片。
要学习如何执行此操作,请新建一个名为ContentFragment的Java类,并完成如下所示的操作,确保导入android.support.v4.app.Fragment而不是导入标准版本:
public class ContentFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.content, container, false); return v; } }
对于XML元素,创建一个名为content.xml的布局文件,并将你选择的视图或小部件放置其中。现在所需的是选中导航项时要调用的Java代码。
打开MainActivity.Java文件,并使用以下语句替换switch语句中的调试代码。
ContentFragment fragment = new ContentFragment(); android.support.v4.app.FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment, fragment); transaction.addToBackStack(null); transaction.commit();
这里构建的示例只用于演示抽屉式布局和导航视图的基本用法。显然,要添加任何实际功能,菜单中的每一项都需要有一个碎片以及代码transaction.addToBackStack(null)。实际上,若非需要,这样写是多余的。上述代码的功能是确保系统记录下用户访问每个碎片的顺序,就像记录使用哪些活动一样。因此,当用户按下后退键时,系统将返回到前一个碎片。没有该代码,系统将返回到先前的应用程序,容器活动将被销毁。
3.2.5 右侧抽屉
作为顶级导航组件,滑动式抽屉只应从左侧滑入,并应遵循之前概述的指标。不过,从右侧滑入的抽屉是很容易实现的,一些次要功能可以使用右侧滑入的抽屉(如图3-12所示)。

图3-12
使滑动抽屉在右侧显示只需简单设置layout_gravity,如下所示。
android:layout_gravity="end"
传统的导航视图宽度不应该超过屏幕宽度减去主应用程序栏的高度,与传统的导航视图不同,右侧抽屉可以延伸至整个屏幕。
本章所有内容都关于UI设计,没有任何设计模式。这里本可以使用模式,却选择了Android的UI机制。在本书后面我们会看到对于简化复杂菜单或布局的编码,外观模式是多么有用。
有一种设计模式,绝大部分地方会介绍它,它就是单例模式。这是因为绝大部分地方可以使用,其作用是提供某个对象的一个全局实例。