Android: Kotlin 材料设计入门

来源:互联网 发布:tplink访客网络设置 编辑:程序博客网 时间:2024/05/04 04:44

原文:Android: Kotlin 材料设计入门
作者:Joe Howard
译者:kmyhy
更新说明:本教程由 Joe Howard 升级为 Kotlin。原教程作者是 Megha Bambra。

谷歌的材料设计使 Android app 的可视化外观以一种令人兴奋的方式亮瞎了用户的眼睛。但稍安勿躁——什么是材料设计?

根据谷歌的描述,它“在创建漂亮直观的体验的同时融汇了触感的外观、醒目的图形设计以及流畅的动画”。材料设计是 Android app 的“用户体验哲学”。

在本教程中,你将把材料设计添加到一个叫做 Travel Wishlist 的 app 中。在这个过程中,你将学习:

  • 如何实现材料主题;
  • 用 RecyclerView 和 CardView 之类的 widget 构建动态视图;
  • 用 Palette API 生成颜色主题并用于文字和背景色;
  • 用 Android animation API 创建精彩的交互。

本教程假定你熟悉基本的 Android 编程,包括 Kotlin、XML、Android Studio 和 Gradle。如果你是一个新手,你可以先阅读我们的 Android 开发入门教程系列 和 Kotlin 入门。

要完成本教程,你必须使用 Android Studio 3.0 Beta 2 以上,以及 Kotlin 1.1.4-2 以上。

让我们开始吧!

开始

下载 开始项目,打开 Android Studio。

要导入这个开始项目,首先选择 Android Studio 欢迎界面中的 Open an existing Android Studio Project。

然后选择已经下载的项目,再点 Open:

Travel Wishlist 是一个非常简单的 app。用户会看到一个网格列出了世界各地的图片,可以通过点击某张图片来添加笔记,表示你想看的东西和想做的事情。

Build & run,你会看到一个只包含了基本界面的窗口:

现在,世界还是空的!你将在项目中加入一些材料组件,包括 dynamic view、color scheme 和 animations,它们会为你数据库中的图片增色不少!

打开 build.gradle 添加 RecyclerView、CardView、Paletter 和 Picasso 到依赖中:

dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"    implementation 'com.android.support:appcompat-v7:26.0.1'    implementation 'com.android.support:recyclerview-v7:26.0.1'    implementation 'com.android.support:cardview-v7:26.0.1'    implementation 'com.android.support:palette-v7:26.0.1'    implementation 'com.squareup.picasso:picasso:2.5.2'    testImplementation 'junit:junit:4.12'    androidTestImplementation 'com.android.support.test:runner:1.0.0'    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'}

这里,你直接声明了接下来要在教程中用到的依赖。一开头是 Google 提供的 API,但最后一个 Picasso 是由 Square 中的一群人提供的一个精彩的图片下载和缓存库。

声明完依赖,就可以将材料设计导入到 app 中了!

创建主题

在继续后面的内容之前,你需要修改主题。打开位于 res/values 目录下的 style.xml 。默认主题是 Theme.AppCompat.Light.DarkActionBar。在 theme 标签之内添加下列内容:

<item name="android:navigationBarColor">@color/primary_dark</item><item name="android:displayOptions">disableHome</item>

Android 自动将 colorPrimary 应用到 action bar,将 colorPrimaryDark 应用到状态栏,将 colorAccent 应用到 UI widget 比如 text field 和 checkbox。

在上面的代码中,你改变了 navigation bar 的颜色。对于 android:displayOptions,你设置为 disableHome 以适应屏幕布局。

Build & run,你会看到 app 变成了新的颜色主题。

这只是一个小小改变,就像在 Travel Wishlist 中的每次旅程,修改设计都是以一个简单的步骤开始。

使用 RecyclerView 和 CardView

为了给用户能够看到所有他们想去的地方,你需要一个视图。你可以用 RecyclerView 代替 ListView,它的功能要比后者强太多。Google 将 RecyclerView 称作“一个灵活的、能够为大数据及提供有限窗口的视图”。在这一节,你将看到如何在数据源相同(指定用户坐标)的情况下,将列表视图切换到一个自定义的网格视图。

用 XML 定义一个 RecyclerView

首先,打开 activity_main.xml,在 LinearLayout 标签内添加:

<android.support.v7.widget.RecyclerView  android:id="@+id/list"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:background="@color/light_gray"/>

这里,你添加了一个 RecyclerView 到 acitivity 中,让它占据父 view 的所有空间。

初始化 RecyclerView 并使用 LayoutManager

在编写 Kotlin 代码之前,需要配置 Android Studio 以便它自动插入 import 语句,节省你的时间。

找到菜单 Preferences\Editor\General\Auto Import 然后勾选 Add unambiguous imports on the fly 选项。在 MainActivity.kt 文件中,在类头部添加:

lateinit private var staggeredLayoutManager: StaggeredGridLayoutManager

简单声明了一个属性用于保存 LayoutManager。

然后,在 onCreate() 方法底下添加:

staggeredLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)list.layoutManager = staggeredLayoutManager

在上面的代码中,你将 RecyclerView 的布局管理器设置为一个 StaggeredGreidLayoutManager,这种管理器允许你创建两种方式排列的网格。这里,你使用第一种类型,然后将 span count 设置为 1,将方向设置为 StaggeredGridLayoutMananger.VERTICAL。span count 为 1 将显示列表而不是网格,等会你会看到。稍后,你会用两列来设置网格的紧凑格式。

注意你使用了 Kotlin 的 Android 扩展来找到表格,因此不需要调用 findViewById()。确认下列行在你的导入语句中存在,当你输入 list 的时候它应该是自动添加的:

import kotlinx.android.synthetic.main.activity_main.*

用 CardView 创建行和单元格

CardView 为你的视图提供了一种一致的背景,包括圆角和阴影。你将用它作为 RecyclerView 的行或单元格的布局。默认,CardView 继承了 FrameLayout,因此它能够包含其他子视图。

在 res\layout 目录中,新建一个布局文件,叫做 row_places.xml。点击 OK 新建。

为了创建你想要的单元格布局,将整个文件的代码提换为:

<?xml version="1.0" encoding="utf-8"?><android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"  xmlns:card_view="http://schemas.android.com/apk/res-auto"  android:id="@+id/placeCard"  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:layout_margin="8dp"  card_view:cardCornerRadius="@dimen/card_corner_radius"  card_view:cardElevation="@dimen/card_elevation">  <ImageView    android:id="@+id/placeImage"    android:layout_width="match_parent"    android:layout_height="200dp"    android:scaleType="centerCrop" />  <!-- Used for the ripple effect on touch -->  <LinearLayout    android:id="@+id/placeHolder"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="?android:selectableItemBackground"    android:orientation="horizontal" />  <LinearLayout    android:id="@+id/placeNameHolder"    android:layout_width="match_parent"    android:layout_height="45dp"    android:layout_gravity="bottom"    android:orientation="horizontal">    <TextView      android:id="@+id/placeName"      android:layout_width="match_parent"      android:layout_height="wrap_content"      android:layout_gravity="center_vertical"      android:gravity="start"      android:paddingStart="10dp"      android:paddingEnd="10dp"      android:textAppearance="?android:attr/textAppearanceLarge"      android:textColor="@android:color/white" />  </LinearLayout></android.support.v7.widget.CardView>

通过 xmlns:card_view=”http://schemas.android.com/apk/res-auto” 这一行,你可以分配像 card_view:cardCornerRadius 和 card_view:cardElevation 这样的属性,它们负责给 Android App 添加符合 card 外观的材料设计。

注意 placeHolder,你添加了一个 ?android:selectableItemBackground 作为它的背景。当用户点击单元格时,这会开启一个水波纹动画特效,就像许多 app 一样。待会你会看到。

为 RecyclerView 实现一个 Adapter

你将通过一个 adapter 将 RecyclerView 和数据绑定。在 main/java 文件夹,右击 com.raywenderlich.android.travelwishlist 包,选择 New\Kotline File/Class。类名就叫做 TravelListAdapter。

在类中添加代码,注意保留文件头部的 package 语句:

// 1class TravelListAdapter(private var context: Context) : RecyclerView.Adapter<TravelListAdapter.ViewHolder>() {  override fun getItemCount(): Int {  }  override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {  }  override fun onBindViewHolder(holder: ViewHolder?, position: Int) {  }  // 2  inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {      }}

上述代码分为几个步骤:

  1. 让 TravelListAdapter 继承 Recycler.Adapter,这样你就可以通过覆盖后面的方法来添加实现逻辑。你创建了一个构造函数,用一个 Context 作为参数,这个参数会在你在 MainAcitivity 创建一个 TravelListAdapter 的时候传给你,这个动作你会在后面来做。
  2. 创建了一个 ViewHolder 类。和 ListView 中使用 ViewHolder 是可选的不同,RecyclerView 是强制的。这种模式避免在每个 cell 上使用 findViewById(),从而改善了滚动和性能。

将 TravelListAdapter 中的 RecyclerView.Adapter 方法修改为:

// 1override fun getItemCount() = PlaceData.placeList().size// 2override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {  val itemView = LayoutInflater.from(parent.context).inflate(R.layout.row_places, parent, false)  return ViewHolder(itemView)}// 3override fun onBindViewHolder(holder: ViewHolder, position: Int) {  val place = PlaceData.placeList()[position]  holder.itemView.placeName.text = place.name  Picasso.with(context).load(place.getImageResourceId(context)).into(holder.itemView.placeImage)}

代码解释如下:

  1. getItemCount() 用于从你的数据数组中返回条目数。这里,你使用了 PlaceData.placeList()。
  2. onCreateViewHolder(…) 通过用 row_places 创建一个 inflated 的 view,然后用它得到并返回一个新的 ViewHolder 对象。
  3. onBindViewHolder(…) 将 Place 对象和 ViewHolder 中的 UI 元素绑定。你将用 Picassoso 来缓存列表中的图片。

在 MainActivity 中添加一个字段,用于保存你的 Adapter 对象:

lateinit private var adapter: TravelListAdapter

然后在 onCreate() 方法配置完 LayoutManager 之后创建一个 adapter 对象,将它传递给 RecyclerView:

adapter = TravelListAdapter(this)list.adapter = adapter

Build & run,你会看见列表中的地点渲染出来了。

那个地方在呼唤着你了?我喜欢天蓝色的水。但无论你想去哪儿,你都可以通过记录你想去做什么来描绘你的梦想。首先,你需要让单元格能够响应用户触摸。

实现每个单元格的点击事件

和 ListView 不同,RecyclerView 没有 onItemClick 接口,因此你必须在 adapter 中实现一个。在 TravelListAdapter 中,增加一个属性用于保存 OnItemClickListener 对象。在 TravelListAdapter 顶部添加代码:

lateinit var itemClickListener: OnItemClickListener

现在来实现 View.OnClickListener,以便为类内部的 ViewHolder 增加一个接口:

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {

然后在 ViewHolder 内部类中添加这个方法:

override fun onClick(view: View) {      }

在 ViewHolder 头部添加 init 方法将二者绑定:

init {  itemView.placeHolder.setOnClickListener(this)}

这里,你调用了 placeholder 的 setOnClickListener 并覆盖了 onClick 方法。

对于 RecyclerView 你必须在 onClick 方法中继续编写代码。首先,为内部类 ViewHolder 定义一个接口:

interface OnItemClickListener {  fun onItemClick(view: View, position: Int)}

然后,为 TravelListAdapter 添加 onClickListener 的 setter 方法:

fun setOnItemClickListener(itemClickListener: OnItemClickListener) {  this.itemClickListener = itemClickListener}

在内部类 ViewHolder 中实现 onClick() 的逻辑:

override fun onClick(view: View) = itemClickListener.onItemClick(itemView, adapterPosition)

在 MainActivity,在 onCreate() 之前创建一个 OnItemClickListener:

private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {  override fun onItemClick(view: View, position: Int) {    Toast.makeText(this@MainActivity, "Clicked " + position, Toast.LENGTH_SHORT).show()  }}

最后,在 onCreate() 方法最后,就在你设置 adapter 的后面,设置 adapter 的 listener :

adapter.setOnItemClickListener(onItemClickListener)

Build & run。

现在,你点击单元格,你会看到水波纹效果,然后显示一个 toast:

在列表和网格之间切换

StaggeredLayoutManager 为你的布局添加了灵活性。要将你现有的列表切换成更紧凑的两列式网格,你只需要在 MainActivity 中修改 StaggeredLayoutManager 的 spanCount。

在 toggle() 方法中,在 showGridView() 之前添加:

staggeredLayoutManager.spanCount = 2

然后在 showListView 顶部添加:

staggeredLayoutManager.spanCount = 1

你只是简单地将 spanCount 在 1 和 2 之间切换,它就会显示成单列或者双列了。

Build & run,用 action bar 按钮在列表和网格之间切换。

在列表中使用 Palette API

现在你可以添加一些有趣的材料设计特性了,首先从 Palette API 开始。回到 TravelListAdapter,你将在那里定义 placeNameHolder 的背景色,它将动态地使用图片中的颜色。

在 onBindViewHolder(…) 方法底部添加:

val photo = BitmapFactory.decodeResource(context.resources, place.getImageResourceId(context))Palette.from(photo).generate { palette ->  val bgColor = palette.getMutedColor(ContextCompat.getColor(context, android.R.color.black))  holder.itemView.placeNameHolder.setBackgroundColor(bgColor)}

generate(…) 方法创建一个用于背景的 palette,同时要传入一个 lambda 表达式用于当 palette 成功创建后调用。在这里,你可以拿到生成的 palette 并设置 holder.itemView.placeNameHolder 的背景色。如果颜色不存在,这个方法会使用一个替代色——这里也就是 android.R.color.black。

Build & run,看看实际的效果!

注意:Palette API 会从一张图片中获取如下颜色配置:

  • Vibrant
  • Dark Vibrant
  • Light Vibrant
  • Muted
  • Dark Muted
  • Light Muted

为建议你实际体验一下这些值。除了 palette.getMutedColor(…),你可以替换成 palette.getVibrantColor(…) 和 palette.getDarkVibrantColor(…)等。

使用新的材料 API

在这一节,你将用到 DetailActivity 和它对应的 activity_detail.xml,通过加入一些新的 材料设计 API 使它们变得更酷。

首先,你可能需要看一下开始项目中 detail view 当前的样子。要看到这个,首先需要在 DetailActivity 的 companion 对象中添加下列代码:

fun newIntent(context: Context, position: Int): Intent {  val intent = Intent(context, DetailActivity::class.java)  intent.putExtra(EXTRA_PARAM_ID, position)  return intent}

然后,打开 MainActivity,将位于 onItemClickListener 的 onItemClick(…) 方法中的 Toast 替换成:

startActivity(DetailActivity.newIntent(this@MainActivity, position))

你可以将 place 对象的位置通过 intent 进行传递,这样 DetailActivity 就能获得信息用于布局 UI。也就是你目前所做的。

Build & run。

这里没有任何让人惊喜的东西,但是一个很好的基础,在此基础上开始添加你极度期望的材料设计 API。你还会看到一个酷酷的 FloatingActionButton,这是材料设计中包含的一个 widget。

添加呈现动画

现在,你想让用户能够添加一些文字记录他们想去这些景色优美的地方做什么。因此,在 activity_detail.xml 中有一个隐藏的 edittext。当用户点击 FAB 按钮,它才会以一个漂亮的动画显示出来:

打开 DetailActivity。你需要实现两个方法:

  • revealEditText()
  • hideEditText()

首先,在 revealEditText() 方法中添加:

val cx = view.right - 30val cy = view.bottom - 60val finalRadius = Math.max(view.width, view.height)val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius.toFloat())view.visibility = View.VISIBLEisEditTextVisible = trueanim.start()

用两个 int 值在 view 的 x,y 坐标上加一些偏移。这会产生一种动画是从 FAB 方向开始的效果。

然后,用一个半径使动画呈现出环形效果,如上面的 gif 图中所示。所有这些值——x,y 和半径——都被传递给动画对象。动画对象用 ViewAnimationUtils 创建,它允许你创建一种环形呈现动画。

因为 EditText 一开始是隐藏的,你将 view 的 visibility 设置为 VISIBLE,将 isEditTextVisible 设置为 true。最后,你调用了动画对象的 start() 方法。

要解散视图,在 hideEditText() 中添加:

val cx = view.right - 30val cy = view.bottom - 60val initialRadius = view.widthval anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius.toFloat(), 0f)anim.addListener(object : AnimatorListenerAdapter() {  override fun onAnimationEnd(animation: Animator) {    super.onAnimationEnd(animation)    view.visibility = View.INVISIBLE  }})isEditTextVisible = falseanim.start()

