Material Design - TransitionManager

材料设计其实在上一篇图片和色彩之后,应该是已经结束了。但是后来又发现忘了动画,这是让材料设计焕发光彩的重点啊。所以又翻开了文档和 google,开始了检索和阅读。

谷歌在 api 19 中添加了 android.transition 这个包,用于优化安卓动画的体验。但是事实上,api 19 中 transition 中的类寥寥无几,大部分的类都是在 api 21 之后新添加的。那么有人会说了,现在的工程最少也要兼容到 api 19 吧?这样的类,并没有什么使用价值。

话糙理不糙,我还是先放上 github 上有几千 star 的兼容库链接:这是链接

这位前辈是真厉害,据说谷歌工程师解决部分 transition 包中 bug 的解决方案都是直接从这里来的。虽然有现成的库可以使用,但是原来的东西还是需要会用。所以来一起看看吧。

今天,主要看一下这里最基础的 TransitionManager


google 文档开头就提出了三个类,TransitionManager 、Transition、 Scene。
根据英文名称不难看出各个类的基本作用:

  • TransitionManager:动画的管理类,其中封装了 Transition 和 Scene
  • Scene:场景,它记录了 ViewTree 的某个时刻的关键帧,它通常作为动画的起始帧和最终帧使用
  • Transition:过渡,它代表了这个动画的过渡方式,包括渐变透明(Fade)、滑动(Slide) 等等

介绍就到这里,如果需要更多参考信息,可以移步 google 文档


一、TransitionManager API

去除参数,只看方法名一共有以下几种:

方法名 作用 备注
beginDelayedTransition 以当前帧为起始帧,直到下一次绘制后为结束帧,补齐中间的过渡动画 Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame.
endTransitions 结束所有过渡动画 Ends all pending and ongoing transitions on the specified scene root.
go 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 Convenience method to simply change to the given scene using the given transition.
setTransition 根据传入参数确认起始帧和结束帧,补齐中间的过渡动画 Sets a specific transition to occur when the given pair of scenes is exited/entered.
transitionTo 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 using the appropriate transition for this particular scene change (as specified to the TransitionManager, or the default if no such transition exists)

beginDelayedTransition 是一种特别方便,又好用的方法。

查看源码可以看到这个方法将会发生变化的 ViewGroup 缓存起来,并给它添加了 再次绘制的监听器:

public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
    if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
        //debug 模式日志
        if (Transition.DBG) {
            Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
                    sceneRoot + ", " + transition);
        }
        //rootView 缓存
        sPendingTransitions.add(sceneRoot);
        if (transition == null) {
            transition = sDefaultTransition;
        }
        //过渡方式
        final Transition transitionClone = transition.clone();
        //设置切换到当前场景的过渡
        sceneChangeSetup(sceneRoot, transitionClone);
        //设置 rootView 为当前场景
        Scene.setCurrentScene(sceneRoot, null);
        //添加绘制监听,在合适时机确认最终帧,并实现过渡
        sceneChangeRunTransition(sceneRoot, transitionClone);
    }
}

private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
        final Transition transition) {
    if (transition != null && sceneRoot != null) {
        //封装了 OnAttachStateChangeListener 和 OnPreDrawListener
        MultiListener listener = new MultiListener(transition, sceneRoot);
        sceneRoot.addOnAttachStateChangeListener(listener);
        sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
    }
}

beginDelayedTransition 可以在代码中直接应用过渡, View 变换大小后,它会在下一次绘制的时候执行过渡,从而使得这个过程不那么突兀。

TransitionManager.beginDelayedTransition(mRootView);
ViewGroup.LayoutParams layoutParams = mSquareView.getLayoutParams();
layoutParams.height = newSize;
layoutParams.width = newSize;
mSquareView.setLayoutParams(layoutParams);
GIF.gif

上面的例子是直接使用 LinearLayout 设置背景色获取的方块。当然普通 view 本身的大小变化也可以获得过渡效果,但是如果你使用的是自定义 View 可能你获得的过渡效果和想象的会有所不同。

这里我们用自定义 View 圆点视图为例,为了让效果更明显,我给这个 PointView 的容器添加了背景,获取效果为:

GIF.gif

产生这样的效果,是因为默认的 AutoTransition 是 Fade 和 ChangeBound 的组合,其中大小变化由 ChangeBound 完成,它能达到的效果只对 ViewGroup 生效。

TransitionManager 后面的方法都与 Scene 相关,看得出 Scene 也是个很重要的类。因此,接下来让我们了解一下 Scene 的用法。

三、Scene 场景

文档阅读 : 链接

我们通常把 Scene 译为场景,这个翻译其实还是挺好的。一个过渡动画其实就是从一个场景到另一个场景的过渡,这里的两个场景我们取名为 起始帧结束帧。而过渡动画,就是将场景中所有的子视图从起始帧移动到结束帧的运动效果。

1)创建 Scene 对象
根据文档,我们可以看到,一共有两种方式可以获取到我们需要的 Scene 对象。

