六、片段与材质设计

本章演示了如何实现结合了丰富的视觉外观和动画过渡的片段,如谷歌的材质设计指南所述。

本章涵盖以下主题:

  • 材质设计
  • 将我们的应用程序转换为使用材质设计
  • 在片段过渡中加入运动

到本章结束时,我们将能够创建丰富的、视觉上吸引人的应用程序,这些应用程序利用片段来执行屏幕过渡,并根据谷歌的材质设计指南整合复杂的动画。

打造丰富的用户体验

正如我们所讨论的,片段使我们能够创建灵活、适应性强、支持多种导航选项的应用程序用户界面。这些行为是构建成功应用的关键功能方面。然而,在现代应用程序开发中,要想成功,应用程序必须不仅仅是功能性的。要想成功,应用程序还必须具有视觉吸引力和吸引力。

在这一章中,我们将通过创建一个应用程序来结束我们对片段的讨论,该应用程序建立在我们已经讨论过的片段的功能基础上,在从一个片段过渡到另一个片段时,也具有视觉吸引力并包含丰富的动画。我们将使用材质设计来做到这一点。

材质设计

材质设计是谷歌的一个设计指南,用于创建视觉上吸引人的应用程序,这些应用程序结合了非常丰富的图形外观和丰富的动画用户体验。这些材质设计指南并不是专门针对移动设备的,而是作为一套单一的想法来设计跨移动、基于网络和桌面应用程序的丰富和高度交互的用户体验。

材质设计的整体主题是一个复杂的主题,超出了本书的范围。在这一章中,我们将涉及一些材质设计的一般问题;然而,我们的重点是那些特定于安卓片段的材质设计的方面。

谷歌提供了几个在线资源,有助于了解更多关于材质设计的知识。对于高级别的材质设计,参考http://www.google.com/design/spec/material-design。要深入了解安卓特有的材质设计方面,请看http://developer.android.com/design/material

在我们开始将材质设计融入我们的应用程序之前,让我们先来看看材质设计的核心原则。

材质设计原则

材质设计的中心是在应用体验中融入物理世界的感觉。通过使用阴影和分层,应用程序体验具有深度和有序感。用户体验是高度图形化的,图像色彩鲜艳,注重视觉愉悦。动画和运动用于提供用户反馈和有意义的过渡。

这些原则结合在一起,为用户提供了丰富的体验,符合物理世界提供的秩序感,同时充分利用了计算机虚拟世界中可用的功能。

运动的作用

运动在材质设计体验中起着重要的作用,是大多数专门应用于片段编程的材质设计方面。当应用程序从一个屏幕过渡到另一个屏幕时,运动是提供引人入胜的体验的有效工具,并且有助于在一个屏幕上的项目与另一个屏幕上的项目之间创建关联。正如我们将在本章后面讨论的,片段类提供了在片段之间移动时创建运动感所必需的功能,甚至可以提供创建一个屏幕上的项目看起来移动到另一个屏幕上的效果的能力。

在我们将运动融入片段过渡之前,我们将创建一个符合材质设计的安卓图书应用版本。

将我们的应用程序转换为使用材质设计

在本章中,我们将使用我们在第 4 章中完成的安卓图书应用程序版本,使用片段交易。大家会记得,这是我们的应用程序版本,在一个片段上显示书籍列表,允许用户从该列表中选择一本书,然后在另一个片段上显示所选书籍的详细信息。为了刷新您的内存,应用程序会出现,如下图所示:

Converting our application to use material design

本章完成后,该应用程序的外观和行为将与材质设计保持一致,外观类似于以下截图:

Converting our application to use material design

这个更新后的版本的应用明显比之前的版本有更吸引人的外观。左侧列表中显示的每本书都有丰富的图形外观,并使用阴影来呈现放置在屏幕上方分层卡片上的外观。当用户选择一本书时,应用程序会切换到右侧的屏幕,显示该书的大图以及书名和描述。

在前面的截图中有一点不明显,那就是屏幕过渡是动画的。卡片向左滑动,图像和标题似乎从卡片移到细节屏幕上,描述从屏幕底部向上滑动。我发布了一个短视频,展示了 http://bit.ly/jimwfragments0601 T2 的转变。视频首先以全速显示过渡,然后以四分之一的速度显示,以使过渡的细节更加清晰可见。

处理不同安卓版本

作为安卓棒棒糖的一部分,安卓平台增加了对材质设计的原生支持,安卓棒棒糖是安卓 5.0 版本,API 级别为 21。通过安卓支持库,旧的安卓版本可以获得对材质设计的支持。本章中与片段相关的讨论适用于本机应用编程接口和安卓支持库;但是,本章的示例代码完全是使用本机 API 构建的。

本机应用编程接口或安卓支持库是否是您应用的正确选择取决于您发布应用的时间和您的特定用户群。原生支持材质设计的设备数量正在快速增长,在您阅读本章时可能已经占大多数。

有关安卓版本当前分布的信息,请访问http://developer.android.com/about/dashboards的安卓仪表盘。

我鼓励大家看一下安卓仪表盘,而不是依赖安卓 Studio 内显示的平台支持信息。根据我的经验,Android Studio 大大低估了对安卓新版本的支持水平。

设置主题

为了使我们的应用程序符合材质设计,我们需要给它一个材质设计主题。记住第一个原生支持材质设计的安卓版本是 API 21。为了创建资源文件,我们将从使用 Android Studio新资源文件对话框开始,创建一个名为styles的新值资源文件,目标为 API 21 及以上,如下图所示:

Setting up the theme

如果你创建了一个以 API 21 或更高版本为目标的项目,Android Studio 会包含一个名为styles的以 API 21 或更高版本为目标的值资源文件。

styles资源文件中,定义一个名为AppTheme的样式,该样式继承了名为Theme.Material.Light的内置主题,并设置四种基本主题颜色,如下 XML 所示:

<resources>
  <style name="AppTheme" parent="android:Theme.Material.Light">
    <item name="android:colorPrimary">#F44336</item>
    <item name="android:colorPrimaryDark">#B71C1C</item>
    <item name="android:colorAccent">#FF8A80</item>
    <item name="android:textColorPrimary">#FFFFFF</item>
  </style>
</resources>

仅仅通过继承材质设计主题,我们的应用程序就承担了许多材质设计的外观和行为。颜色值允许我们自定义应用程序的配色方案。

关于选择颜色的指南,请参考位于http://bit.ly/materialdesigncolor的谷歌材质设计风格指南。

受各颜色值影响的部分显示如下图所示:

Setting up the theme

关于给棒棒糖前的设备增加材质设计支持的信息,请看一下http://bit.ly/appcompatmateriald的谷歌博客文章。

更新片段外观

我们现在需要给我们的每个片段一个更丰富的外观。我们先来看看显示书单的片段:BookListFragment。我们将改变这个类,用更像卡片的外观展示每本书。为了简单起见,我们现在让片段只显示一张卡片。我们将在本章后面的保持多张卡片的连续性部分更新片段以显示多张卡片。

要创建卡片状布局,使用 Android Studio新资源文件对话框创建名为book_card_view.xml的新布局文件。book_card_view.xml文件的相关部分如下所示:

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  ... >
  <android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    card_view:cardCornerRadius="4dp"
    card_view:cardElevation="4dp"
    card_view:cardUseCompatPadding="true"
    ... >
  <RelativeLayout
    ... >
    <ImageView
      android:id="@+id/topImage"
      android:src="@drawable/db_programming_top_card"
      ... />
    <TextView
      android:id="@+id/bookTitle"
      android:layout_toEndOf="@+id/topImage"
      android:textColor="@android:color/black"
      android:text="@string/androidDbProgTitle"
      ... />
  </RelativeLayout>
  </android.support.v7.widget.CardView>
</LinearLayout>

布局文件使用安卓支持库 v7 中的CardView类来创建卡片般的外观。cardCornerRadius属性将卡角设置为略微圆形,cardElevation属性在卡周围创建一个小阴影,使卡的外观在屏幕上分层。将cardUseCompatPadding属性设置为true会使多张卡片之间的间距在安卓版本中表现一致。CardView位于垂直方向的LinearLayout内,包含ImageViewTextView视图,分别显示书籍图像和标题。

注意CardView类是安卓支持库 v7 的一部分。即使目标是本机支持材质设计的应用编程接口版本,也是如此。您可以通过在项目窗口中右键单击项目名称,选择打开模块设置,选择依赖项选项卡,然后单击 + ,将支持库添加到您的 Android Studio 项目中。

第 4 章处理片段事务中,BookListFragment类显示了一个简单的列表,因此扩展了ListFragment类。我们现在让BookListFragment类直接管理显示布局,从而扩展Fragment类,如下面的代码所示:

public class BookListFragment extends Fragment {
  private OnSelectedBookChangeListener mListener;
  @Override
  public View onCreateView(LayoutInflater inflater,
    ViewGroup container, Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.book_card_view, container, false);
    return rootView;
  }
  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);
    mListener = (OnSelectedBookChangeListener)activity;
  }
  // other members elided for clarity
}

BookListFragment类在这一点上很简单。onCreateView方法膨胀我们的book_card_view.xml布局资源。onAttach方法在mListener成员字段中存储对活动的引用,就像它在第 4 章处理片段事务中所做的那样。

当我们运行我们的程序时,活动创建并显示BookListFragment类,该类显示单个图书卡片,如下图截图所示:

Updating the fragments appearance

为了允许用户选择卡片和查看图书详细信息,我们将更新onCreateView方法为CardView添加点击处理程序,如下代码所示:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  View rootView = inflater.inflate(R.layout.book_card_view, container, false);
  rootView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      mListener.onSelectedBookChanged(0);
    }
  });
  return rootView;
}

膨胀布局资源返回顶层视图,即包含CardViewLinearLayout。然后我们将使用setOnClickListener方法来关联点击监听器,点击监听器使用mListener成员字段来通知活动用户已经选择了索引值为 0 的书籍。就像在第 4 章处理片段事务一样,活动将显示BookDescFragment,传入索引值为 0 的图书数据。

为了赋予BookDescFragment更丰富的外观,我们将对其进行更新,以显示图书图像、标题和描述。现在,当用户选择BookListFragment显示的卡片时,BookDescFragment出现,如下图所示:

Updating the fragments appearance

随着我们的应用程序更新为具有更丰富的外观,现在让我们看看如何添加从一个片段到另一个片段的动画过渡。

在片段过渡中加入运动

融入有意义的运动是材质设计的中心思想。作为开发人员,我们被鼓励使用 motion 来丰富用户体验,尤其是当用户从一个屏幕移动到下一个屏幕时。为了简化片段过渡中的运动合并,Fragment类包含了大大简化从一个片段到另一个片段的动画过渡的特征。

我们将在本章中介绍的片段转换特性可以在从 API 21 开始的原生片段类上获得,并且可以在安卓支持库 v4 中的片段类的早期安卓版本中获得。

让我们首先来看看添加一个简单的动作,将一个片段中的项目滑出屏幕,并将下一个片段的项目滑到屏幕上。

在屏幕上和屏幕外转换片段

自平台最初发布以来,安卓就支持动画视图。问题是,对于不擅长动画制作的开发人员来说,管理单个视图动画制作的细节可能非常复杂。为了简化动画视图的过程,安卓提供了过渡。转场保存关于要应用于一组视图的动画的信息。

过渡不是片段特有的。概括地说,过渡适用于称为场景的视图组。为了避免不必要地使我们的讨论复杂化,我们将特别关注片段上下文中的转换。

片段支持以下四种转换:

  • 退出:这是当前片段隐藏时和我们不弹出后栈时使用的过渡
  • 输入:这是初始显示当前片段时使用的过渡
  • 返回:这是当前片段由于弹出后栈而被隐藏时使用的过渡
  • 重新输入:这是当前片段作为弹出后栈的结果显示时使用的转换

为了更好地理解片段转换,让我们来看看应用程序中发生的转换。

当用户查看BookListFragment并点击卡片显示BookDescFragment时,会出现以下转换:

  • 退出转换运行BookListFragment
  • 进入转换运行BookDescFragment

当用户查看BookDescFragment并按下返回按钮返回BookListFragment时,会发生以下转换:

  • 返回转换运行BookDescFragment
  • 重新输入转换运行BookListFragment

正如我们的应用程序目前所写的,我们的片段的转换都设置为空。这样做的结果是,每当用户从一个片段移动到另一个片段时,第一个片段就消失了,而下一个片段出现了。使用过渡,我们可以通过结合运动来改善用户体验,让用户更好地感受从一个片段到另一个片段的移动。

在屏幕上切换图书卡

让我们更新我们的应用程序,这样当用户在BookListFragment上选择一张卡片时,BookListFragment上的视图从显示屏的左边缘滑出,而BookDescFragment的视图从显示屏的底部向上滑动。我们将使用Slide类来实现这一点。

我们将首先设置 BookListFragment的过渡。我们将在活动的onCreate方法中这样做,在这里我们将创建BookListFragment并将其添加到活动中,如以下代码所示:

