创建抽屉式导航栏

抽屉式导航栏是一个面板,它将应用的主要导航选项显示在屏幕左边缘。大多数情况下,它处于隐藏状态,但是如果用户从屏幕左边缘滑动手指,同时在应用顶层触摸操作栏中的应用图标,它将会显示出来。

本课程介绍如何使用支持库中提供的 DrawerLayout API 实现抽屉式导航栏。

抽屉式导航栏设计

决定在应用中使用抽屉式导航栏之前, 您应了解抽屉式导航栏设计 指南中定义的用例和设计原则。

创建抽屉式导航栏布局

要添加抽屉式导航栏,请将包含 DrawerLayout 对象的用户界面声明为布局的根视图。在 DrawerLayout 内,添加一个包含屏幕主内容(当抽屉式导航栏处于隐藏状态时为主要布局)的视图和另一个包含抽屉式导航栏内容的视图。

例如,以下布局使用包含两个子视图的 DrawerLayout:包含主内容的 FrameLayout(在运行时由 Fragment 填充)和抽屉式导航栏的 ListView

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!-- The navigation drawer -->
    <ListView android:id="@+id/left_drawer"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="#111"/>
</android.support.v4.widget.DrawerLayout>

此布局演示了一些重要的布局特性:

  • DrawerLayout 中,主内容视图(上面的 FrameLayout必须是第一个子视图,因为 XML 顺序意味着按 z 序(层叠顺序)排序,并且抽屉式导航栏必须位于内容顶部。
  • 主内容视图设置为匹配父视图的宽度和高度, 因为在抽屉式导航栏处于隐藏状态时, 它代表整个 UI。
  • 抽屉式导航栏视图 (ListView) 必须使用 android:layout_gravity 属性指定其水平重力。要支持“从右到左”(RTL) 语言,请使用 "start"(而非 "left")指定该值(这样当布局为 RTL 时,抽屉式导航栏会显示在右侧)。

  • 抽屉式导航栏视图以 dp 为单位指定其宽度, 且高度与父视图相匹配。抽屉式导航栏的宽度不应超过 320dp,从而用户始终可以看到部分主内容。

初始化抽屉式导航栏列表

在您的 Activity 中,首要任务之一是初始化抽屉式导航栏的项目列表。 如何执行该操作取决于您的应用内容,但抽屉式导航栏通常包含 ListView,因此该列表应由 Adapter 填充(例如,ArrayAdapterSimpleCursorAdapter)。

例如,您可以按如下方法 使用字符串数组初始化导航列表:

public class MainActivity extends Activity {
    private String[] mPlanetTitles;
    private DrawerLayout mDrawerLayout;
    private ListView mDrawerList;
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPlanetTitles = getResources().getStringArray(R.array.planets_array);
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerList = (ListView) findViewById(R.id.left_drawer);

        // Set the adapter for the list view
        mDrawerList.setAdapter(new ArrayAdapter<String>(this,
                R.layout.drawer_list_item, mPlanetTitles));
        // Set the list's click listener
        mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

        ...
    }
}

此代码还调用 setOnItemClickListener() 来接收抽屉式导航栏列表中的点击事件。下一部分介绍如何在用户选择某一项时实现此接口并更改内容视图。

处理导航点击事件

当用户在抽屉式导航栏的列表中选择某一项时,系统会在提供给 setOnItemClickListener()OnItemClickListener 上调用 onItemClick()

您在 onItemClick() 方法中执行的操作取决于应用结构的实现方法。在以下示例中,在该列表中选择每个项目会将不同的 Fragment 插入主内容视图(由 R.id.content_frame ID 标识的 FrameLayout 元素):

private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        selectItem(position);
    }
}

/** Swaps fragments in the main content view */
private void selectItem(int position) {
    // Create a new fragment and specify the planet to show based on position
    Fragment fragment = new PlanetFragment();
    Bundle args = new Bundle();
    args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
    fragment.setArguments(args);

    // Insert the fragment by replacing any existing fragment
    FragmentManager fragmentManager = getFragmentManager();
    fragmentManager.beginTransaction()
                   .replace(R.id.content_frame, fragment)
                   .commit();

    // Highlight the selected item, update the title, and close the drawer
    mDrawerList.setItemChecked(position, true);
    setTitle(mPlanetTitles[position]);
    mDrawerLayout.closeDrawer(mDrawerList);
}

@Override
public void setTitle(CharSequence title) {
    mTitle = title;
    getActionBar().setTitle(mTitle);
}

侦听打开和关闭事件

