深入理解Android L新特性之 页面内容&共享元素过渡动画

0.662字数 2778阅读 12137

今天我们来聊聊Android L(5.0)引入的新特性:页面内容过渡动画和页面共享动画,这两个特性都是基于我们前面已经说过的Transition动画,如果你对Transition动画不太属性,请先看我前面的两篇文章。在5.0之前,我们从一个Activity A进入到另外一个Activity B时,如果需要设置一个动画,我们通常可以通过overridePendingTransition(int enterAnim, int exitAnim)来设置一个B的进入动画和A的退出动画,但是这个方式的缺点也很明显,我们设置的动画只能针对页面中所有元素。

在5.0以后为了提升Android在页面切换时更好的用户体验,页面间过渡动画应运而生,更加强大灵活的API为开发者提供了更多页面过渡的动画选择。本文的主要内容有:什么是内容过渡动画以及如何使用;什么是共享元素动画以及如何使用;两种动画的原理;最后说了两种共享元素常用场景的实现。

在开始所有内容之前,我首先要请大家树立一个基本原则:页面过渡动画是基于Transition动画来实现的!,文中的所有demo都可以从这里下载。ok,我们进入主题吧。

基本概念

为了简化描述,我们首先定义一些标记,在Activity A打开Activity B这个场景中:

  1. Activity A:我们称它为calling Activity,简称calling
  2. Activity B:我们称为called Activity,以后简称called
  3. calling->called:用来描述calling打开called
  4. called->>calling:用来描述called返回到calling

在页面过渡中,我们有两种动画可以设置:1)页面内容过渡动画,指定整个页面的变化(类似于之前的overridePendingTransition);2)共享元素过渡动画,我们可以指定calling中的一些View平滑过渡到called页面中,下面我们分开讨论这两个东西。

页面内容过渡动画

在内容过渡动画中,我们可以指定四种动画,分别为exit(离开) enter(进入) return(返回) reenter(重新进入),分别对应于下图中的四种状态:

AE4DCA1972E923DB60C7E070F68B2099.jpeg

可以看到 exitreenter作用在calling Activity上,enterreturn作用在called Activity上。

和以前一样,我们还是先来一个简单的例子,再次安利一下,本系列文章中的代码都可以从这里直接下载

有人跟我反馈说文章中的gif不知道从哪里开始的,因为它是一直在重复,额从这个例子开始,我会在页面开始加一个begin的黑色页面,同时右下角有时间轴

content_transiton1.gif

我们前面定义过,calling表示第一个页面,called表示第二个页面。

calling->called之后,calling执行了一个淡入淡出的动画,什么?不知道Fade!?,那你先去读一下我的这篇文章-什么是过渡动画。比如,我们在calling中可以直接这样指定:

Fade fade = new Fade();
getWindow().setExitTransition(fade);`  //指定退出动画

实际上,这里一共有四个动画:

  1. calling exit: 一个淡出效果,通过Fade实现
  2. called enter:一个从上往下的Slide效果
  3. called return:从左往右消失
  4. calling reenter:从底部往上出现

上面例子中,我们直接指定了四种动画效果。这四种效果是两两一对的:
exit对应reenter, enter对应return。如果我们只指定了exit或者enter,那么与这两个对应的另外一个动画将默认使用关联动画的反向执行。此外,还需要注意一点的是,上例中能够这么看清楚这四个动画的原因是我将动画叠加执行去掉了,你可以通过setAllowEnterTransitionOverlap改变这个设置。

**注意,为了启动页面过渡效果,calling需要将startActivity(intent)改成调用startActivity(Intent intent, Bundle options),后面的options可以通过ActivityOptions.makeSceneTransitionAnimation(this).toBundle()来获取;
called中我们需要将调用finish()改成调用finishAfterTransition()
**
我们前面说了,这个页面过渡基于Transition来做的,那Transition动画无非就是两点呗:1)记录动画前后的视图树的状态变化;2)定义两个视图树之间的过渡效果。第二条我们前面已经分析过了,现在来看看系统是如何来记录两个场景的不同状态的。
其实对于整个页面的进入和退出,系统采取的办法也很简单,以退出为例,在将calling真正隐藏之前,系统直接调用TransitionManager.beginDelayedTransition(decorView,exitTransition);,然后再调用decorView.setVisibility(INVISIBLE);,这样退出动画就自然执行了。
是不是很简单??

这里其实说明了如果我们自己定义一个过渡动画,那么必须要处理View的可见性变化,因为只有处理了可见性变化才能触发动画,如果不明白这里,可以去查看我前面的文章。

共享元素动画

怎么描述共享元素动画呢?所谓共享就是calling中的一个viewcalled中的一个view可以做一个平滑的过渡,下面动图中的红色的小圆圈就是一个共享元素,在实际应用中,大家可以看一下微信朋友圈图片打开的动画就是一个典型的共享元素动画。

share_transiton.gif

如果仔细的朋友可以看到,我们不仅仅将view在两个页面间共享,而且在共享之前,我们有颜色变化的动画,这里也涉及了四个动画,和之前内容过渡一一对应。
它们分别是

  1. calling share exit: 颜色由红色变成绿色
  2. called share enter:大小由小变大
  3. called share return:大小由大变小
  4. calling share reenter:颜色由绿色变成红色

我们来看一下如何来让两个页面之间的View做一个过渡:

  1. 在两个Activity的布局中为需要进行过渡变化的View设置android:transitionName,android:transitionName必须成对出现。
  2. startActivity时,我们通过startActivity(intent,ActivityOptions.makeSceneTransitionAnimation(this,view,transitionName).toBundle()); 关联两个Activity中间的View。

我在前面曾经让你想想看,我们startActivity的第二个参数的作用是啥,因为共享元素也是基于过渡动画来做的,而我们需要过渡的View又处在两个不同的Activity中,但是一个动画肯定不能在两个Activity中间无缝过渡,那系统是怎么做到的呢?
其实着仅仅是一个障眼法而已,比如上面例子中,我们将一个Viewcalling移动到了called,但是实际上这个动画仅仅是在called中执行,而我们startActivity第二个参数的Bundle实际上是将calling中的共享元素的相关信息传递给calledcalled在进入时,会先吧这些共享元素的信息取出来,然后在直接操作当前视图树中相关联的View的属性和calling中元素一样,记录一下状态,再恢复成called本来的布局,记录一下状态,然后就可以愉快地开始动画了,真相就是这个样子。
除了Activity,其实Android还提供了Fragment切换时进行页面过渡动画的相关API,和Android完全一致,这里就不去说明了。
Material Design引入了很多UI的新技能,其实本人不太喜欢专研UI方面的东西,但是有时候产品需求压下来,还是得调研,多了解一些新的知识总归是好的。
多谢大家关注这个Transition动画系列,到这里基本上就已经完全结束了,如果你觉得自己有一些收获,请点击底下的LIKE吧!

2016-08-07补充:

在分享元素实战应用中有两个比较重要的场景,官方文档对这个场景的解决方案并不明确,在这里补充一下。

补充一 更新共享元素对应关系

很多情况下,我们需要共享元素的页面满足下面的条件:
calling是一个列表页面,called是一个详情页,我们在列表中选择一项打开,然后会有一个元素共享过渡到详情页,而我们在详情页是可以左右滑动切换元素的,如下图所示:

share_transiton000.gif

这个系统自带的相册APP,可以看到我们进入大图预览时,点击的是图片4,然后我们左右滑动,切换图片之后返回列表页,这个时候过渡动画直接返回到了图片1,那这是怎么做到的呢?

其实这里无非在过渡动画返回时,需要告诉系统,我的绑定关系改变了,我们可以在calling设置一个SharedElementCallback,然后在回调的onMapSharedElements更新一下对应关系,大概的逻辑代码如下:

setExitSharedElementCallback(new SharedElementCallback() {
        @Override
        public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
            super.onMapSharedElements(names, sharedElements);      
            sharedElements.put("avator",getItemByPosition(curentPos));
        }
    });

我们在celled中左右滑动修改了列表Index之后,需要通过某种方式告知calling,你可以通过startActivityForResult()也可以通过EventBus之类的第三方库,但是需要注意的是,onMapSharedElementsexitreenter时都会触发!

延迟共享元素

想想这种场景吧,called中的共享元素在进入时,并没有到达最终的位置(比如这个数据需要从网络获取数据之后,才展示它真正的大小位置),此时如果过渡动画如果过早的执行,那么过渡动画获取的called中共享view的状态并不正确,所以我们有时候希望called中的共享元素完全layout完毕之后再执行,幸好API提供了支持。

calledonCreate中我们可以这样处理:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 延迟共享动画的执行
postponeEnterTransition();
}

然后在共享元素的最终布局确定后,你可以执行startPostponedEnterTransition来启动共享元素动画,我们一般可以通过下面的工具方法来启动延迟动画:

private void scheduleStartPostponedTransition(final View sharedElement) {
sharedElement.getViewTreeObserver().addOnPreDrawListener(
    new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            //启动动画       
           sharedElement.getViewTreeObserver().removeOnPreDrawListener(this);
            startPostponedEnterTransition();
            return true;
        }
    });
}

但是使用延迟共享动画有两点需要注意:
*1. 调用postponeEnterTransition必须要在适当的时候调用startPostponedEnterTransition,否则Activity的过渡就会卡到这里,不能执行下去

2.基于1,我们在这两个方法间实际上也不能间隔太久(前面说的等待网络返回只是举个例子,不要在实际上使用等待网络返回的共享元素),如果间隔太大,那页面的过渡会直到调用startPostponedEnterTransition才能进行页面切换!

推荐阅读更多精彩内容