protected void onCreate(Bundle savedInstanceState) {
  // code to call base class and load resources elided for clarity
  Slide slideLeftTransition = new Slide(Gravity.LEFT);
  slideLeftTransition.setDuration(500);
  BookListFragment listFragment = BookListFragment.newInstance();
  listFragment.setExitTransition(slideLeftTransition);
  FragmentManager fm = getFragmentManager();
  fm.beginTransaction()
  .add(R.id.layoutRoot, listFragment)
  .commit();
}

在调用基类实现并加载资源后,活动的onCreate方法创建一个Slide类的实例,传递一个LEFT的重力值。LEFT的重力值告诉Slide实例在隐藏视图时将视图滑出左边缘,在显示视图时将视图从左边缘滑入。新的Slide实例被分配给局部slideLeftTransition变量。然后,我们将使用setDuration方法来指示幻灯片动画应该运行500毫秒。

创建BookListFragment后,我们将使用setExitTransition方法将slideLeftTransition设置为BookListFragment隐藏时要执行的过渡。请注意,我们从不在BookListFragment实例上调用setReenterTransition。退出和重新进入转换被认为是互补的;因此,通过不设置重新进入过渡,安卓在重新进入片段时会自动使用slideLeftTransition。我们只需要在希望重新进入转换与退出转换行为不同时调用setReenterTransition

一旦我们创建了BookListFragment并设置了退出转换,我们就会像平常一样将BookListFragment添加到活动中。

在屏幕上和屏幕外切换书籍详细信息

现在,让我们为BookDescFragment设置过渡,以便从显示屏底部将视图滑入和滑出。我们将在活动的OnSelectedBookChangeListener.onSelectedBookChanged方法中进行操作,如下面的代码所示:

public void onSelectedBookChanged(int bookIndex) {
  Slide slideBottomTransition = new Slide(Gravity.BOTTOM);
  slideBottomTransition.setDuration(500);
  BookDescFragment bookDescFragment =
    BookDescFragment.newInstance(mTitles[bookIndex],
    mDescriptions[bookIndex], mImageResourceIds[bookIndex]);
  bookDescFragment.setEnterTransition(slideBottomTransition);
  bookDescFragment.setAllowEnterTransitionOverlap(false);
  FragmentManager fragmentManager = getFragmentManager();
  fragmentManager.beginTransaction()
                 .replace(R.id.layoutRoot, bookDescFragment)
                 .addToBackStack(null)
                 .commit();
}

BookDescFragment设置过渡与我们为BookListFragment所做的工作非常相似。我们将从创建一个Slide类的实例开始。当我们希望视图从显示屏底部滑入和滑出时,我们将使用BOTTOM的重力值。我们将把新的Slide实例分配给slideBottomTransition局部变量。

创建BookDescFragment后,我们将通过传递slideBottomTransition来调用setEnterTransition,以指示视图应该从显示屏底部滑入和滑出。我们不需要显式设置返回转换,因为进入和返回转换是互补的,就像退出和重新进入转换一样。设置进入转换后,我们将通过传递一个值false来调用setAllowEnterTransitionOverlap,这表明我们希望进入转换等待BookListFragment退出转换完成后再开始。如果不调用setAllowEnterTransitionOverlap,则BookDescFragment的视图将滑动到显示屏上,因为BookListFragment的视图仍在滑动关闭。最后,我们会像平常一样显示BookDescFragment

我们现在在应用中添加了Slide 过渡。当用户选择图书卡片时,卡片从屏幕的左边缘滑出,图书细节从底部向上滑动。当用户点击后退按钮时,细节从显示屏底部滑出,卡片从左侧滑入。你可以在http://bit.ly/jimwfragments0602看到动画的视频。

除了Slide过渡,隐藏和显示片段时使用的其他常见过渡是Fade,它会淡入淡出视图,以及Explode,它会导致视图从显示屏边缘飞入和飞出。

幻灯片运动的加入给我们的应用程序带来了更加丰富和专业的感觉,但我们还可以做更多的事情。现在让我们看看如何更进一步,并使用过渡在片段之间创建更大的连续性。

通过共享元素过渡创建连续性

在我们的应用程序中,BookListFragment提供了书籍摘要信息:图片和标题。当用户选择一本书的卡片时,BookDescFragment显示这本书的细节:图像、标题和描述。我们可以使用运动在两个片段之间创建更大的连续性,以给出图像和标题从摘要屏幕移动到细节屏幕的外观。这向用户强调了详细屏幕BookDescFragment上的信息与用户从概要屏幕BookListFragment中的选择相关联。共享元素转换给了我们这种能力。

