动画体系知识梳理(1) - 转场动画 ContentTransition 理论篇

一、概述

Android 5.0当中,Google基于Android 4.4中的Transition框架引入了转场动画,设计转场动画的目的,在于让Activity之间或者Fragment之间的切换更加自然,其根本原因在于界面间切换时的动画不再是以Activity或者Fragment的整个布局作为切换时动画的执行单元,而是将动画的执行单元细分到了View。目前提供的转场动画分为两种:

  • Content Transition:用于两个界面之间非共享的View
  • Shared Element Transition:用于两个界面之间需要共享的View

二、什么是Transition

2.1 Transition的基本概念

在学习Content Transition之前,我们先对转场动画所依赖的Transition框架做一个简要的介绍,这个框架是围绕着两个概念Scene(场景)和Transition(变换)来建立的,在后面我们会多次提到它:

  • 场景(Scene):表示UI所对应的状态,一般来说,会有两个场景:起点场景和终点场景,在这两个场景当中,UI有可能会有不同的状态。在上图当中,SceneASceneB就是两个不同的场景,ViewA在两个场景中对应的状态分别为VISIBLEINVISIBLE
  • 变换(Transition):用来定义两个场景之间切换的规则,当场景发生发生变换时,Transition需要做的有两点:
  • 确定View在起点场景和终点场景的状态。
  • 创建View从终点场景切换到终点场景所需的Animator

2.2 Transition的简单例子

下面,我们通过一个简单的例子,对上面的概念有一个直观的感受:

public class ExampleActivity extends Activity implements View.OnClickListener {
    private ViewGroup mRootView;
    private View mRedBox, mGreenBox, mBlueBox, mBlackBox;

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

        mRootView = (ViewGroup) findViewById(R.id.layout_root_view);
        mRootView.setOnClickListener(this);

        mRedBox = findViewById(R.id.red_box);
        mGreenBox = findViewById(R.id.green_box);
        mBlueBox = findViewById(R.id.blue_box);
        mBlackBox = findViewById(R.id.black_box);
    }

    @Override
    public void onClick(View v) {
        TransitionManager.beginDelayedTransition(mRootView, new Fade());
        toggleVisibility(mRedBox, mGreenBox, mBlueBox, mBlackBox);
    }

    private static void toggleVisibility(View... views) {
        for (View view : views) {
            boolean isVisible = view.getVisibility() == View.VISIBLE;
            view.setVisibility(isVisible ? View.INVISIBLE : View.VISIBLE);
        }
    }
}
  • 第一步:通过beginDelayedTranstion传入场景对应布局的根节点(mRootView)以及场景变换的规则(Fade),此时系统理解调用TransitioncaptureStartValues方法,来确定场景当中所有子Viewvisibility
  • 第二步:当beginDeleyedTransition返回后,我们将子View设置为不可见。
  • 第三步:在下一帧,系统调用TranstioncaptureEndValues()方法获取场景当中所有子View的可见性。
  • 第四步:这时候系统发现在起始场景中ViewVISIBLE的,而在终点场景中它变为了INVISIBLE,那么Fade Transition就会根据这些信息创建并返回AnimatorSet,用它来将那些发生变化的Viewalpha值渐变为0,而不是直接设为不可见。
  • 第五步:系统启动这个Animator,使得这些View慢慢隐藏。

2.3 Transition小结

我们可以总结出Transition的两个特点:

  • Animator对于开发者而言是抽象的,开发者设置View的起始值和最终值,Transition会根据这两者的差异,自动地创建切换的Animator
  • 可以随时通过替换Transition来改变切换的规则。

三、Content Transition基本概念

3.1 旧的界面切换动画

回忆一下,在5.0之前:

  • Activity之间的切换添加动画,在启动Activity的地方加上overridePendingTransition
  • Fragment之间的切换添加动画,通过FragmentTransationsetCustomAnimation

这两种方式都有一个共同的特点,那就是它们都是将Activity所在的窗口或Fragment所对应的布局作为切换动画的执行单元

3.2 新的界面切换动画

在新的切换方式当中,可以将布局中的每个View作为切换的执行单元,我们以Activity之间的切换为例。

3.2.1 启动BActivity

AActivity启动中BActivity,这时候就会涉及到四种Scene和两种Transition

  • AActivity's Exit Transition:它定义了AActivity中的元素如何从VISIBLE(起点场景)变为INVISIBLE(终点场景)。
  • BActivity's Enter Transition:它定义了BActivity中的元素如果从INVISIBLE(起点场景)变为VISIBLE(终点场景)。

3.2.1.1 确定需要执行TransitionView

整个Transition的第一步,就是先要确定当前界面中需要执行Transition的动画切换单元,这一过程是通过对整个View树进行递归调用得到的,而递归的逻辑在ViewGroup当中:

public void captureTransitioningViews(List<View> transitioningViews) {
        if (getVisibility() != View.VISIBLE) {
            return;
        }
        if (isTransitionGroup()) {
            transitioningViews.add(this);
        } else {
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                child.captureTransitioningViews(transitioningViews);
            }
        }
}

而在View中,该方法为:

public void captureTransitioningViews(List<View> transitioningViews) {
        if (getVisibility() == View.VISIBLE) {
            transitioningViews.add(this);
        }
}

由此可见,所有需要变换的ViewGroup/View都保存在transitioningViews当中,关于这个集合的构成依据以下三点:

  • 节点不可见,那么它以及它的所有子节点都不加入集合。
  • 节点的isTransitionGroup()标志位为true,那么把它和它的所有子节点当成一个变换单元加入到集合当中。
  • 除了以上两种情况,那么View树的所有叶子节点都加入到集合当中。

其中isTransitionGroup()的值我们可以通过setTransitionGroup(boolean flag)来改变,如果在场景当中用到了WebView,而我们希望将它作为一个整体进行变换,那么应当加上这个标志位。
除了系统默认的遍历,我们还可以通过Transitionaddedexcluded来改变这个集合。

3.2.1.2 Exit Transition的执行过程

下面,我们以AActivityExit Transition为例,描述一下它整个的执行过程:

  • 第一步:系统遍历AActivityView树,并决定在exit transition运行时需要变换的View,把它们放在集合当中,也就是我们在3.2.1.1中所说的transitionViews
  • 第二步:AActivityExit Transition获取集合中View的起始状态,调用的是captureStartValues方法。
  • 第三步:将集合中的View设为INVISIBLE
  • 第四步:在下一帧时,Exit Transition获取集合中View的终点状态,调用的是captureEndValues方法。
  • 第五步:Exit Transition根据第二步中的起始状态和终点状态,创建一个Animator,并执行这个Animator,由于是从VISIBLE变为INVISIBLE,因此,是通过onDisappear方法得到Animator

3.2.1.3 Enter Transition的执行过程。

BActivityEnter TransitionAActvityExit Transition类似,只不过第三步操作是从INVISIBLEVISIBLE

3.2.2 从BActivity返回

而当我们从BActivity返回到AActivity,那么就会涉及到下面四种Scene和两种Transition

  • BActivity's Return Transition
  • AActivity's Reenter Transition

其原理和上面是相同的,就不多介绍了。

3.2.3 小结

无论是AActivity启动BActivity,还是BActivity返回到AActivity,当View的可见性不断切换的时候,系统能保证根据状态信息来创建所需的动画。很显然,所有的Content transition对象都需要能够捕捉并记录View的起始状态和终点状态,幸运的是,抽象类Visiblity已经帮我们做了,我们只需要实现onAppearonDisappear方法,在里面创建一个Animator来定义进入和退出场景的View的动画,系统默认提供了三种Transition - Fade、Slide、Explode,下面我们在分析Fade源码的时候,会详细解释这一过程。

3.3 Content TransitionShared Element Transition

在上面的讨论当中,我们是从切换的角度来考虑的,而如果我们从Transition的角度来看,那么每个Transition又可以细分为两类:

  • content transitions:定义了Activity非共享View进入和退出场景的方式。
  • shared element transitions:定义了Acitivity共享View进入和退出场景的方法。

3.4 例子

下面,我们以一个视频来解释一下上面谈到的四个Transition


在这个视频当中,我们将列表页称为AActivity,详情页称为BActivity,此时,对应于上面提到的四种Transition

  • AActivity's Exit Transitionnull
  • AActivity's Reenter Transitionnull
  • BActivity's Enter Transition则分为三个部分:
  • 封面从小的圆形渐渐变成大的方形
  • 播放图标的半径渐渐变大
  • 底下的列表采用了自定义的Slide-in动画。
  • BActivity's Exit Transition
  • 上半部分采用了Slide(TOP)的方式,而下半部分采用Slide(BOTTOM)的方式。

四、源码分析

系统默认自带了三种TransitionFade、Slide、Explode,这一节,我们一起来分析一下它们的实现方式:

4.1 Fade

4.1.1 captureXXX函数

首先,我们看一下它获取起点和终点属性的函数:

  • public void captureStartValues(TransitionValues transitionValues)
  • public void captureEndValues(TransitionValues transitionValues)

Fade只重写了captureStartValues,在这里面,它把View当前的translationAlpha值保存起来,这个值表示的是在Transition开始之前ViewtranslationAlpha的值:

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        super.captureStartValues(transitionValues);
        transitionValues.values.put(PROPNAME_TRANSITION_ALPHA, transitionValues.view.getTransitionAlpha());
    }

4.1.2 onAppearonDisappear

