六、片段与材质设计
本章演示了如何实现结合了丰富的视觉外观和动画过渡的片段,如谷歌的材质设计指南所述。
本章涵盖以下主题:
- 材质设计
- 将我们的应用程序转换为使用材质设计
- 在片段过渡中加入运动
到本章结束时,我们将能够创建丰富的、视觉上吸引人的应用程序,这些应用程序利用片段来执行屏幕过渡,并根据谷歌的材质设计指南整合复杂的动画。
打造丰富的用户体验
正如我们所讨论的,片段使我们能够创建灵活、适应性强、支持多种导航选项的应用程序用户界面。这些行为是构建成功应用的关键功能方面。然而,在现代应用程序开发中,要想成功,应用程序必须不仅仅是功能性的。要想成功,应用程序还必须具有视觉吸引力和吸引力。
在这一章中,我们将通过创建一个应用程序来结束我们对片段的讨论,该应用程序建立在我们已经讨论过的片段的功能基础上,在从一个片段过渡到另一个片段时,也具有视觉吸引力并包含丰富的动画。我们将使用材质设计来做到这一点。
材质设计
材质设计是谷歌的一个设计指南,用于创建视觉上吸引人的应用程序,这些应用程序结合了非常丰富的图形外观和丰富的动画用户体验。这些材质设计指南并不是专门针对移动设备的,而是作为一套单一的想法来设计跨移动、基于网络和桌面应用程序的丰富和高度交互的用户体验。
材质设计的整体主题是一个复杂的主题,超出了本书的范围。在这一章中,我们将涉及一些材质设计的一般问题;然而,我们的重点是那些特定于安卓片段的材质设计的方面。
注
谷歌提供了几个在线资源,有助于了解更多关于材质设计的知识。对于高级别的材质设计,参考http://www.google.com/design/spec/material-design。要深入了解安卓特有的材质设计方面,请看http://developer.android.com/design/material。
在我们开始将材质设计融入我们的应用程序之前,让我们先来看看材质设计的核心原则。
材质设计原则
材质设计的中心是在应用体验中融入物理世界的感觉。通过使用阴影和分层,应用程序体验具有深度和有序感。用户体验是高度图形化的,图像色彩鲜艳,注重视觉愉悦。动画和运动用于提供用户反馈和有意义的过渡。
这些原则结合在一起,为用户提供了丰富的体验,符合物理世界提供的秩序感,同时充分利用了计算机虚拟世界中可用的功能。
运动的作用
运动在材质设计体验中起着重要的作用,是大多数专门应用于片段编程的材质设计方面。当应用程序从一个屏幕过渡到另一个屏幕时,运动是提供引人入胜的体验的有效工具,并且有助于在一个屏幕上的项目与另一个屏幕上的项目之间创建关联。正如我们将在本章后面讨论的,片段类提供了在片段之间移动时创建运动感所必需的功能,甚至可以提供创建一个屏幕上的项目看起来移动到另一个屏幕上的效果的能力。
在我们将运动融入片段过渡之前,我们将创建一个符合材质设计的安卓图书应用版本。
将我们的应用程序转换为使用材质设计
在本章中,我们将使用我们在第 4 章中完成的安卓图书应用程序版本,使用片段交易。大家会记得,这是我们的应用程序版本,在一个片段上显示书籍列表,允许用户从该列表中选择一本书,然后在另一个片段上显示所选书籍的详细信息。为了刷新您的内存,应用程序会出现,如下图所示:
本章完成后,该应用程序的外观和行为将与材质设计保持一致,外观类似于以下截图:
这个更新后的版本的应用明显比之前的版本有更吸引人的外观。左侧列表中显示的每本书都有丰富的图形外观,并使用阴影来呈现放置在屏幕上方分层卡片上的外观。当用户选择一本书时,应用程序会切换到右侧的屏幕,显示该书的大图以及书名和描述。
在前面的截图中有一点不明显,那就是屏幕过渡是动画的。卡片向左滑动,图像和标题似乎从卡片移到细节屏幕上,描述从屏幕底部向上滑动。我发布了一个短视频,展示了 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 及以上,如下图所示:
注
如果你创建了一个以 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的谷歌材质设计风格指南。
受各颜色值影响的部分显示如下图所示:
注
关于给棒棒糖前的设备增加材质设计支持的信息,请看一下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
内,包含ImageView
和TextView
视图,分别显示书籍图像和标题。
注
注意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
类,该类显示单个图书卡片,如下图截图所示:
为了允许用户选择卡片和查看图书详细信息,我们将更新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;
}
膨胀布局资源返回顶层视图,即包含CardView
的LinearLayout
。然后我们将使用setOnClickListener
方法来关联点击监听器,点击监听器使用mListener
成员字段来通知活动用户已经选择了索引值为 0 的书籍。就像在第 4 章、处理片段事务一样,活动将显示BookDescFragment
,传入索引值为 0 的图书数据。
为了赋予BookDescFragment
更丰富的外观,我们将对其进行更新,以显示图书图像、标题和描述。现在,当用户选择BookListFragment
显示的卡片时,BookDescFragment
出现,如下图所示:
随着我们的应用程序更新为具有更丰富的外观,现在让我们看看如何添加从一个片段到另一个片段的动画过渡。
在片段过渡中加入运动
融入有意义的运动是材质设计的中心思想。作为开发人员,我们被鼓励使用 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_image
的transitionName
属性,TextView
包含一个值为title_text
的transitionName
属性。您可以为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.xml
中ImageView
元素的 transitionName
属性与book_card_view.xml
中ImageView
元素的transitionName
属性具有相同的值,即book_image
。同样地,fragment_book_desc.xml
中第一个TextView
元素的transitionName
属性与 book_card_view.xml
中的TextView
元素具有相同的值,即title_text
。
注
在两个布局资源文件中,ImageView
和TextView
元素碰巧具有相同的各自的id
属性值。这是作为一个好的程序设计来完成的,但是对于共享元素的转换来说并不是必需的。
除了具有共同过渡名称的视图之外,我们还需要对应于用户选择的ImageView
和TextView
的引用。为了允许我们访问ImageView
和TextView
元素,我们将更新我们的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);
}
});
将更改为点击监听器后,该活动将收到对所选视图的引用,然后可以使用检索对所选ImageView
和TextView
元素的引用。
处理共享元素转换的剩余工作发生在活动的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
引用来获取对应于用户选择的ImageView
和TextView
元素的引用。
现在,我们需要设置一个过渡来动画化书籍的图像和标题,这实际上需要两个单独的过渡。我们需要一个ChangeBounds
过渡来将图像和标题从它们在BookListFragment
中的屏幕位置动画化到它们在BookDescFragment
中各自的屏幕位置。我们还需要一个ChangeTransform
过渡来将图像和标题从它们在BookListFragment
内的显示尺寸动画化到它们在BookDescFragment
内的相应尺寸。为了应用这两种转换,我们将创建一个TransitionSet
实例。然后我们将使用addTransition
方法向TransitionSet
实例添加ChangeBounds
和ChangeTransform
实例。最后,我们将使用setDuration
方法设置TransitionSet
实例在500
毫秒内执行。我们可以通过减少持续时间来加速过渡,或者通过增加持续时间来减缓过渡。使用TransitionSet
,我们可以同时发生ChangeBounds
和ChangeTransform
转换。
创建好过渡后,我们将创建BookDescFragment
的新实例。使用setEnterTransition
和setAllowEnterTransitionOverlap
方法,我们将设置非共享视图在BookListFragment
退出转换完成后从显示屏底部滑入,就像我们之前在本章的转换屏幕上和屏幕外的书籍详细信息部分所做的那样。然后,我们将告诉BookDescFragment
将我们的TransitionSet
实例用于任何共享的过渡元素。
我们现在需要指出哪些视图包含在共享元素转换中。我们将通过传递ImageView
和TextView
对FragmentManager
类的addSharedElement
方法的引用以及它们相应的转换名称来实现这一点。这些与我们在布局资源中使用transitionName
属性设置的过渡名称相同:book_image
表示ImageView
,title_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
类负责存储特定显示行的TextView
和ImageView
引用。它的实现如下代码所示:
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
参数找到该显示行的TextView
和ImageView
实例,并分别将其存储在mTextView
和mImageView
成员字段中。
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
引用来访问该显示行的TextView
和ImageView
实例。最后,onCreateViewHolder
方法返回新的ViewHolder
实例。
BookAdapter
类的最后一点工作发生在onBindViewHolder
方法中,该方法负责将一本书的标题和图像与特定显示行中的TextView
和ImageView
实例相关联。在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
类的成员字段在请求的位置显示书籍的书名和图像。当用户做出选择时,这些TextView
和ImageView
实例将被激活。
对于工作的共享元素转换,我们必须确保每个视图都有一个唯一的转换名称。我们不能依赖当前出现在book_card_view
布局资源中的过渡名称,因为相同的过渡名称会在每一行重复出现。相反,我们将使用setTransitionName
方法,通过将位置值连接到基本字符串值上,以编程方式将每个视图的转换名称设置为唯一的值。第一行数据,TextView
实例的过渡名称为title_text_0
,而ImageView
实例的过渡名称为book_image_0
;对于下一行,过渡名称分别为title_text_1
和book_image_1
;等等。
为了保持转换名称的连续性,我们需要更新活动的onSelectedBookChangelistener
方法来设置传递给片段事务的转换名称,以匹配所选卡内视图的名称,如以下代码所示:
fragmentManager.beginTransaction()
.replace(R.id.layoutRoot, newFragment)
.addSharedElement(bookImageView, "book_image_" + bookIndex)
.addSharedElement(titleTextView, "title_text_" + bookIndex)
.addToBackStack(null)
.commit();
事务管理器通过简单地附加所选卡的位置值,将适当的转换名称与TextView
和ImageView
实例相关联,就像我们在BookAdapter
类中所做的那样。
我们需要做的最后一点工作是让BookDescFragment
将其包含的ImageView
和TextView
实例设置为适当的过渡名称。为此,当我们在活动的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的漫游。
总结
现代应用程序开发要求应用程序不仅仅是功能性的才能成功。应用程序必须支持市场上各种各样的安卓设备,具有视觉吸引力,并提供丰富的交互体验。在本书中,我们讨论了片段在满足这些需求中的重要作用。
片段允许我们创建模块化的用户界面组件,这些组件比单独使用活动的单一方法更具适应性,也更容易使用。片段是创建现代应用导航体验的关键要素,例如可滑动屏幕和导航抽屉。随着材质设计的出现,片段使我们能够结合运动来提供更吸引人的用户体验,从而提供更强的连续性。
利用您在本书中所学到的关于使用片段的知识,您将能够成功地提供用户所需的丰富、适应性强、引人入胜的应用体验。我们祝愿你在片段创意方面有一个成功的开端。
版权属于:月萌API www.moonapi.com,转载请注明出处