当使用共享元素转换时,每个片段中的相关视图必须被赋予一个公共转换名。最简单的方法是在片段的布局资源中包含受影响视图的transitionName属性。

如果您喜欢以编程方式设置过渡名称,可以使用View.setTransitionName方法。我们将在本章后面的保持多张卡片之间的连续性部分查看一个以编程方式设置过渡名称的示例。

我们将首先更新的book_card_view.xml布局资源以包括transitionName属性,如以下 XML 中的所示:

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  ... >
  <android.support.v7.widget.CardView
    android:id="@+id/card_view"
    ... >
    <RelativeLayout
      ... >
      <ImageView
        android:id="@+id/topImage"
        android:transitionName="book_image"
        ... />
      <TextView
        android:id="@+id/bookTitle"
        android:transitionName="title_text"
        ... />
    </RelativeLayout>
  </android.support.v7.widget.CardView>
</LinearLayout>

ImageView元素现在包含一个值为book_imagetransitionName属性,TextView包含一个值为title_texttransitionName属性。您可以为transitionName使用任何您想要的值,只要每个片段中对应视图的值相同。考虑到这一点,我们将更新fragment_book_desc.xml布局资源,以包括transitionName属性,如下 XML 所示:

<ScrollView
  xmlns:android="http://schemas.android.com/apk/res/android"
  ... >
  <RelativeLayout
    ...>
    <ImageView
      android:id="@+id/topImage"
      android:transitionName="book_image"
      .../>
    <TextView
      android:id="@+id/bookTitle"
      android:transitionName="title_text"
      .../>
    <TextView
      android:id="@+id/bookDescription"
      ...>
  </RelativeLayout>
</ScrollView>

请注意,fragment_book_desc.xmlImageView元素的 transitionName属性与book_card_view.xmlImageView元素的transitionName属性具有相同的值,即book_image。同样地,fragment_book_desc.xml中第一个TextView元素的transitionName属性与 book_card_view.xml中的TextView元素具有相同的值,即title_text

在两个布局资源文件中,ImageViewTextView元素碰巧具有相同的各自的id属性值。这是作为一个好的程序设计来完成的,但是对于共享元素的转换来说并不是必需的。

除了具有共同过渡名称的视图之外,我们还需要对应于用户选择的ImageViewTextView的引用。为了允许我们访问ImageViewTextView元素,我们将更新我们的OnSelectedBookChangeListener界面,以接受对与用户选择相对应的视图的引用,如以下代码所示:

public interface OnSelectedBookChangeListener {
  void onSelectedBookChanged(View view, int bookIndex);
}

现在OnSelectedBookChangeListener界面接受了对所选视图的引用,我们在BookDescFragment onCreateView方法中设置的点击监听器可以被更新以传递所选视图,如下代码所示:

rootView.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    mListener.onSelectedBookChanged(v, 0);
  }
});

将更改为点击监听器后,该活动将收到对所选视图的引用,然后可以使用检索对所选ImageViewTextView元素的引用。

处理共享元素转换的剩余工作发生在活动的OnSelectedBookChangeListener.onSelectedBookChanged方法中,如以下代码所示实现:

public void onSelectedBookChanged(View view, int bookIndex) {
  Slide slideBottomTransition = new Slide(Gravity.BOTTOM);
  slideBottomTransition.setDuration(500);
  ImageView bookImageView = (ImageView)view.findViewById(R.id.topImage);
  TextView titleTextView = (TextView)view.findViewById(R.id.bookTitle);
  TransitionSet sharedTransitionSet = new TransitionSet();
  sharedTransitionSet.addTransition(new ChangeBounds())
                     .addTransition(new ChangeTransform())
                     .setDuration(500);
  BookDescFragment bookDescFragment =
    BookDescFragment.newInstance(mTitles[bookIndex],
    mDescriptions[bookIndex], mImageResourceIds[bookIndex]);
  bookDescFragment.setEnterTransition(slideBottomTransition);
  bookDescFragment.setAllowEnterTransitionOverlap(false);
  bookDescFragment.setSharedElementEnterTransition(
    sharedTransitionSet);
  FragmentManager fragmentManager = getFragmentManager();
  fragmentManager.beginTransaction()
                 .replace(R.id.layoutRoot, bookDescFragment)
                 .addSharedElement(bookImageView, "book_image")
                 .addSharedElement(titleTextView, "title_text")
                 .addToBackStack(null)
                 .commit();
}

onSelectedBookChanged方法从设置幻灯片切换开始,正如我们在本章前面的切换屏幕内外的书籍详细信息部分所讨论的。然后,该方法使用作为参数传入的View引用来获取对应于用户选择的ImageViewTextView元素的引用。

现在,我们需要设置一个过渡来动画化书籍的图像和标题,这实际上需要两个单独的过渡。我们需要一个ChangeBounds过渡来将图像和标题从它们在BookListFragment中的屏幕位置动画化到它们在BookDescFragment中各自的屏幕位置。我们还需要一个ChangeTransform过渡来将图像和标题从它们在BookListFragment内的显示尺寸动画化到它们在BookDescFragment内的相应尺寸。为了应用这两种转换,我们将创建一个TransitionSet实例。然后我们将使用addTransition 方法向TransitionSet实例添加ChangeBoundsChangeTransform实例。最后,我们将使用setDuration方法设置TransitionSet实例在500毫秒内执行。我们可以通过减少持续时间来加速过渡,或者通过增加持续时间来减缓过渡。使用TransitionSet,我们可以同时发生ChangeBoundsChangeTransform转换。

创建好过渡后,我们将创建BookDescFragment的新实例。使用setEnterTransitionsetAllowEnterTransitionOverlap方法,我们将设置非共享视图在BookListFragment退出转换完成后从显示屏底部滑入,就像我们之前在本章的转换屏幕上和屏幕外的书籍详细信息部分所做的那样。然后,我们将告诉BookDescFragment将我们的TransitionSet实例用于任何共享的过渡元素。

我们现在需要指出哪些视图包含在共享元素转换中。我们将通过传递ImageViewTextViewFragmentManager类的addSharedElement方法的引用以及它们相应的转换名称来实现这一点。这些与我们在布局资源中使用transitionName属性设置的过渡名称相同:book_image表示ImageViewtitle_text表示TextView。然后我们将对TextView进行同样的操作。除了调用addSharedElement方法,我们将显示BookDescFragment,就像我们一直在做的那样。

随着共享元素过渡的增加,图书图像和标题将会从卡片上移开,扩展到BookDescFragment内的位置。不属于共享元素过渡的图书描述将在共享的元素移动到位后从屏幕底部滑入。你可以在http://bit.ly/jimwfragments0603观看这一转变的视频。

保持多张卡的连续性

为了完成我们的应用程序,我们需要从在BookListFragment内显示单个图书卡转移到显示图书卡列表。要显示卡片列表,我们将使用安卓支持库 v7 中的RecyclerView

RecyclerView类类似于CardView类,是安卓支持库 v7 的一部分,即使目标是原生支持材质设计的 API 版本。

RecyclerView类提供了一种有效的方法来显示潜在的大数据集并定制它们的外观。从概念上来说,RecyclerView类的工作方式与ListView类非常相似。RecyclerView实例创建少量显示行,通常比屏幕上显示的多几行。当用户滚动通过RecyclerView实例时,RecyclerView实例回收已经滚离屏幕的行的视图,以显示现在正在屏幕上滚动的行的数据。

我们将使用我们在本章前面的更新片段外观部分中创建的book_card_view布局资源来定制RecyclerView中每行的外观。这样做有点复杂。要使共享元素转换工作,每个视图必须有一个唯一的转换名称;因此,我们不能依靠book_card_view布局资源内的transitionName属性来设置过渡名称。相反,我们需要为每本书动态设置过渡名称。

为了开始使用RecyclerView类,让我们创建一个新的布局资源fragment_book_list.xml,包含RecyclerView类,如下 XML 所示:

<android.support.v7.widget.RecyclerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/book_recycler_view"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

RecyclerView类占据了整个可用显示区域,其id值为book_recycler_view。我们现在可以更新BookListFragment来显示和填充RecyclerView类,如下面的代码所示:

public class BookListFragment extends Fragment {
  private String[] mTitles;
  private int[] mImageResourceIds;
  private RecyclerView mRecyclerView;
  private RecyclerView.Adapter mAdapter;
  private RecyclerView.LayoutManager mLayoutManager;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // base class and resources elided for clarity
    mAdapter = new BookAdapter(mTitles, mImageResourceIds);
  }
  @Override
  public View onCreateView(LayoutInflater inflater,
    ViewGroup container, Bundle savedInstanceState) {
    View rootView = inflater.inflate( R.layout.fragment_book_card, container, false);
    mRecyclerView =
      (RecyclerView)rootView.findViewById(R.id.book_recycler_view);
    mRecyclerView.setHasFixedSize(true);
    mLayoutManager = new LinearLayoutManager(getActivity());
    mRecyclerView.setLayoutManager(mLayoutManager);
    mRecyclerView.setAdapter(mAdapter);
    mRecyclerView.addOnItemTouchListener(
      new RecyclerItemClickListener(getActivity(),
      new RecyclerItemClickListener.OnItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
          mListener.onSelectedBookChanged(view, position);
        }
      }));
  return rootView;
  }
  // other members elided for clarity
}

