Android最佳实践之高效的应用导航

设计(一)- 规划Screens和他们之间的关系

原文地址:http://developer.android.com/training/design-navigation/screen-planning.html#beyond-simplistic-design

设计和开发Android应用程序的第一个步骤是确定用户能够查看和处理应用。一旦你知道用户与之交互的应用程序之间交互什么数据,下一步就是设计交互,允许用户导航到app的不同部分,进入和退出应用程序中的界面。

这篇文章开始向你展示如何规划高水平的应用程序层次结构,然后选择合适的导航形式允许用户有效、直观地遍历你的内容。每一堂课涵盖了Android应用程序导航的交互设计过程的不同阶段,大致是按照时间排序的。学完这门课程后,你应该能够把这里概述的方法和导航模式,将其应用到你自己的应用程序中,为用户提供一个流畅的导航体验。

规划Screens和他们之间的关系

大多数应用程序都有一个固有的信息模型,可以表示为一个树或图的对象类型。很明显,你可以将用户在你的应用交互中代表的不同信息类型画成一个图。软件工程师和数据架构师经常使用实体关系图(ERD)来描述应用程序的信息模型。
让我们考虑一个示例应用程序,允许用户浏览一组分类新闻和照片。这样的一个可能的模型应用在ERD的形式如下所示

创建一个Screen列表

定义信息模型之后,你可以开始定义必要的上下文,以便用户能够有效地发现、观察并按应用程序中的数据操作。实际操作中,实现的方法之一是确定哪些屏幕允许用户导航到数据并与之交互的。这些屏幕的展现我们实际上通常取决于目标设备;特别是在设计过程的早期考虑这个非常重要,它能确保应用程序能够适应设备环境。
在我们的示例应用程序中,我们想让用户查看、保存、分享分类的故事和照片。下面是这些用例的屏幕的详尽清单:
- 桌面或快捷方式用来访问Story和Photo
- 分类列表Category
- 给定类别的新闻列表
- 未分类的的照片列表,
- 照片详细信息视图(我们可以保存和分享)
- 所有保存项目的列表
- 保存照片的列表
- 保存Story的列表
- 图表和Screen的关系

现在我们可以定义Screen之间的直接关系,箭头从一个屏幕A到另一个屏幕B意味着屏幕B应该可以被一些用户直接通过屏幕A访问到。一旦我们定义两组屏幕和它们之间的关系,我们可以一致的表达这些屏幕为地图,显示你所有的屏幕和它们之间的关系:

如果我们以后想允许用户提交新闻故事或上传照片,我们可以添加另外的屏幕图。

极简的设计

我们可以从这个详尽的屏幕地图上设计一个功能齐全的应用程序。一个简单的用户界面可以包含列表,列表可以点击按钮导航到子屏幕
按钮导航到不同部分(如故事、照片,保存项目)
垂直列表代表收藏(如照片列表,故事列表等等)。
详细信息视图。(全屏照片视图等等)
不过,你可以使用屏幕分组技术和更先进的导航元素更直观、屏幕友好的展示内容。下节课,我们探索屏幕分组技术,如为平板设备提供多窗格布局。之后,我们将深入介绍在Android上的各种导航模式

设计(二)- 规划多个触摸屏尺寸

原文地址:http://developer.android.com/training/design-navigation/multiple-sizes.html#multi-pane-layouts
上面讲的详细的屏幕地图并不能关联到一个特定设备尺寸,尽管它通常能手机或同尺寸设备上显式良好。但是Android应用程序需要适应许多不同类型的设备,从3英寸的手机到10英寸的平板电脑再到42英寸电视。在这节课中我们将在组成多屏的详尽屏幕地图中探讨原因和策略。
注意:为电视机设计应用程序还需要注意其他因素如交互方法(没有触摸屏),远距离阅读文本的易读性等等。尽管这个讨论超出了这篇文章的范围,你可以在Google TV中的 design patterns.找到更多的信息

多窗格屏幕布局分组屏幕

3到4英寸的屏幕通常只适合于显示一个垂直面板的内容,包括列表或者详细信息,等等。因此这些设备的屏幕一般和水平层次结构信息(类别列表category→对象列表object list→对象细节object detail)是一对一的关系。
另一方面,平板电脑和电视的大屏幕,通常有更多可用的屏幕空间和能够显示多个窗格的内容。在横屏时,窗格通常从左到右顺序显示详细。用户从年复一年的大屏幕桌面应用程序和桌面web站点使用中习惯了多个窗格。很多桌面应用程序和网站提供左侧导航面板或者使用主/明细双栏布局。
除了满足这些用户的期望之外,通常需要在平板电脑上提供多个窗格的信息,以避免留下太多的空白或无意中引入尴尬的交互,

多窗格在横屏布局中有更好的视觉平衡,同时提供更多实用和易读性

注意:在决定为哪个屏幕尺寸的单窗格和多窗格布局之间绘制后,可以为不同屏幕大小设备提供包含一个或多个窗格的不同布局(如large/xlarge)或不同的最低屏幕宽度(例如sw600dp)。

注意:当一个子屏幕在Activity的子类中被分成单独的窗格,我们就可以用Fragment的子类来实现。这样可以在不同的尺寸和显示内容的区域最大化的进行代码重用。

设计成多个平板方向

虽然我们还没有开始安排我们屏幕上的用户界面元素,但这是一个很好的时间来考虑多窗格屏幕将如何适应不同设备方向。多窗格在横向布局中工作很好,因为大量的可用的水平空间。然而,在竖向屏幕中,你的水平空间很有限,所以你可能需要设计一个单独的布局方向。
- Stretch
最直接的策略是每个面板的宽度延伸到最好的展现画像中的每个窗格的内容方向。窗格可以固定宽度或采取一定比例可用的屏幕宽度
- Expand/collapse
上述延伸策略的一个变化就是在屏幕竖向时折叠。这个在master/detail的窗格中表现的很好,左边的窗格(master)可以很容易的折叠
- Show/Hide
在这个场景中,左边的面板是在竖屏模式下完全隐藏
- Stack
最后一个策略是垂直方向堆叠在水平方向放置的窗格。

在屏幕地图中进行屏幕分组

现在我们能够在大屏幕设备上通过提供多窗格布局单独进行屏幕分组,在这些设备上,将这种技术应用到我们上一课中的屏幕地图中以便更好地了解应用程序的层次结构

平板电脑最新的demo新闻应用程序屏幕地图
在接下来的课程中我们将讨论子导航和横向导航。

设计(三) - 设计子代导航和水平导航

原文地址:http://developer.android.com/training/design-navigation/descendant-lateral.html
这节课中我们将讨论子代导航(允许用户点击按钮进入子屏幕)、水平导航(允许同级访问)
app-navigation-descendant-lateral-desc
图1:竖向(子页面)和水平导航

有2种同级的导航视图:列表类(collection-related)和区域类(section-related)的。看下图:
Collection-related children and section-related children
图2:列表类和区域类导航
collection-related是通过父界面展示每一个单独的子界面,而section-related显示的一个父界面的不同部分。

子页面和水平导航可以通过Tab,List以及其他的UI设计模式来实现。UI设计模式,广义上说类似于软件设计模式,它是常见的复用交互设计问题的解决方案。下面各部分我们将探讨一些常见的水平导航模式

按钮和简单的控件(Buttons and Simple Targets)

section-related形式的导航,一般是包含了按钮,列表Views,或Text链接的UI组件。点击其中一个就可以进入相应的子界面,替换掉整个当前界面。按钮和其他一些简单的控件很少出现在List视图上。
app-navigation-descendant-lateral-buttons
图3:基于按钮导航界面和有关Screen Map的摘要示例

一般,基于按钮导航到不同层级的视图的模式称为仪表盘(dashboard)模式。dashboard模式是由一些大的,图标话的按钮充斥整个或大部分父视图的模式。网格一般有2-3列,根据不同app来定。这种模式是一种非常好的展现模式。大的UI组件很容易被点击,用户体验会比较好。然而,这种模式不适合大屏幕,因为他需要额外的操作才能进入到不同的子视图内容。

ListView、GridView、水平滚动视图(Carousels)以及卡片视图(Stacks)

对于列表类的视图,一般采用ListView|GridView
app-navigation-descendant-lateral-lists
图4:各种视图及相关对应的界面摘要

这种模式有几个问题。基于列表的导航,用户通常要进行大量的滑动点击,进入下一个列表,这种情况下性能不高,也不好处理,特别是对于“手快”的用户,用户体验不好。
使用垂直的列表视图,也会比较尴尬,因为通常在大屏上,宽度是全宽,高度的固定的,这样会留出大量的空白区域出来。一种缓和的方案是加一些额外的文字信息到列表上来填充水平的区域;另一种方案是在列表旁边添加另一个水平窗格视图。

Tabs

Tab是实现水平导航的一个流行的做法。它允许一个界面同时存在多屏内容。最适合在4英寸或更小的屏幕上使用。
app-navigation-descendant-lateral-tabs
图5:基于Tab导航的示例