在上面的分析当中,我们提到过,当View的可见性从INVISIBLE变为VISIBLE时会调用Transition中的Animator来执行这一变换的过程,例如从AActivity跳转到BActivity,那么BActivity中的View就会调用onAppear所返回的Animator

    @Override
    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        float startAlpha = getStartAlpha(startValues, 0);
        if (startAlpha == 1) {
            startAlpha = 0;
        }
        return createAnimation(view, startAlpha, 1);
    }

这里首先会通过getStartAlpha去获取起始的transitionAlpha值,它是把之前保存在PROPNAME_TRANSITION_ALPHA中的值取出来:

    private static float getStartAlpha(TransitionValues startValues, float fallbackValue) {
        float startAlpha = fallbackValue;
        if (startValues != null) {
            Float startAlphaFloat = (Float) startValues.values.get(PROPNAME_TRANSITION_ALPHA);
            if (startAlphaFloat != null) {
                startAlpha = startAlphaFloat;
            }
        }
        return startAlpha;
    }

下面,我们再回到onAppear函数当中,看一下Animator的创建过程:

    private Animator createAnimation(final View view, float startAlpha, final float endAlpha) {
        if (startAlpha == endAlpha) {
            return null;
        }
        view.setTransitionAlpha(startAlpha);
        final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", endAlpha);
        final FadeAnimatorListener listener = new FadeAnimatorListener(view);
        anim.addListener(listener);
        addListener(new TransitionListenerAdapter() {
            @Override
            public void onTransitionEnd(Transition transition) {
                view.setTransitionAlpha(1);
            }
        });
        return anim;
    }

从上面可以看出,它返回的是一个ObjectAnimator,这个Animator会把ViewtranslationAlphastartAlpha变为1,这也就是一个渐渐显示的过程。
再看一下onDisappear函数,它就是onAppear的反向过程:

    @Override
    public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
            TransitionValues endValues) {
        float startAlpha = getStartAlpha(startValues, 1);
        return createAnimation(view, startAlpha, 0);
    }

4.2 Slide

下面,我们来看一下另一种Transition - Slide的实现原理,和上面类似,我们先看一下captureXXX方都做了什么:

4.2.1 captureXXX

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        super.captureStartValues(transitionValues);
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        super.captureEndValues(transitionValues);
        captureValues(transitionValues);
    }

对于起点和终点值的获取都是调用了下面这个函数,它保存的是View在窗口中的位置:

private void captureValues(TransitionValues transitionValues) {
    View view = transitionValues.view;
    int[] position = new int[2];
    view.getLocationOnScreen(position);
    transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
}

4.2.2 onAppearonDisappear

    @Override
    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        if (endValues == null) {
            return null;
        }
        int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
        //终点值是确定的
        float endX = view.getTranslationX();
        float endY = view.getTranslationY();
        //起点值则需要根据所选的模式来确定
        float startX = mSlideCalculator.getGoneX(sceneRoot, view, mSlideFraction);
        float startY = mSlideCalculator.getGoneY(sceneRoot, view, mSlideFraction);
        //根据起点值、终点值、View所处窗口的位置,来得到一个`Animator`
        return TranslationAnimationCreator.createAnimation(view, endValues, position[0], position[1], startX, startY, endX, endY, sDecelerate, this);
    }

这里面,最关键的是mSlideCalculator,默认情况下为:

    private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() {
        @Override
        public float getGoneY(ViewGroup sceneRoot, View view, float fraction) {
            return view.getTranslationY() + sceneRoot.getHeight() * fraction;
        }
    };

用一张图解解释一下上面的坐标:


所以当我们采用这个Transition的时候,就可以看到它从屏幕的底端滑上来。
onDisappear则也是一个反向的过程:

    @Override
    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null) {
            return null;
        }
        int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
        //这里的起始值和终点值正好是和onAppear相反的.
        float startX = view.getTranslationX();
        float startY = view.getTranslationY();
        float endX = mSlideCalculator.getGoneX(sceneRoot, view, mSlideFraction);
        float endY = mSlideCalculator.getGoneY(sceneRoot, view, mSlideFraction);
        return TranslationAnimationCreator.createAnimation(view, startValues, position[0], position[1], startX, startY, endX, endY, sAccelerate, this);
    }

4.3 小结

通过分析FadeSlide的源码,它们的主要思想就是:

  • capturexxx方法中,把属性保存在TranslationValues中,这里,一定要记得调用对应的super方法让系统保存一些默认的状态。
  • onAppearonDisappear中,根据起点和终点和终点的TranslationValues,构造一个改变View属性的Animator,同时在动画结束之后,还原它的属性。

五、总结

这一篇我们分析了Content Transition的设计思想和原理,下一篇文章我们将着重讨论如何通过代码来实现上面的效果。

六、参考文献

1.Getting Started with Activity & Fragment Transitions (part 1)
2.Content Transitions In-Depth (part 2)
3.Material-Animations

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

推荐阅读更多精彩内容