你的目的是隐藏view 并从反方向展现环形动画。因此,你将初始半径设置为 view 的宽度,终止半径为 0,就会使圆变小了。

你先显示动画然后隐藏视图。为此,你实现了一个动画监听器,当动画结束就隐藏视图。

现在 build & run,看看动画效果!

注意:如果键盘弹出,你必须隐藏它才能看到完整的效果。将 DetailActivity 中的 inputManager.showSoftInput(…) 注释掉,待会别忘了将注释取消。呃,你的按钮没有显示 + 号图标,不用担心,待会你会解决这个问题。

在 FAB 按钮上进行贝塞尔变形

现在你已经完成了 edit text field 的显示动画和隐藏动画,你可以将 FAB 上的图标进行调整,使它看起来如下所示:

开始项目已经包含了 + 号和 √ 号的矢量路径。你将看到如何将 + 号动画或变形成 √ 号,以及相反动作。

在 res/drawables 目录,用 New\Drawable resource file 新建一个资源文件,命名为 icn_morph,然后将根元素定义为 animated-vector:

<?xml version="1.0" encoding="utf-8"?><animated-vector xmlns:android="http://schemas.android.com/apk/res/android"  android:drawable="@drawable/icn_add"></animated-vector>

animated-vector 需要有一个 android:drawable。这个 animated vector 将从 + 号开始变形成 √ 号,因此你需要将 drawable 设置为 icn_add。

现在来进行真正的变形,在 animated-vector 标签中添加:

<target  android:animation="@anim/path_morph"  android:name="sm_vertical_line" /><target  android:animation="@anim/path_morph_lg"  android:name="lg_vertical_line" /><target  android:animation="@anim/fade_out"  android:name="horizontal_line" />

这里,你实际上将 + 号的一竖变化成 √ 号,同时消除掉一横,如下图所示:

然后,将一竖分成两个路径,一段短一段长:

从上图中你会看到,你可以将前两个 target:sm_vertical_line 和 lg_vertical_line 的路径通过修改角度的方式转换成一个 √,也就是通过上述代码来进行的,同时隐藏 horizontal_line。

接下来,你还要逆转动画,将 √ 变回 +。新建一个 drawable resource 文件,叫做 icn_morph_reverse,编辑其内容为:

<?xml version="1.0" encoding="utf-8"?><animated-vector xmlns:android="http://schemas.android.com/apk/res/android"  android:drawable="@drawable/icn_add">  <target    android:animation="@anim/path_morph_reverse"    android:name="sm_vertical_line"/>  <target    android:animation="@anim/path_morph_lg_reverse"    android:name="lg_vertical_line" />  <target    android:animation="@anim/fade_in"    android:name="horizontal_line" /></animated-vector>

构成 + 号一竖的两段线段又变回了原来的状态,而一横重新显示,产生一个平滑效果。

现在,来完成这个动画。打开 DetailActivity.kt,在 onClick() 中 if 分支最后及 else 分支之前添加:

addButton.setImageResource(R.drawable.icn_morph)val animatable = addButton.drawable as Animatableanimatable.start()

这里,你将按钮的 image resource 设置成前面新建的 icn_morph,从 drawable 中抽取 animatable,然后播放动画。

然后,在 else 分支的底部添加:

addButton.setImageResource(R.drawable.icn_morph_reverse)val animatable = addButton.drawable as Animatableanimatable.start()

基本上和前面一样,只不过 image resource 变成了 icn_morph_reverse 而已,这样将播放反序动画。

除了图标的变化,当用户点击时也会将文本从 todoText 添加到 toDoAdapter,同时刷新地点活动列表。因为文字是白色的,所以你看不见,但你将在下一节为视图添加一个鲜艳的颜色让文字变得显眼。

Buid & run,观察眼前的变化。FAB 图标被点击时会在 + 和 √ 之间变化。

用 Palette API 添加动态颜色

是时候用 Palette API 给视图添加一些颜色了。不仅仅是和之前一样的颜色,而且会是动态的颜色!

在 DetailActivity 中,在 colorize() 添加:

val palette = Palette.from(photo).generate()applyPalette(palette)

就像你前面所做的,从一张照片中生成一个 palette——尽管这次是以同步方式——然后将 palette 传给 applyPalette。将 applyPalette() 中的代码替换为:

  private fun applyPalette(palette: Palette) {    window.setBackgroundDrawable(ColorDrawable(palette.getDarkMutedColor(defaultColor)))    placeNameHolder.setBackgroundColor(palette.getMutedColor(defaultColor))    revealView.setBackgroundColor(palette.getLightVibrantColor(defaultColor))  }

这里你使用了柔和的暗色调、柔和色调以及明亮的鲜艳色调分别作为 window、placeNameHolder 和 revealView 的背景色。

最后,闭合这个事件链,在 getPhoto() 方法最后添加:

colorize(photo)

再次 build & run!看看 detail activity 使用对应图片中的 palette 系统的配色方式。

用共享元素进行 Acitivity 动画转换

我们在 Google 的 app 中看到并赞叹不已的图片和文字动画都已经在材料设计中得到了改进。立马你就会学习这些平滑动画的细节。

注意:包含共享元素的 acitivity 动画,允许你的 app 在两个共享视图的 activity 之间进行动画。例如,你可以在一个 detail acitivity 上将列表上的缩略图转换到大图,同时保持内容的连续性。

在地名列表、MainActivity 和地名详情视图、DetailActivity 之间,你将对下列元素进行动画转换:

  • 地点的图片
  • 地点的名字
  • 名字后面的背景区域

打开 row_paces.xml ,在 id 为 placeImage 的 ImageView 标签中添加声明:

android:transitionName="tImage"

然后在 id 为 placeNameHolder 的 LinearLayout 标签中添加:

android:transitionName="tNameHolder"

注意 placeName 没有 transitionName。因为它是 placeNameHolder 的子元素,placeNameHolder 会对所有子视图进行动画转换。

在 activity_detail.xml 中,在 id 为 placeImage 的 ImageView 中添加一个 transitionName:

android:transitionName="tImage"

同样,在 placeNameHolder 的 LinearLayout 标签中添加一个 transitionName:

android:transitionName="tNameHolder"

你想转换的 activity 之间的共享元素应当用同样的 android:transitionName,也就是你在这里所设置的。另外,请注意这张图片的大小,以及 placeNameHolder 的高度比 activity 要大。你将在 activity 转换过程中以动画方式修改布局以提供一种看起来比较漂亮的连续效果。

将 MainActivity 中的 onItemClickListener() 方法修改为:

override fun onItemClick(view: View, position: Int) {  val intent = DetailActivity.newIntent(this@MainActivity, position)  // 1  val placeImage = view.findViewById<ImageView>(R.id.placeImage)  val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)  // 2  val imagePair = Pair.create(placeImage as View, "tImage")  val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")  // 3  val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,    imagePair, holderPair)  ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle())}

添加这段代码之后,你需要手动添加 import 语句,因为 Android Studio 无法自动导入这个包:

import android.support.v4.util.Pair

有几个地方需要注意:

  1. 通过在 RecyclerView 中的位置,获取 placeImage 和 placeNameHolder 对象。这里无法使用 Kotlin 的 Android 扩展,因为你是从某个具体 view 中获取 placeImage 和 placeNameHolder。
  2. 为 image 和 text holder view 创建一个 Pair,用于包含 view 和 transitionName。注意你不得不再次添加导入语句: android.support.v4.util.Pair。
  3. 要让 activity 用共享视图进行动画转换,需要将你的 Pair 对象和起始 activity 放到 options 中。

Build & run,看看从 MainActivity 切换到 DetailActivity 时的图片动画:

但是,有两个地方的动画有点跳跃:

  • FAB 按钮会楚然出现在 DetailActivity 上。
  • 如果你点击 action 或者 navigation bar 下面的行,这行会在动画之前跳一下。

首先解决 FAB 问题。打开 DetailActivity.kt,在 windowTransition() 方法中添加:

window.enterTransition.addListener(object : Transition.TransitionListener {  override fun onTransitionEnd(transition: Transition) {    addButton.animate().alpha(1.0f)    window.enterTransition.removeListener(this)  }  override fun onTransitionResume(transition: Transition) { }  override fun onTransitionPause(transition: Transition) { }  override fun onTransitionCancel(transition: Transition) { }  override fun onTransitionStart(transition: Transition) { }})

当窗口切换结束时,你添加到 enterTransition 中的 listener 被触发,你可以在这里渐入 FAB 按钮。为了实现这个效果,需要将 activity_detail.xml 中的 FAB 的 alpha 设置为 0:

android:alpha="0.0"

Build & run!你会看到 FAB 的切入动画更平滑了:

关于 action bar 和 navigation bar 的问题,首先要修改 styles.xml,将父主题设置为 Theme.AppCompat.Light.NoActionBar:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

因为在 styles.xml 中已经没有 action bar 了,你需要用另外一个 xml 来定义它。

打开 activity_main.xml 在 LinearLayout 添加下列代码,在 RecyclerView 标签前面:

<include layout="@layout/toolbar" />

这里直接在当前 layout 中包含了一个 toolbar layout,这是开始项目中已经提供的。现在你还要在 DetailActivity 的 layout 中做同样的事情。

打开 activity_detail.xml 在第一个 FrameLayout 的底部,在包含的 LinearLayout 结束标签之前添加:

<include layout="@layout/toolbar_detail"/>

然后在 MainActivity 中,初始化 toolbar。在 onCreate() 方法底部添加:

setUpActionBar()

这里,你将 findViewById 调用结果返回给新的 field,然后调用 setUpActionBar()。目前这只是一个空方法。接下来在 setUpActionBar() 中添加:

    setSupportActionBar(toolbar)    supportActionBar?.setDisplayHomeAsUpEnabled(false)    supportActionBar?.setDisplayShowTitleEnabled(true)    supportActionBar?.elevation = 7.0f

这里,将你的 toolbar 设置为 action bar,设置 title 为可见,禁用 home 按钮,然后通过 elevation 属性添加一小点下阴影。

Build & run。你会看到没什么改变,但这些修改已经打好了对 toolbar 进行动画转换的基础。

打开 MainActivity,将 onItemClickListener 修改为:

private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {  override fun onItemClick(view: View, position: Int) {    // 1    val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)    val placeImage = view.findViewById<ImageView>(R.id.placeImage)    val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)    // 2    val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)    val statusBar = findViewById<View>(android.R.id.statusBarBackground)    val imagePair = Pair.create(placeImage as View, "tImage")    val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")    // 3    val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)    val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)    val toolbarPair = Pair.create(toolbar as View, "tActionBar")    // 4    val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)    if (navigationBar != null && navPair != null) {      pairs += navPair    }    // 5    val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,      *pairs.toTypedArray())    ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())  }}

和原来的方法不同,这个方法是:

  1. 重命名了 intent 以便提供更多的含义;
  2. 引用了 navigation bar 和 status bar;
  3. 增加了 3 个 Pair 对象——一个用于 navigation bar,一个用于 status bar,一个用于 toolbar;
  4. 这里做了一个保护,防止在某些设备上出现 IllegalArgumentException 错误,比如 Galaxy Tab S2,在这个设备上 navPair 为 null。
  5. 修改要传递给新 activity 的 options,让它包含对新的视图的引用。你在将 pairs 转换为类型数组之后使用了展开操作符 *。

很好!Build & run,你会看到动画更平滑了:

现在如果你点击 action/toolbar 或者导航栏下面的行,它不会在切换之前抖动了;它会用剩余的共享元素进行动画转换,这让眼睛更加适应。切换到网格视图,你会看到转换动画仍然运行良好。

这里是 app 最终效果:视频

接下来做什么?

祝贺你:你编写了一个全面使用 Android 材料设计的 app!要挑战一下自己,请尝试如下内容:

  • 用 StaggeredLayoutMananger 实现 3 列的网格布局。
  • 在 MainActivity 和 DetailActivity 中用不同的 palette 选项来提样 Palette API。
  • 在地名列表中添加一个按钮,并把它作为共享元素的收藏按钮,跳转到 detail view。
  • 让转换动画效果更好——参考 Android 的 Newsstand app 是如何用呈现动画从一个网格转换到一个 detail view 的。要模拟这个效果的代码你都知道。
  • 尝试创建你在这里实现的所有变形动画,但这次用 animated-vector。

当然,请将这些技能用到你的 app 中,让它们和这个 app 一样棒!:]

要学习更多材料设计知识,请参考 Google 最新的 Google Design 网站。

你可以从这里下载完成后的项目。

请到下面或论坛中分享你的想法或者提问。