使用标签时的一些最佳实践:Tab应该保留在屏幕上,当选择一个选项卡时只有指定的内容区域应该改变,Tab指示器应保持随时可用。Tab切换不应保留历史记录,比如当用户从Tab A切换到Tab B,按下返回键,就不应该返回到Tab A。Tab通常水平放置,尽管有时比如在Action Bar(pattern docs at Android Design)上是竖向排列的。最后,最重要的是,标签应该运行在屏幕的顶端,和不应该对齐屏幕的底部。

相对于基于按钮的导航,Tab导航有一些很明显的优势。

  • 因为是初始化好的Tab,所以选中后可直接访问里面的内容
  • 用户可以轻松的访问相邻的屏幕内容,而不用先回到父界面

注意:切换Tab时,不要有阻塞的操作。比如Loading数据时显示模态对话框。

水平分页导航 (Swipe Views)

另一个流行的水平导航模式是水平分页导航,也叫Swipe Views。这种模式最适用于子代导航的兄弟屏幕上一个列表(世界、业务、技术和健康等)。像Tab一样,这种模式还允许分组屏幕,父界面提供子界面的内容嵌入到自己的布局。
app-navigation-descendant-lateral-paging
图6:水平分页导航

在一个水平分页导航中,一次只能显示一个子页面。可以点击或拖拽邻近的屏幕进行切换,这样方便用户进行操作也避免设计更多的入口。示例包括点指示器(tick marks), 滚动标签(scrolling labels), 和Tabs。
app-navigation-descendant-lateral-paging-companion
图7:分页导航的UI元素的例子。

最好避免在水平导航中的子页面中有水平平移的交互,这样UI上会有冲突(比如子界面里嵌入一个地图控件)。

在下一课,我们将讨论允许用户浏览信息层次结构以及返回之前访问过的界面的机制

设计(四) - 向上导航和时间导航

原文地址:http://developer.android.com/training/design-navigation/ancestral-temporal.html#ancestral-navigation
既然用户可以沿着视图的层级向下导航到一级一级,那么我们也需要提供一个方法向上导航返回他的父界面一直到最上面。另外我们还要确保时间导航遵守Android的规则。

支持时间导航-回退

时间导航,或者说历史界面之间的导航,是深深扎根于Android系统之中。所有的Android用户期望通过Back按钮回到前面的界面。按Back键我们总是可以回到系统桌面的,再按就没有效果了。
app-navigation-ancestral-navigate-back
图1:Back按钮的行为

系统已经自动处理了Back键的行为,我们不用担心。Back按钮的默认行为是返回上一个界面。
但有时候我们需要重写Back的方法,改变他的默认行为。比如,界面中包含有一个WebView,我们希望Back按钮的行为是返回上一个网页,这时就需要人为去控制它,否则他会直接关闭当前界面。

提供向上导航 - 向上(up)和Home桌面(home)

能够直接返回桌面,是一种能让用户感觉到舒服和安全的设计。无论用户在哪个app的哪个界面,他都可以通过Home键直接返回到最上层的桌面。
Android3.0提出了Up的说法,来替代上面说的Home键的功能。通过点击上一级(Up),用户能够返回视图层级的上一级(就像上面描述的Back键的功能一样),但这并不是很普遍的情况,因此,开发者应该确保Up能使界面返回到上一个简单的可呈现的父界面上。
app-navigation-ancestral-navigate-up
图2:从联系人App进入邮件App的Up导航示例
在一些情况下,使用Up导航比Back返回到父界面更适合。例如,在基于Android3.0的平板上的Gmail的app,横屏时一般左边是邮件List,右边是邮件详细,是一个典型的父 /子的设计,也是上一课描述的。当竖屏看时,我们只看到邮件的详细界面,Up按钮用来临时显示他的父界面,也就是从屏幕左边划出来的列表Panel。再点一下Up按钮,当左边部分显示后,会退出单个的邮件会话,邮件列表会全屏显示。

最后最佳实践,不管是使用Home导航还是Up导航,请确保会清掉栈里的View。在Home模式下,最终保留的界面就是Home界面。对于Up导航,当前界面应该从Back栈里清除,除此之外,导航横穿当前视图层级进入另一个。你可以使用FLAG_ACTIVITY_CLEAR_TOP 和FLAG_ACTIVITY_NEW_TASK一起来实现这个。
接下来,我们将应用到目前为止所有课中讨论的概念为我们的创建一个交互设计线框图新闻App示例。

设计(五) - 借米生蛋,示例App框架

原文地址:http://developer.android.com/training/design-navigation/wireframing.html

我们有了对屏幕导航模式和分组技术扎实的理解,是时候将它们应用到我们的设计中了。让我们来看另一个详尽的示例App界面示意图。
app-navigation-screen-planning-exhaustive-map
图1:新闻App详细的界面示意图
下一步我们就是选择和应用前面的课程中讨论的导航设计模式了。最大化的响应速度最小化的点击次数能访问到数据,同时保持界面直观,且符合Android的最佳实践。我们还需要对不同的目标设备做出不同的选择。简单起见,让我们只关注平板电脑和手机。

选择模式

首先,我们的第二层界面(StoryList,PhotoList和缓存List)应该用Tab组织起来。注意我们不一定要使用水平的Tab,对于窄长的手机有时下拉的UI组件可以充当替代角色。我们还要在手机上将Saved Photo List 和Saved Story List 组织在一起或者在平板上使用多个垂直Panel。
最后,让我们看看如何展示新闻故事(News Story)。第一项工作就是简化不同Story分类导航,在顶部设置一组标签实现水平分页并可以切换。在平板的水平方向,我们可以设置左边时列表,右边是Story的详细View。
下面这个图就显示了在手机和平板上应用了导航模式后的新的界面示意图
app-navigation-wireframing-map-example-phone
图2:最后在手机上的界面示意图

app-navigation-wireframing-map-example-tablet
图3:最后在平板上的界面示意图

此时,我们要好好想想界面示意图如何改进一下,以防在实际工作中他们不能Work Well。下图的例子,就是将平板界面示意图改了下,在不同分类下显示Story的List,然后Story的详细View单独抽出来给StoryList和Saved StoryList共用。
app-navigation-wireframing-map-example-tablet-alt
图4:变化后的平板横屏界面示意图

梗概和线框图

线框图是设计过程中将你的设计进行实际布局的一个步骤。开始发挥想象如何安排允许用户导航的UI元素。记住,此时并不需要像素级精度(创建高保真的原型)。
开始最简单快捷的方法是使用纸和铅笔手工勾勒出你的界面。一旦你开始画草图,你可能会发现在选择原来哪个示意图上存在一些问题。在某些情况下,理论上模式可能也只适用于一个特定的设计问题,但实际上他们相互矛盾。如果发生这种情况,探索其他导航模式,或改进选择的模式,以得到一个更好的草图。
决定好线框图后,就要用 Adobe® Illustrator, Adobe® Fireworks, OmniGraffle或其他一些矢量图形工具勾画出量化的框架。

数字线框图

在纸上布局草图后,选择一个适合你的数字线框图工具。创建数字线框图,将作为应用程序的视觉设计的起点。下面是我们的新闻App线框图例子,与我们的一开始的示意图是一一对应的。
app-navigation-wireframing-wires-phone
图5:手机竖向的数字线框图

app-navigation-wireframing-wires-tablet
图6:平板横向的数字线框图

下一步

现在你已经为你的App设计了有效和直观的内在的导航。接下来你就可以开始花时间每个单独的界面改进用户界面。比如,你可以选择使用更丰富的小部件代替简单的文本标签、图片和按钮时显示的内容。你也可以开始从你的品牌的可视化语言定义应用程序元素的视觉样式。

实现(一) - 使用Tabs实现滑动(Swipe)

原文地址:http://developer.android.com/training/implementing-navigation/index.html
看完这个系列课程,你将对于如何使用Tab、Swipe Views和Drawer实现导航模式有一个深刻的认识,也应该理解如何提供正确的Up和Back导航。

依赖和前提:
- Android2.2或更高
- 理解Fragment和Android布局
- Android Support Library
- 设计高效的导航(上节课的内容)
Swipe views提供切换界面的一种水平导航模式,类似于Tabs导航一样。这篇文章将告诉你如何使用Tabs创建Swipe Views,或者使用标题栏取代Tabs。

注意:需要设计高效应用导航的课程和Swipe Views的知识。

实现滑动视图(Swipe Views)

你可以使用Support Library中的ViewPager创建滑动视图,ViewPager是一种子视图都是独立布局的布局。
再布局中创建ViewPager,方法如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

向ViewPager中添加子Views,你需要为这个布局绑定一个适配器PagerAdapter。你可以使用下面2种适配器:

  • FragmentPagerAdapter
    当子视图少且固定的时候最好用这个
  • FragmentStatePagerAdapter
    当子布局比较多且不定时用这个最好,最小化内存占用。
    使用FragmentStatePagerAdapter适配一个Fragment列表的用法如下:
public class CollectionDemoActivity extends FragmentActivity {
    // When requested, this adapter returns a DemoObjectFragment,
    // representing an object in the collection.
    DemoCollectionPagerAdapter mDemoCollectionPagerAdapter;
    ViewPager mViewPager;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_collection_demo);

        // ViewPager and its adapters use support library
        // fragments, so use getSupportFragmentManager.
        mDemoCollectionPagerAdapter =
                new DemoCollectionPagerAdapter(
                        getSupportFragmentManager());
        mViewPager = (ViewPager) findViewById(R.id.pager);
        mViewPager.setAdapter(mDemoCollectionPagerAdapter);
    }
}

// Since this is an object collection, use a FragmentStatePagerAdapter,
// and NOT a FragmentPagerAdapter.
public class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {
    public DemoCollectionPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int i) {
        Fragment fragment = new DemoObjectFragment();
        Bundle args = new Bundle();
        // Our object is just an integer :-P
        args.putInt(DemoObjectFragment.ARG_OBJECT, i + 1);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public int getCount() {
        return 100;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return "OBJECT " + (position + 1);
    }
}

// Instances of this class are fragments representing a single
// object in our collection.
public static class DemoObjectFragment extends Fragment {
    public static final String ARG_OBJECT = "object";

    @Override
    public View onCreateView(LayoutInflater inflater,
            ViewGroup container, Bundle savedInstanceState) {
        // The last two arguments ensure LayoutParams are inflated
        // properly.
        View rootView = inflater.inflate(
                R.layout.fragment_collection_object, container, false);
        Bundle args = getArguments();
        ((TextView) rootView.findViewById(android.R.id.text1)).setText(
                Integer.toString(args.getInt(ARG_OBJECT)));
        return rootView;
    }
}

上面用代码展示了如何创建滑动视图(Swipe Views),下面将创建标签(Tabs)来帮助切换页面。

在Action Bar中添加Tabs

使用Action Bar添加Tab,必须先允许NAVIGATION_MODE_TABS,然后创建若干ActionBar.Tab,并实现ActionBar.TabListener接口。示例代码如下:

@Override
public void onCreate(Bundle savedInstanceState) {
    final ActionBar actionBar = getActionBar();
    ...

    // Specify that tabs should be displayed in the action bar.
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

    // Create a tab listener that is called when the user changes tabs.
    ActionBar.TabListener tabListener = new ActionBar.TabListener() {
        public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
            // show the given tab
        }

        public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
            // hide the given tab
        }

        public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
            // probably ignore this event
        }
    };

    // Add 3 tabs, specifying the tab's text and TabListener
    for (int i = 0; i < 3; i++) {
        actionBar.addTab(
                actionBar.newTab()
                        .setText("Tab " + (i + 1))
                        .setTabListener(tabListener));
    }
}

再ActionBar.TabListener中处理Tab切换的动作。如果使用Fragment实现ViewPager,接下来将展示切换时如何更新Tab的内容。

在SwipeView中切换Tab

调用ViewPager中的setCurrentItem()方法来选中当前的Tab中的内容。

@Override
public void onCreate(Bundle savedInstanceState) {
    ...

    // Create a tab listener that is called when the user changes tabs.
    ActionBar.TabListener tabListener = new ActionBar.TabListener() {
        public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
            // When the tab is selected, switch to the
            // corresponding page in the ViewPager.
            mViewPager.setCurrentItem(tab.getPosition());
        }
        ...
    };
}

同样得,实现ViewPager.OnPageChangeListener接口,当界面切换时改变Tab的内容。

用标题栏代替选项卡(Tabs)

如果你不想用ActionBar的Tab,而使用scrollable tabs创建较短的可视化界面,你可以在SwipeView中使用PagerTitleStrip

<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.PagerTitleStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#33b5e5"
        android:textColor="#fff"
        android:paddingTop="4dp"
        android:paddingBottom="4dp" />

</android.support.v4.view.ViewPager>

PagerTitleStrip是一个没有交互的指示器,它是特意被设计在XML布局中作为ViewPager 的子元素的。设置他的属性 android:layout_gravity为top或bottom来固定它在顶部或底部。

实现(二)- 创建一个导航抽屉(NavigationDrawer)

参考地址:http://developer.android.com/training/implementing-navigation/nav-drawer.html

导航抽屉是在窗体左边的显示主界面选项的一种导航。它大部分时候都是隐藏的,当用户用手指滑动主界面或点击ActionBar上的App图标时,才会显示出来。
这节课描述如何使用Support Library中的DrawerLayout来实现一个导航抽屉。