BookListFragment类首先声明成员变量来保存书名和图像资源标识数组。然后,它声明成员变量来保存对RecyclerView类和与管理RecyclerView类相关的类的引用。

调用基类实现并加载图书相关数组后,onCreate方法创建BookAdapter类的实例,传入图书标题和图像资源 ID 数组。BookAdapter类处理在RecyclerView实例中显示图书列表的细节。我们稍后将讨论BookAdapter类的实现。

BookListFragment内的大部分工作发生在onCreateView方法中。我们将通过膨胀fragment_book_list资源、检索对所包含的RecyclerView实例的引用并使用值true调用setHasFixedSize方法来启动onCreateView方法。对setHasFixedSize方法的调用告诉RecyclerView实例,对数据所做的更改不会影响RecyclerView实例的显示大小,这允许RecyclerView实例更有效地执行动画。然后我们将创建一个LinearLayoutManager类的实例,并将其与RecyclerView实例相关联。RecyclerView类支持多种布局;LinearLayoutManager类提供了类似于ListView类的简单布局行为。最后,我们将把我们在onCreate方法中创建的适配器与RecyclerView实例相关联,并提供一个处理程序,当用户选择列表中的一本书时通知活动。

我们在onCreate方法中创建的BookAdapter类负责管理将每本书的数据与RecyclerView实例的适当显示行相关联的细节。BookAdapter类实现如下代码所示:

public class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder> {
  private String[] mTitles;
  private int[] mImageResourceIds;
  public BookAdapter(String[] titles, int[] imageResourceIds) {
    mTitles = titles;
    mImageResourceIds = imageResourceIds;
  }
  public int getItemCount() {
    return mTitles.length;
  }
  @Override
  public BookAdapter.ViewHolder onCreateViewHolder(
    ViewGroup parent, int viewType) {
    // implementation elided for clarity
  }
  @Override
  public void onBindViewHolder(ViewHolder holder, int position) {
    // implementation elided for clarity
  }
  public static class ViewHolder extends RecyclerView.ViewHolder {
    // implementation elided for clarity
  }
}

BookAdapter类继承自RecyclerView.Adapter类,并具有成员字段来存储对书名和图像资源标识数组的引用。BookAdapter构造函数只需将传递的图书标题和图像资源标识数组存储到这些成员字段中。getItemCount方法使用书名数组的长度返回包含的数据项的数量。

注意 BookAdapter类的基类RecyclerView.Adapter是在BookAdapter.ViewHolder类上模板化的。BookAdapter.ViewHolder类也是onCreateViewHolder方法的返回类型,是传递给onBindViewHolder方法的第一个参数的类型。BookAdapter.ViewHolder类是出现在BookAdapter类末尾的静态嵌套类。

由于ViewHolder类嵌套在BookAdapter类中,其全名为BookAdapter.ViewHolder。然而,在BookAdapter阶级的内部,它可以简单地称为ViewHolder

BookAdapter.ViewHolder类负责存储特定显示行的TextViewImageView引用。它的实现如下代码所示:

public static class ViewHolder extends RecyclerView.ViewHolder {
  public TextView mTextView;
  public ImageView mImageView;
  public ViewHolder(View v) {
    super(v);
    mTextView = (TextView)v.findViewById(R.id.bookTitle);
    mImageView = (ImageView)v.findViewById(R.id.topImage);
  }
}

BookAdapter.ViewHolder继承自RecyclerView.ViewHolder类。它的实现非常简单,只包含两个成员字段和一个构造函数。调用超类构造函数后,BookAdapter.ViewHolder构造函数只需使用传递的View参数找到该显示行的TextViewImageView实例,并分别将其存储在mTextViewmImageView成员字段中。

ViewHolder类的每个实例都是由BookAdapter类的onCreateViewHolder方法创建的,实现如下代码所示:

public BookAdapter.ViewHolder onCreateViewHolder(
  ViewGroup parent, int viewType) {
  View rootView = LayoutInflater.from(parent.getContext())
    .inflate(R.layout.book_card_view, parent, false);
  ViewHolder vh = new ViewHolder(rootView);
  return vh;
}