//1.构造器
Scene(ViewGroup rootView)// 没有过渡信息,使用时需要自行添加过渡动画处理
Scene(ViewGroup rootView, View layout)//当这个场景进入时,会移除 rootView 中所有的子视图
//2.静态方法
getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)

其中对于构造器创建的 Scene 对象,需要注意的事项都已经备注在上面了,一般情况下,我们还是会使用静态方法获取。

2)一个例子
我们先来看一个例子:


起始帧
结束帧

这是我写的两个布局文件 scene1.xml 和 scene2.xml ,这个布局比较简单,所以也就不贴了,需要注意的是,这里只有一层 ViewGroup ,所有 ImageView 都在同一个层级下,且视图的 id 需要一一对应

按钮代码:

mRootView = ((ViewGroup) findViewById(R.id.activity_scene_rootview));
((RadioGroup) findViewById(R.id.activity_scene_radiogroup)).setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
        switch (checkedId) {
            case R.id.activity_scene_radiobtn1:
                Scene scene1 = Scene.getSceneForLayout(mRootView, R.layout.scene1, SceneActivity.this);
                TransitionManager.go(scene1);
                break;
            case R.id.activity_scene_radiobtn2:
                Scene scene2 = Scene.getSceneForLayout(mRootView, R.layout.scene2, SceneActivity.this);
                TransitionManager.go(scene2);
                break;
        }
    }
});

来看一下效果:


效果图

3)其他方法
在文档中,Scene 有 enter() 、exit() 、setEnterAction() 、 setExitAction()
enter 和 exit 方法就不多说了,在 TransitionManager 中,切换 Scene 的方法中也是调用了它们。关于后面两个方法,我没有找到合适使用他们的场景,但是文档说明中指出,这两个方法用于没有使用布局资源或层次结构定义的场景,或者在这些层次结构更改后需要执行附加步骤的场景。
有点抽象,之后如果有遇到合适的场景,再看吧。

四、Transition
效果图-g.png

上面是 google 文档的截图,可以看到 Transition 的子类非常丰富。实现不同接口的子类,组合出了多种多样的过渡效果。

1)介绍
Transition 类承载了切换到目标场景所有的动画效果,它的子类可以实现一组动画,也可以自定义实现动画。Transition 有两个核心任务:1.记录特定的属性;2.根据记录属性的变化执行过渡动画。

2)声明一个 TransitionSet 对象
TransitionSet 对象可以在 xml 文件中初始化,路径为:res/transition
例如我们创建一个带有 explode、changeBounds、changeTransform、changeClipBounds、changeImageTransform 的过渡动画。

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <explode/>
    <changeBounds/>
    <changeTransform/>
    <changeClipBounds/>
    <changeImageTransform/>
</transitionSet>

相应的,各个 Transition 有多种属性,这里就不再贴出,需要的时候可以浏览文档。

3)利用 TransitionManager 将动画应用到指定的 View 上

4)如果 explode、changeBounds 等自带的 Transition 并没有完成你需要的效果,那么你也可以用 transition 标签来声明:

<transition class="com.arno.CustomTransition"/>

CustomTransition 是一个自定义的动画。自定义动画和自定义 View 很像,它需要继承 Transition 类,并实现三个方法:

// 记录动画起始帧
public void captureStartValues(TransitionValues transitionValues)
// 记录动画终结帧
public void captureEndValues(TransitionValues transitionValues)
// 根据记录的起始帧和终结帧的属性,创建动画
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)

这里,我们简单实现一个改变布局高度的动画。
首先在captureStartValues方法中获取动画起始高度属性:

@Override
public void captureStartValues(TransitionValues transitionValues) {
    if (transitionValues == null) {
        return;
    }
    transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
}

然后在captureEndValues中获取动画结束高度属性:

@Override
public void captureEndValues(TransitionValues transitionValues) {
    if (transitionValues == null) {
        return;
    }
    transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
}

最后在createAnimator中创建动画:

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
    if (startValues == null || endValues == null) { return null;}

    final View endView = endValues.view;

    final int startHeight = (int) startValues.values.get(VIEW_HEIGHT);
    final int endHeight = (int) endValues.values.get(VIEW_HEIGHT);

    ValueAnimator sizeAnimator = ValueAnimator.ofInt(startHeight, endHeight);
    sizeAnimator.setDuration(500);
    sizeAnimator.setInterpolator(new LinearInterpolator());

    sizeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int current = (int) valueAnimator.getAnimatedValue();
            endView.getLayoutParams().height = current;
            endView.requestLayout();
        }
    });

    AnimatorSet set = new AnimatorSet();
    set.play(sizeAnimator);

    return set;
}
GIF.gif

之后会再看看把 Share Element 和 Transition 结合,做页面跳转动画。

以上。

感谢:
Material-Animations

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,321评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,001评论 5 13
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,612评论 4 59
  • 从创文明城市以来,大队领导身先士卒,包岗到人,每天早晚高峰亲自上岗和对各类非机动车违章进行査纠。 为了更...
    董班长阅读 613评论 2 1