导航抽屉的设计原则参见:http://developer.android.com/design/patterns/navigation-drawer.html

创建一个Drawer Layout

使用导航抽屉,你要使用DrawerLayout作为你布局的RootView。在DrawerLayout的子View中,创建一个View作为DrawerLayout的content,另一个View作为Drawer。
比如,下面的布局使用了DrawerLayout,他有2个子View,一个是包含了主视图(一般在运行时是一个Fragment)FrameLayout,另一个是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>

这个布局展示了一些重要的布局特点:

  • 上面的主界面(FrameLayout)必须是DrawerLayout的第一个子View。因为XML中z轴顺序必须让抽屉置于内容(Content)的上面。
  • 主界面(FrameLayout)必须和它的父视图保持一样的宽高,这样Drawer隐藏时,他可以显示整个的UI
  • 抽屉视图(ListView)必须指定它的android:layout_gravity属性,为支持right-to-left (RTL) 语言,需要指定‘start’而不是‘left’将抽屉设置到左边(同样,‘end’取代‘right’设置为右边)。
  • 抽屉视图(ListView)需要用dp单位指定它的宽高,并保证不会超过320dp,这样就可以一直显示主界面了。

初始化Drawer List

在Activity中,我们首先要初始化Drawer中的内容。一般Drawer中是ListView,我们需要为它指定适配器和数据。下面我们使用String-Array作为例子:

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()为ListView指定监听,来响应点击事件。

处理导航点击事件

点击了抽屉中ListView的某一项,我们需要为它设置事件。下面的例子,我们让ListView中每一项点击后都插入一个新的Fragment到主界面中(Framelayout的id设置为R.id.content_frame)。

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有ActionBar的时候,就不能实现DrawerLayout.DrawerListener接口了,你需要继承ActionBarDrawerToggle类,因为ActionBarDrawerToggle类实现了DrawerLayout.DrawerListener,并重写其中的回调方法。但你也要处理ActionBar和导航抽屉的交互问题(下一部分将深入探讨哦)。
下面的是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的构造器参数。

使用App图标打开关闭抽屉

打开关闭抽屉导航可以使用手势滑动的方式实现,也可以在ActionBar中通过点击App图标做到。要实现这种情况,需要使用ActionBarDrawerToggle类。ActionBarDrawerToggle的构造函数有下面这些参数:
1. 托管抽屉的Activity
2. DrawerLayout实例
3. 作为抽屉指示器的Drawable资源。标准的抽屉icon参考Download the Action Bar Icon Pack.
4. 描述打开抽屉的字符串资源
5. 描述关闭抽屉的字符串资源

然后,不管你是否创建了一个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);
    }

    ...
}

下载完整的例子,请点击我

滑动视图(SwipeViews)的使用

原文参考:http://developer.android.com/design/patterns/swipe-views.html#between-tabs

高性能的导航是一个设计良好的应用程序的基石之一。应用程序通常是建立在分层视图上,在一些场景中,水平导航可以使垂直布局扁平化,并使访问相关数据变得更快更有趣。SwipeViews允许用户使用一个简单的手势浏览数据,并使浏览更流畅的体验。

在详细视图中滑动

App的数据经常被组织成“列表/详细”的关系。用户可以看到一个列表的相关数据,比如图片、图表或Email,然后选择其中一个进入单独的界面中查看详细内容。
swipe_views
图1:列表(左)/详细(右)的视图

在手机上,一般是这种模式,看列表数据需要从详细界面返回然后再选择一个点击进入另一条数据。有时候,用户希望可以通过滑动手势进行连续浏览数据。
swipe_views2
swipe_views3
图2-3:通过手势滑动查看下一条邮件

在Tab之间滑动

swipe_tabs
如果你的应用程序使用ActionBar Tabs,使用手势滑动在不同的Views之间导航。

备注:
- 用户使用手势滑动滑动平移不同的View时,不用完全滑完一个View的宽度
- 如果过去你在详细视图上使用按钮切换视图,现在请使用滑动视图
- 考虑在详细View中加入列表的索引,让用户知道现在是哪一个了。
- 使用滑动快速的在详细视图或标签页之间导航

更多详细的使用滑动视图的内容,请参考Android最佳实践之实现高效的应用导航(一) - 使用Tabs实现滑动导航

实现(三) - 提供Up导航

参考地址:http://developer.android.com/training/implementing-navigation/ancestral.html
这节课讨论如何在ActionBar上添加一个Up按钮,实现上一级的导航。
implementing-navigation
图1:ActionBar上的Up按钮

指定一个父Activity

为实现Up导航,第一步就是要声明哪个Activity是每个当前Activity的父Activity。这样做是为了让系统更好的在manifest文件中确认逻辑上的父Activity。
从Android 4.1(API 16)开始,你可以在元素中设置android:parentActivityName属性为Activity指定一个逻辑的父Activity。
如果你的App支持Android 4.0或更早的版本,需依赖Support Library,在Activity中添加元素,设置android.support.PARENT_ACTIVITY属性,这个和android:parentActivityName对应。
例如:

<application ... >
    ...
    <!-- The main/home activity (it has no parent activity) -->
    <activity
        android:name="com.example.myfirstapp.MainActivity" ...>
        ...
    </activity>
    <!-- A child of the main activity -->
    <activity
        android:name="com.example.myfirstapp.DisplayMessageActivity"
        android:label="@string/title_activity_display_message"
        android:parentActivityName="com.example.myfirstapp.MainActivity" >
        <!-- Parent activity meta-data to support 4.0 and lower -->
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.example.myfirstapp.MainActivity" />
    </activity>
</application>

这样声明之后,可以使用NavUtils的API导航到父Activity。NavUtils下面会讲到。

处理Up行为

调用setDisplayHomeAsUpEnabled(),使ActionBar上的icon有Up的导航功能:

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    getActionBar().setDisplayHomeAsUpEnabled(true);
}

上述代码会在ActionBar上的App icon左边添加一个左向的箭头,点击它会回调 onOptionsItemSelected(),其id是android.R.id.home。

导航到父Activity

你可以使用NavUtils的静态方法navigateUpFromSameTask()来实现。这个方法调用后,会启动(或Resume)一个父Activity,如果父Activity在同一个Back任务栈里的话,它将会回到前台。父Activity回到前台是否会调用onNewIntent()取决于:

  • 如果父Activity的启动模式是,或者在Intent中设置了FLAG_ACTIVITY_CLEAR_TOP的Flag,那么在父Activity回到前台时会调用onNewIntent()
  • 如果父Activity的启动模式是,而且没有设置FLAG_ACTIVITY_CLEAR_TOP的Flag,那么Up导航后会生成一个新的父Activity的实例。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    // Respond to the action bar's Up/Home button
    case android.R.id.home:
        NavUtils.navigateUpFromSameTask(this);
        return true;
    }
    return super.onOptionsItemSelected(item);
}

navigateUpFromSameTask()这个方法只适用你的App是当前任务栈的所有者(即当前TaskStack从你的App启动)。如果不是这种情况,而且你的App在别的App创建的Task中启动了,则Up导航需要为你的App创建一个新的属于你App的Back任务栈。

导航到一个新的Back栈

如果你的Activity在intent filters 中声明可以被其他的App调用,那么你需要实现onOptionsItemSelected()方法,这样的话,当用户在其他App的任务栈里点击了Up,则你的App会创建一个新的Back栈,然后Resume。
你可以使用NavUtils的shouldUpRecreateTask()方法判断当前Activity是否存在与另一个App的任务栈里。如果返回true,则使用TaskStackBuilder创建一个新的Task,否则可以使用上面描述的navigateUpFromSameTask()方法直接返回。

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    // Respond to the action bar's Up/Home button
    case android.R.id.home:
        Intent upIntent = NavUtils.getParentActivityIntent(this);
        if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
            // This activity is NOT part of this app's task, so create a new task
            // when navigating up, with a synthesized back stack.
            TaskStackBuilder.create(this)
                    // Add all of this activity's parents to the back stack
                    .addNextIntentWithParentStack(upIntent)
                    // Navigate up to the closest parent
                    .startActivities();
        } else {
            // This activity is part of this app's task, so simply
            // navigate up to the logical parent activity.
            NavUtils.navigateUpTo(this, upIntent);
        }
        return true;
    }
    return super.onOptionsItemSelected(item);
}

为了让addNextIntentWithParentStack()方法有效,需要在manifest中为Activity声明父Activity,使用android:parentActivityName (或 )。

例子代码下载:EffectiveNavigation

实现(四) - 提供Back导航

参考地址:http://developer.android.com/training/implementing-navigation/temporal.html
Back导航是让用户可以返回到之前访问过的界面,所有的Android设备都为这种导航提供了Back按钮,所以你的App的UI中不应该添加Back按钮
大部分情况下,当用户点击Back按钮系统都提供了默认的Back栈管理Activity,但一些情况下,允许你的App自定义Back按钮的行为给用户更好的体验。
导航模式中需要你手动指定的行为包括:

  • 当用户从 notification、app widget或navigation drawer直接进入App中比较深的Activity时
  • 用户在多个Fragment中导航
  • 在WebView中切换网页
    下面介绍如何最好的实现Back导航。