要侦听抽屉式导航栏的打开和关闭事件,请在 DrawerLayout 上调用 setDrawerListener() 并向其传递 DrawerLayout.DrawerListener 的实现。此接口为抽屉式导航栏事件(例如,onDrawerOpened()onDrawerClosed())提供了回调。

但是,如果您的 Activity 包括操作栏,则可扩展 ActionBarDrawerToggle 类,而非实现 DrawerLayout.DrawerListenerActionBarDrawerToggle 实现了 DrawerLayout.DrawerListener,因此您仍然可以替代这些回调,但这还有助于在操作栏图标与抽屉式导航栏之间正确交互(下一部分中将进一步阐述)。

正如抽屉式导航栏设计指南中所述,您应在抽屉式导航栏可见时修改操作栏的内容,例如,更改标题和移除与主内容有关的操作项目。 以下代码显示了如何通过使用 ActionBarDrawerToggle 类的实例替代 DrawerLayout.DrawerListener 回调方法来实现这一点:

public class MainActivity extends Activity {
    private DrawerLayout mDrawerLayout;
    private ActionBarDrawerToggle mDrawerToggle;
    private CharSequence mDrawerTitle;
    private CharSequence mTitle;
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...

        mTitle = mDrawerTitle = getTitle();
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {

            /** Called when a drawer has settled in a completely closed state. */
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
                getActionBar().setTitle(mTitle);
                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
            }

            /** Called when a drawer has settled in a completely open state. */
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                getActionBar().setTitle(mDrawerTitle);
                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
            }
        };

        // Set the drawer toggle as the DrawerListener
        mDrawerLayout.setDrawerListener(mDrawerToggle);
    }

    /* Called whenever we call invalidateOptionsMenu() */
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // If the nav drawer is open, hide action items related to the content view
        boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
        menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
        return super.onPrepareOptionsMenu(menu);
    }
}

下一部分介绍 ActionBarDrawerToggle 构造函数参数以及将构造函数设置为处理与操作栏图标之间的交互所需执行的其他步骤。

通过应用图标打开和关闭

用户可以通过远离或朝向屏幕的左边缘滑动手势打开和关闭抽屉式导航栏,但是如果您正在使用操作栏,还应允许用户通过触摸应用图标来打开和关闭它。 而且,应用图标还应通过特殊图标说明抽屉式导航栏的存在。 您可以使用上一部分中显示的 ActionBarDrawerToggle 来实现所有这些行为。

要让 ActionBarDrawerToggle 正常运行,请使用构造函数创建其实例,这需要以下参数:

  • 托管抽屉式导航栏的 Activity
  • DrawerLayout
  • 用作抽屉式导航栏指示器的可绘制对象资源。

    下载操作栏图标包中提供了标准抽屉式导航栏图标。

  • 用于描述“打开抽屉式导航栏”操作的字符串资源(用于无障碍功能)。
  • 用于描述“关闭抽屉式导航栏”操作的字符串资源(用于无障碍功能)。

然后,无论您是否已创建 ActionBarDrawerToggle 的子类作为抽屉式导航栏侦听器,在整个 Activity 生命周期中,您都需要在几个位置调用 ActionBarDrawerToggle

public class MainActivity extends Activity {
    private DrawerLayout mDrawerLayout;
    private ActionBarDrawerToggle mDrawerToggle;
    ...

    public void onCreate(Bundle savedInstanceState) {
        ...

        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(
                this,                  /* host Activity */
                mDrawerLayout,         /* DrawerLayout object */
                R.drawable.ic_drawer,  /* nav drawer icon to replace 'Up' caret */
                R.string.drawer_open,  /* "open drawer" description */
                R.string.drawer_close  /* "close drawer" description */
                ) {

            /** Called when a drawer has settled in a completely closed state. */
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
                getActionBar().setTitle(mTitle);
            }

            /** Called when a drawer has settled in a completely open state. */
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                getActionBar().setTitle(mDrawerTitle);
            }
        };

        // Set the drawer toggle as the DrawerListener
        mDrawerLayout.setDrawerListener(mDrawerToggle);

        getActionBar().setDisplayHomeAsUpEnabled(true);
        getActionBar().setHomeButtonEnabled(true);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        mDrawerToggle.syncState();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mDrawerToggle.onConfigurationChanged(newConfig);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Pass the event to ActionBarDrawerToggle, if it returns
        // true, then it has handled the app icon touch event
        if (mDrawerToggle.onOptionsItemSelected(item)) {
          return true;
        }
        // Handle your other action bar items...

        return super.onOptionsItemSelected(item);
    }

    ...
}

有关抽屉式导航栏的完整示例,请下载页面顶部提供的示例。