onCreateViewHolder方法开始于膨胀book_card_view布局资源并将返回的View引用存储在rootView局部变量中。然后它创建我们的ViewHolder类的一个实例,传入rootView变量。然后,ViewHolder类使用传递的rootView引用来访问该显示行的TextViewImageView实例。最后,onCreateViewHolder方法返回新的ViewHolder实例。

BookAdapter类的最后一点工作发生在onBindViewHolder方法中,该方法负责将一本书的标题和图像与特定显示行中的TextViewImageView实例相关联。在onBindViewHolder方法中,我们需要处理启用共享元素转换的细节。

onBindViewHolder方法实现如下代码所示:

public void onBindViewHolder(ViewHolder holder, int position) {
  holder.mTextView.setText(mTitles[position]);
  holder.mImageView.setImageResource(mImageResourceIds[position]);
  holder.mTextView.setTransitionName("title_text_" + position);
  holder.mImageView.setTransitionName("book_image_" + position);
}

onBindViewHolder方法接收对应于特定显示行的ViewHolder实例的引用和要显示的数据的位置。onBindViewHolder方法使用ViewHolder类的成员字段在请求的位置显示书籍的书名和图像。当用户做出选择时,这些TextViewImageView实例将被激活。

对于工作的共享元素转换,我们必须确保每个视图都有一个唯一的转换名称。我们不能依赖当前出现在book_card_view布局资源中的过渡名称,因为相同的过渡名称会在每一行重复出现。相反,我们将使用setTransitionName方法,通过将位置值连接到基本字符串值上,以编程方式将每个视图的转换名称设置为唯一的值。第一行数据,TextView实例的过渡名称为title_text_0,而ImageView实例的过渡名称为book_image_0;对于下一行,过渡名称分别为title_text_1book_image_1;等等。

为了保持转换名称的连续性,我们需要更新活动的onSelectedBookChangelistener方法来设置传递给片段事务的转换名称,以匹配所选卡内视图的名称,如以下代码所示:

fragmentManager.beginTransaction()
  .replace(R.id.layoutRoot, newFragment)
  .addSharedElement(bookImageView, "book_image_" + bookIndex)
  .addSharedElement(titleTextView, "title_text_" + bookIndex)
  .addToBackStack(null)
  .commit();

事务管理器通过简单地附加所选卡的位置值,将适当的转换名称与TextViewImageView实例相关联,就像我们在BookAdapter类中所做的那样。

我们需要做的最后一点工作是让BookDescFragment将其包含的ImageViewTextView实例设置为适当的过渡名称。为此,当我们在活动的onSelectedBookChangelistener方法中创建位置时,我们需要将该位置传递给BookDescFragment,如以下代码所示:

BookDescFragment bookDescFragment =
  BookDescFragment.newInstance(mTitles[bookIndex],
  mDescriptions[bookIndex], mImageResourceIds[bookIndex],
  position);

然后,我们可以使用setTransitionName方法在BookDescFragment.onCreateView方法中设置过渡名称,就像我们在BookAdapter.onBindViewHolder方法中所做的那样。

有了这个,我们的应用就完成了!我们的应用程序现在显示卡片内的书籍列表。当用户选择一张卡片时,应用程序会将卡片从屏幕的左边缘滑出,选定的标题和图像会从选定的卡片动画显示到我们的细节屏幕上,描述文本会从底部向上滑动。您可以在http://bit.ly/jimwfragments0601观看动画视频。

我们关注的是类中那些特定于我们应用程序的方面。关于RecyclerView的更一般的讨论,请看谷歌在http://bit.ly/recyclerlists的漫游。

总结

现代应用程序开发要求应用程序不仅仅是功能性的才能成功。应用程序必须支持市场上各种各样的安卓设备,具有视觉吸引力,并提供丰富的交互体验。在本书中,我们讨论了片段在满足这些需求中的重要作用。

片段允许我们创建模块化的用户界面组件,这些组件比单独使用活动的单一方法更具适应性,也更容易使用。片段是创建现代应用导航体验的关键要素,例如可滑动屏幕和导航抽屉。随着材质设计的出现,片段使我们能够结合运动来提供更吸引人的用户体验,从而提供更强的连续性。

利用您在本书中所学到的关于使用片段的知识,您将能够成功地提供用户所需的丰富、适应性强、引人入胜的应用体验。我们祝愿你在片段创意方面有一个成功的开端。