为深度访问Activity生成一个Back Stack


比如,你收到一个notification,点击进入到新闻详情界面,那么返回时,不应该退出应用,而是返回新闻列表。这时,你需要创建一个新的Back栈,因为之前新闻App没有创建任何Stack。

在Manifest中指定父Activity


类似于Up导航中的描述。直接查看

当启动Activity时启动一个Back栈


使用TaskStackBuilder将一个Activity定义到一个新的Stack中,使用startActivities()或使用getPendingIntent()创建合适的Intent来启动这个Activity。
例如,当一个notification让用户点击很深的Activity中,你可以使用下面这段代码创建一个PendingIntent来启动这个Activity并插入一个新的Stack到当前Task中。

// Intent for the activity to open when user selects the notification
Intent detailsIntent = new Intent(this, DetailsActivity.class);

// Use TaskStackBuilder to build the back stack and get the PendingIntent
PendingIntent pendingIntent =
        TaskStackBuilder.create(this)
                        // add all of DetailsActivity's parents to the stack,
                        // followed by DetailsActivity itself
                        .addNextIntentWithParentStack(upIntent)
                        .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentIntent(pendingIntent);
...

PendingIntent不仅指定要启动的Activity,一个Back Stack也插入到当前Task中(DetailsActivity所有的父Activity都被 detailsIntent定义好了)。所以当DetailsActivity启动,按Back按钮,可以导航到DetailsActivity 的各个父Activity中。

为了让addNextIntentWithParentStack()方法有效,需要在manifest中为Activity声明父Activity,使用android:parentActivityName (或 元素)

为Fragment实现Back导航

当你在App中使用Fragment时,每一个FragmentTransaction对象代表可能变化的加入到Stack中的Context。
比如说,你在手机上要通过置换Fragment实现一个“列表/详细”流,就应该确保在详细界面按下Back按钮会返回到列表界面。在提交transaction之前调用addToBackStack()

// Works with either the framework FragmentManager or the
// support package FragmentManager (getSupportFragmentManager).
getSupportFragmentManager().beginTransaction()
                           .add(detailFragment, "detail")
                           // Add this transaction to the back stack
                           .addToBackStack()
                           .commit();

当FragmentTransaction对象在Back栈中,用户按下Back按钮时,FragmentManager会从Back栈中取出最上面的transaction并执行反向操作(比如如果transaction要添加进去就删除Fragment)。

注意:在水平导航(比如切换tabs)或修改内容的外观时,你不应该向Back栈中添加transaction。
如果你的应用程序需要更新其他UI元素(比如ActionBar)来反映当前Fragment的状态的话,记住当commit transaction时要更新UI。除此之外,当Back栈变化后你也要更新UI。你可以在FragmentTransaction恢复后使用FragmentManager.OnBackStackChangedListener监听:

getSupportFragmentManager().addOnBackStackChangedListener(
        new FragmentManager.OnBackStackChangedListener() {
            public void onBackStackChanged() {
                // Update your UI here.
            }
        });

为WebView实现Back导航

如果你的应用中有webview,你需要为让Back按钮点击变成返回上一个网页。

@Override
public void onBackPressed() {
    if (mWebView.canGoBack()) {
        mWebView.goBack();
        return;
    }

    // Otherwise defer to system default behavior.
    super.onBackPressed();
}

在这种机制中使用会产生大量History的高度动态网页时要小心。因为他会让用户一直返回,很难跳出当前Activity。

实现(五) - 实现子代导航

原文参考:http://developer.android.com/training/implementing-navigation/descendant.html

子代导航通常使用startActivity()或者使用FragmentTransaction对象创建Fragment来实现。

在手机和平板上实现Master/Detail流

在手机上,使用startActivity()分别在两个Activity上是实现;在平板上,通常是左右结构,左边是Master,右边时Detail,然后使用FragmentTransaction添加、删除、置换Fragment实现。

导航到外部的Activities

我们经常需要打开外部的App,比如打开联系人,发送邮件,打开地图等等。我们通常不希望当用户从桌面重新启动我们的App时进入到别人的App之中,这样会让人感到很迷惑的。
为避免发生这样的事情,我们在Intent中添加FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET的Flag。如:

Intent externalActivityIntent = new Intent(Intent.ACTION_PICK);
externalActivityIntent.setType("image/*");
externalActivityIntent.addFlags(
        Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(externalActivityIntent);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值