手把手实战安卓动画(一)

编者按

多个安卓应用开发者Mariusz Brona通过一个系列的博客详细描述了自己开发新App Workcation的过程,原文请点击这里 这里

前言

几个月前再一次公司会议上, 我朋友Paweł Szymankiewicz给我看了一个他做的动画效果,我非常喜欢。会议之后我就决定把代码写出来,不过我当时并不知道我将面对什么...

开始吧!

正如我们从上面的GIF里看到的,好几件事情在发生:

  1. 点击了底部了菜单选项之后,需要变换到下一屏,我们可以看到地图从上部通过伸缩/渐隐的动画效果出现。RecyclerView的Item从下部被加载,地图上通过伸缩/渐隐动画效果出现标记。
  2. 滚动RecyclerView里面的item的时候地图上面的标记也会在闪动显示每个item代表的位置
  3. 点击了其中一个item之后会转场到下一屏,下方的地图会有动画效果产生起点/终点标记和之间的路线。RecyclerView的item会出现更多的描述,更大的图片,行程详细信息和一个按钮。
  4. 返回时,RecyclerView的item又会回到原来的状态,标记再一次出现,路线消失
    大概就是这样,我会在接下来的一系列博客中上述这些效果如何实现,今天我们先来讨论地图Fragment的入场动画。
问题定义

如上图的GIF,看起来地图早就加载好只是动画过渡到合适的位置,实际上不是这样的,实际上发生的是这样:

解决方法
  1. 预先加载地图
  2. 地图加载后,用Google Maps的API拿到它的位图并且放到缓存中
  3. 当进入DetailsFragment的时候对地图实现伸缩/渐隐效果
预先加载地图

达成这一点我们要先给已经加载好的地图生成一个位图的截图。当然了,如果我们想要屏幕见平滑的过渡,我们不能在DetailsFragment里面做这件事情。我们需要做的是在HomeFragment下拿到位图存进缓存。如你所见,地图的底部有一些边框,所以我们还需要让“之后”的地图尺寸合适。

如你在上面的代码片段中所见,MapFragment被放在剩下的布局的下面,这让我们可以加载地图时不被用户看见。

MainActivity继承MvpActivity,一个来自MosbyFramework(作者Hannes Dorfmann)的类。整个项目都遵循MVP方式,而Mosby Framework是个很优雅的实现。

onCreate方法中我们做三件事情:

  1. 我们给地图提供LatLngBounds -- 定义地图的边界
  2. 我们在activities container的布局中替换掉HomeFragment
  3. 我们在MapFragment中设置OnMapReadyCallback

地图就绪之后,onMapReady方法会被调用,我们可以在这里做一些操作把加载的地图保存到位图中。我们用CameraUpdateFactory.newLatLngBounds()方法把摄像头移动到之前提供的LatLngBounds上。我们确切的知道下一屏里地图的尺寸是多少,所以我们可以直接宽度(屏幕的宽度)和高度(屏幕的高度以及底部的边框)这两个参数传给这个方法。我们这样计算这两个参数:

很容易吧?在googleMap.moveCamera()方法被调用之后,我们就可以设置OnMapLoadedCallback了。当摄像投被移动到需要的位置,onMapLoaded方法会被调用我们就可以开始从它生成位图了。

生成位图并存入缓存

onMapLoaded()方法只有一项任务 -- 在截图完成后调用presenter.saveBitmap()。幸亏有lambda表达式,我们可以把样板代码简化到一行

presenter的代码非常简单,它只需要把位图放到缓存里。

我用了LruCache,因为这是推荐做法,这里 有详细描述。
把位图存到cache之后剩下的就是实现进入DetailsFragment时候自定义的缩放/渐隐动画效果了,小菜一碟。

地图的缩放/渐隐动画效果

最有趣的部分来了!代码其实很简单,不过我们能干很多事情。

在变换中我们通过讲scaleFactor(默认为8)把ImageView中的scalaXscaleY属性缩小到我们想要的视图尺寸。换句话说,我们先把宽度和高度发到scaleFactor倍数,然后在缩小到我们想要的尺寸。

创建自定义变换

为了创建自定义的变换,我们要继承Transition类。下一步是重写capturesStartValuescaptureEndValues。这里具体发生了什么呢?

Transition架构用Property Animation API实现视图的起始属性值和终止属性值之间的动画过渡。如果你对这块不熟悉的话,你应该读读 这篇文章 。像之前讲的一样,我们想缩小图片,所以startValue是我们的scaleFactor,而endValue是我们想要的scaleXscaleY-- 通常就是1。

如何传递这些值呢?就像之前说的,小菜一碟。我们把TransitionValues对象作为参数传给captureStartcaptureEnd这两个方法中。这个对象包含了对视图和地图的引用,在地图中你可以存储这些视图的值,也就是scaleXscaleY

拿到值之后,我们需要重写createAnimator()方法。这个方法中我们返回Animator(或者AnimatorSet)对象,用来产生视图值之间的变换效果。在我们的例子中我们返回AnimatorSet来产生视图的scalealpha值。同事我们我们要变换只对ImageView起作用,所以我们要检查TransitionValues中对视图的引用是不是ImageView的示例。

使用自定义变换

我们存好了位图,我们创建好了变换,现在是最后一步 -- 把变换运用到fragment中。我喜欢用静态工厂方法(static factory method)来创建fragments和activities。这种方法看起来很好,也帮助我们保持代码整洁。同样的方法也能帮我们程序化的把变换运用起来。

如我们所见代码非常简单。我们创建一个变换的示例,加上target并且也在target view的XML中加上transistionName属性。

接下来我们把transition通过setEnterTransition()方法传给fragment,这样就做好了,效果如下

最后附上示例项目的代码: