Android动画 —— 过渡框架

前不久,我写了篇关于Activity之间的过渡跳转的文章(有兴趣的请戳 Android动画 —— Activity过渡,算是初窥了Android过渡Transition)的基本概念,以及它在Activity跳转中的应用。今天,我们就来进一步学习一下它,然后实现自定义的过渡,填补系统内置效果的不足。

一、过渡框架浅谈

Android提供过渡框架,目的是让应用的界面“动起来”,以提升视觉吸引力和视觉上的线索,让用户知道应用到底是怎么工作的。

好像还是不甚明了?

从开发的角度来讲,根本上,过渡框架就是实现了View树View Hierarchy)之间的动画切换 —— 用动画将两个View树下的所有View连接起来。这里的View树,可能一个View,也可能是一个复杂的ViewGroup。

过渡框架支持的功能包括:

  • 组级动画

对View树中的所有View应用一个或多个动画效果

  • 基于过渡的动画

根据起始View和结束View的属性值变化来应用动画

  • 预置动画

引入预置动画以实现通用效果,例如渐隐或移动

  • 资源文件

从资源文件加载View树和内置动画

  • 生命周期

定义了生命周期回调,在动画和View树变化过程中提供更好的控制

二、场景过渡

场景

在过渡框架中,有一个很重要的概念,称为场景Scene)。一个场景,保存了一个View树的状态及其所有View的属性值,也保存了View树的父引用,场景的改变与动画都将发生在这个父引用里面。

Android字义了类Scene来表示一个场景。

场景的过渡过程将涉及起始终止两个状态。在多数情况下,我们不用显示设置起始场景。为什么不用设置?第一,如果已经有应用过过渡,那么接下来的过渡将以已应用过的过渡的终止场景作为起始场景。第二,如果从未应用过渡,那么过渡框架会从当前的屏幕状态收集所有View的信息,然后把它们作为过渡的起始场景。

过渡

场景过渡的使用主要包括三个步骤:

  1. 创建起始场景、终止场景
  2. 设置过渡动画
  3. TransitionManager启动动画

说了这么多,大概还是云里雾里?还是来个例子尝尝鲜吧。

首先,定义场景所在的View树父布局:

1
2
3
4
5
6
7
8
9
10
	<FrameLayout
        android:id="@+id/scene_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="16dp"
        android:paddingTop="16dp">

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

    </FrameLayout>

前面已经说到,场景的转换将发生在它的父引用里面,也就是这里的FrameLayout了。而这里include的布局(a_scene),其实就是我们的起始场景布局。

a_scene.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	    android:id="@+id/scene_container"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent">
	
	    <!-- Ids here must be added and be the same with those in another_scene.xml as well-->
	    <ImageView
	        android:id="@+id/sun"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:src="@drawable/ic_sun" />
	
	    <ImageView
	        android:id="@+id/moon"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:src="@drawable/ic_moon"/>
	</LinearLayout>

然后,定义终止场景布局another_scene。

another_scene.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	    android:id="@+id/scene_container"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent">
	
	    <ImageView
	        android:id="@+id/moon"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:src="@drawable/ic_moon"/>
	
	    <ImageView
	        android:id="@+id/sun"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:src="@drawable/ic_sun" />
	</LinearLayout>

两个场景的布局控件相同,只是摆放的位置不同。

场景布局定义完毕,那就要开始创建场景了。调用Scene类的静态方法getSceneForLayout (ViewGroup sceneRoot, int layoutId, Context context),第一个参数是场景的父引用,第二个参数是场景的布局id。

1
2
3
    mSceneRoot = (FrameLayout) findViewById(R.id.scene_root);
    mAScene = Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this);
    mAnotherScene = Scene.getSceneForLayout(mSceneRoot, R.layout.another_scene, this);

最后,启动场景过渡动画。这里使用ChangeBounds过渡。调用TransitionManager的静态方法go (Scene scene, Transition transition),第一个参数是要切换的目的场景,第二个参数是过渡动画。

1
	TransitionManager.go(mAnotherScene, new ChangeBounds());

效果如下:

Scenes.gif.gif

是不是很方便?虽然用一般的属性动画,也可以做得到这样的效果,但是利用过渡框架,可就简单多了。

值得注意的是:两个场景的“相同”元素,需要使用相同的id,否则没有动画效果

三、无场景过渡

有时候,我们只是需要在当前界面添加或者删除一个View —— 也就是说,变化前后的两个View树几乎是一样的。如果使用场景过渡来实现变化效果,需要维护两个几乎相同的场景布局,未免小题大做。这种情况,无场景的过渡就有了用武之地。

无场景过渡仅包含一个View树,但是维持两个状态,通过“延时过渡”(Delayed transition)实现两个状态的过渡效果。

无场景过渡的使用也主要包括三个步骤:

  1. 调用TransitionManager.beginDelayedTransition()保存View树状态并设置过渡
  2. View树发生改变,过渡框架记录新的状态
  3. 系统重绘,过渡框架启动过渡动画

实际上,需要自行控制的主要是前两步,其它的都由过渡框架完成。

还是来个例子。首先,定义一个过渡动画,包括ChangeBounds和Fade两个效果的组合。然后调用beginDelayedTransition()保存View树状态,并设置好前面定义的过渡动画。接着,addView或者removeView使得View树发生变化。当系统重绘时,将执行上述过渡动画。这里,不断地添加与删除View,观察过渡效果。

1
2
3
4
5
6
7
8
9
10
    mWithoutTransition = new TransitionSet().addTransition(new ChangeBounds()).addTransition(new Fade());
    if (!mIsRemoved) {
    	TransitionManager.beginDelayedTransition(mWithoutRoot, mWithoutTransition);
    	mWithoutRoot.removeView(mRight);
    	mIsRemoved = true;
    } else {
    	TransitionManager.beginDelayedTransition(mWithoutRoot, mWithoutTransition);
    	mWithoutRoot.addView(mRight);
    	mIsRemoved = false;
    }

Without scenes.gif.gif

View的增删再也不用那么无趣了。

四、局限性

当然,虽然过渡框架这么有用,它也有使用限制。

在SurfaceView上应用过渡动画,可能就不会正常实现。前面已经看到,过渡是与View树的变化是息息相关的,View树的变化引起View树重绘,然后过渡才会执行。View的动画是在UI线程,而SurfaceView实例的更新却在非UI线程,所以很可能造成不一致问题。

TextureView上应用特定的过渡动画,也可能不能正常实现。

AdapterView系的类,也不能正确实现过渡动画,因为它们与过渡框架不兼容。

另外,如果在过渡过程中改变一个TextView的大小,文本会在完全改变大小之前跳到新位置,也没法有效实现过渡。官方建议,应该避免过渡过程中改变包含文本TextView的View的尺寸。

五、自定义过渡

介绍完过渡框架在实际应用中的基本使用,是时候切入本文重点了 —— 自定义过渡。

实现

该如何实现呢?

过渡框架提供了一个类Transition,它是一个抽象类,ChangeBoundsChangeImageTransform等等这些都是它的实现子类。类似地,自定义过渡动画,只需要继承Transition,并实现下面几个关键方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public class CustomTransition extends Transition {
	
	    @Override
	    public void captureStartValues(TransitionValues values) {
			//TODO 捕获需要关注的起始场景的相关属性值
		}
	
	    @Override
	    public void captureEndValues(TransitionValues values) {
			//TODO 捕获需要关注的终止场景的相关属性值
		}
	
	    @Override
	    public Animator createAnimator(ViewGroup sceneRoot,
	                                   TransitionValues startValues,
	                                   TransitionValues endValues) {
			//TODO 根据起始、终止场景的属性值,决定是否生成过渡动画及动画效果的实现
		}
	}

捕获属性值

过渡框架的动画来自属性动画系统(Property Animation),因为属性动画是根据View的属性值在一段时间的变化来实现的,那么,过渡框架自然也是需要确定起止属性值的。

对于一个过渡,动画所需要的属性是确定的,所以过渡框架只提供这些需要的属性到过渡中(区别于属性动画系统),然后通过捕获属性的回调方法来保存属性值。

起始属性值

起始属性值在回调方法captureStartValues(TransitionValues transitionValues)中设置,然后将View的起始属性值传递给过渡框架。

其中,参数中的TransitionValues类包含两个域:

  • view:View类型,保存所关注的View的引用
  • values:Map类型,保存所有的属性值

为避免冲突,官方建议values的键的命名按如下规则:

1
package_name:transition_name:property_name

终止属性值

终止属性值在回调方法captureEndValues(TransitionValues transitionValues)中设置。此方法之于captureStartValues中的参数,虽然包含相同的view引用,但是却维护不同的values值,独立存在。

动画适配器

自定义过渡动画需要实现的第三个方法就是动画适配器了。

1
2
3
	Animator createAnimator(ViewGroup sceneRoot,
                               TransitionValues startValues,
                               TransitionValues endValues)

参数sceneRoot是根View,startValues和endValues分别是前面捕获生成的起始和终止属性值。如果确定起始和终止的属性值有变化,那就可以生成自定义动画,并返回给过渡框架,框架将添加动画时长、插值器等等并启动此动画。默认返回null,框架不作操作。

createAnimator()被回调的次数,依赖于起始与终止场景中的“变化数” —— 也就是说,到底我们需要改变多少个目标对象(毕竟,不同目标,动画可能并不相同)。比如,起始场景有5个目标,其中3个留至结束,2个删除。终止场景有4个目标,3个来自于起始场景,1个是新增,那么,createAnimator()将被回调3 + 2 + 1=6次。

自定义过渡动画

说了这么多,我们来看看具体的代码实现。

背景颜色过渡

首先来看一个官方的例子,此过渡用于改变背景颜色(ChangeColor)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
	/*
	 * Copyright (C) 2014 The Android Open Source Project
	 *
	 * Licensed under the Apache License, Version 2.0 (the "License");
	 * you may not use this file except in compliance with the License.
	 * You may obtain a copy of the License at
	 *
	 *      http://www.apache.org/licenses/LICENSE-2.0
	 *
	 * Unless required by applicable law or agreed to in writing, software
	 * distributed under the License is distributed on an "AS IS" BASIS,
	 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	 * See the License for the specific language governing permissions and
	 * limitations under the License.
	 */
	
	package com.djx.customtransition;
	
	import android.animation.Animator;
	import android.animation.ArgbEvaluator;
	import android.animation.ValueAnimator;
	import android.graphics.drawable.ColorDrawable;
	import android.graphics.drawable.Drawable;
	import android.transition.Transition;
	import android.transition.TransitionValues;
	import android.view.View;
	import android.view.ViewGroup;
	
	public class ChangeColor extends Transition {
	
	    /** Key to store a color value in TransitionValues object */
	    private static final String PROPNAME_BACKGROUND = "customtransition:change_color:background";
	
	    /**
	     * Convenience method: Add the background Drawable property value
	     * to the TransitionsValues.value Map for a target.
	     */
	    private void captureValues(TransitionValues values) {
	        // Capture the property values of views for later use
	        values.values.put(PROPNAME_BACKGROUND, values.view.getBackground());
	    }
	
	    @Override
	    public void captureStartValues(TransitionValues transitionValues) {
	        captureValues(transitionValues);
	    }
	
	    // Capture the value of the background drawable property for a target in the ending Scene.
	    @Override
	    public void captureEndValues(TransitionValues transitionValues) {
	        captureValues(transitionValues);
	    }
	
	    // Create an animation for each target that is in both the starting and ending Scene. For each
	    // pair of targets, if their background property value is a color (rather than a graphic),
	    // create a ValueAnimator based on an ArgbEvaluator that interpolates between the starting and
	    // ending color. Also create an update listener that sets the View background color for each
	    // animation frame
	    @Override
	    public Animator createAnimator(ViewGroup sceneRoot,
	                                   TransitionValues startValues, TransitionValues endValues) {
	        // This transition can only be applied to views that are on both starting and ending scenes.
	        if (null == startValues || null == endValues) {
	            return null;
	        }
	        // Store a convenient reference to the target. Both the starting and ending layout have the
	        // same target.
	        final View view = endValues.view;
	        // Store the object containing the background property for both the starting and ending
	        // layouts.
	        Drawable startBackground = (Drawable) startValues.values.get(PROPNAME_BACKGROUND);
	        Drawable endBackground = (Drawable) endValues.values.get(PROPNAME_BACKGROUND);
	        // This transition changes background colors for a target. It doesn't animate any other
	        // background changes. If the property isn't a ColorDrawable, ignore the target.
	        if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) {
	            ColorDrawable startColor = (ColorDrawable) startBackground;
	            ColorDrawable endColor = (ColorDrawable) endBackground;
	            // If the background color for the target in the starting and ending layouts is
	            // different, create an animation.
	            if (startColor.getColor() != endColor.getColor()) {
	                // Create a new Animator object to apply to the targets as the transitions framework
	                // changes from the starting to the ending layout. Use the class ValueAnimator,
	                // which provides a timing pulse to change property values provided to it. The
	                // animation runs on the UI thread. The Evaluator controls what type of
	                // interpolation is done. In this case, an ArgbEvaluator interpolates between two
	                // #argb values, which are specified as the 2nd and 3rd input arguments.
	                ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(),
	                        startColor.getColor(), endColor.getColor());
	                // Add an update listener to the Animator object.
	                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	                    @Override
	                    public void onAnimationUpdate(ValueAnimator animation) {
	                        Object value = animation.getAnimatedValue();
	                        // Each time the ValueAnimator produces a new frame in the animation, change
	                        // the background color of the target. Ensure that the value isn't null.
	                        if (null != value) {
	                            view.setBackgroundColor((Integer) value);
	                        }
	                    }
	                });
	                // Return the Animator object to the transitions framework. As the framework changes
	                // between the starting and ending layouts, it applies the animation you've created.
	                return animator;
	            }
	        }
	        // For non-ColorDrawable backgrounds, we just return null, and no animation will take place.
	        return null;
	    }

非常简单,背景的变化通过ValueAnimator的更新监听进行控制。

使用起来也和系统内置过渡效果一样。

在AS中生成一个简单工程,在主界面MainActivity添加一个文本“Hello World”,然后SecondActivity中同样添加一个文本“Hello World”,设置Activity的共享过渡,并添加过渡动画为这个ChangeColor

SecondActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.djx.customtransition;

import android.app.SharedElementCallback;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.transition.ChangeBounds;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.widget.TextView;

import java.util.List;

public class SecondActivity extends AppCompatActivity {

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

        getWindow().setSharedElementEnterTransition(getTransition());
        setEnterSharedElementCallback(new SharedElementCallback() {
            @Override
            public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);
                target.setBackground(new ColorDrawable(getColor(R.color.bg_start)));
            }

            @Override
            public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);
                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
            }
        });
    }

    private Transition getTransition() {
        TransitionSet set = new TransitionSet();

        ChangeBounds bounds = new ChangeBounds();
        bounds.addTarget(R.id.hello);
        set.addTransition(bounds);

        ChangeColor bg = new ChangeColor();
        bg.addTarget(R.id.hello);
        set.addTransition(bg);

        return set;
    }
}

关于上面的代码,需要注意两点。

第一,为了设置正确的起始和终止场景属性值,共享过渡的回调SharedElementCallback必须添加,因为几个关键方法的回调顺序为:

1
onSharedElementStart -> captureStartValues -> onSharedElementEnd -> captureEndValues

如果不在onSharedElementStartonSharedElementEnd中设置正确的始末属性值,那过渡无法知晓属性值的变化,createAnimator不会回调,过渡动画也就无从谈起。

第二,过渡动画不仅添加了自定义的ChangeColor,还添加了ChangeBounds。单独一个ChangeColor是无法生效的。

在主界面中启动SecondActivity,看下效果

ChangeColor.gif

文本颜色过渡

前面官方的例子,自然成功实现,没有问题。现在依葫芦画瓢,来实现一个文本颜色的过渡动画。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
	package com.djx.customtransition;
	
	import android.animation.Animator;
	import android.animation.ArgbEvaluator;
	import android.animation.ObjectAnimator;
	import android.content.Context;
	import android.transition.Transition;
	import android.transition.TransitionValues;
	import android.util.AttributeSet;
	import android.view.View;
	import android.view.ViewGroup;
	import android.widget.TextView;
	
	public class ChangeTextColor extends Transition {
	
	    private static final String PROPNAME_TEXT_COLOR = "com.djx.customtransition:ChangeTextColor:textColor";
	
	    public ChangeTextColor() {
	    }
	
	    public ChangeTextColor(Context context, AttributeSet attrs) {
	        super(context, attrs);
	    }
	
	    private void captureValues(TransitionValues transitionValues) {
	        if (transitionValues.view instanceof TextView) {
	            transitionValues.values.put(PROPNAME_TEXT_COLOR,
	                    ((TextView) transitionValues.view).getCurrentTextColor());
	        }
	    }
	
	    @Override
	    public void captureStartValues(TransitionValues transitionValues) {
	        captureValues(transitionValues);
	    }
	
	    @Override
	    public void captureEndValues(TransitionValues transitionValues) {
	        captureValues(transitionValues);
	    }
	
	    @Override
	    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
	                                   TransitionValues endValues) {
	        if (startValues == null || endValues == null) {
	            return null;
	        }
	        final View view = endValues.view;
	        if (view instanceof TextView) {
	            TextView textView = (TextView) view;
	            int start = (Integer) startValues.values.get(PROPNAME_TEXT_COLOR);
	            int end = (Integer) endValues.values.get(PROPNAME_TEXT_COLOR);
	            if (start != end) {
	                return ObjectAnimator.ofObject(textView, "textColor",
	                        new ArgbEvaluator(), start, end);
	            }
	        }
	        return null;
	    }
	}

也非常简单,颜色的变化直接使用ObjectAnimator生成了一个属性动画。在SecondActivity中,注释掉前面的ChangeColor过渡,添加ChangeTextColor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	private Transition getTransition() {
	        TransitionSet set = new TransitionSet();
	        
	        ChangeBounds bounds = new ChangeBounds();
	        bounds.addTarget(R.id.hello);
	        set.addTransition(bounds);
	
	//        ChangeColor bg = new ChangeColor();
	//        bg.addTarget(R.id.hello);
	//        set.addTransition(bg);
	
	        ChangeTextColor textColor = new ChangeTextColor();
	        textColor.addTarget(R.id.hello);
	        set.addTransition(textColor);
	        
	        return set;
	    }

同时,在onSharedElementStartonSharedElementEnd中分别设置起始和终止的文本颜色值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
		setEnterSharedElementCallback(new SharedElementCallback() {
	            @Override
	            public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
	                TextView target = (TextView) sharedElements.get(0);
        //            target.setBackground(new ColorDrawable(getColor(R.color.bg_start)));
	                target.setTextColor(getColor(R.color.colorPrimary));
	            }
	
	            @Override
	            public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
	                TextView target = (TextView) sharedElements.get(0);
	//                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
	                target.setTextColor(getColor(R.color.colorAccent));
	            }
	        });

主界面中启动SecondActivity,效果如下

ChangeTextColor.gif.gif

同时添加ChangeColor和ChangeTextColor,主界面中启动SecondActivity,效果正常

ChangeColor & ChangeTextColor.gif.gif

文本大小过渡

既然背景颜色和文本颜色都可以过渡,那么文本大小呢?

同样,实现一个文本大小过渡ChangeTextSize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.djx.customtransition;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class ChangeTextSize extends Transition {

    private static final String PROPNAME_TEXT_SIZE = "com.djx.customtransition:ChangeTextSize:textSize";

    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceof TextView) {
            transitionValues.values.put(PROPNAME_TEXT_SIZE,
                    ((TextView) transitionValues.view).getTextSize());
        }
    }

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

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

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }
        final View view = endValues.view;
        if (view instanceof TextView) {
            final TextView textView = (TextView) view;
            float start = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
            float end = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
            if (start != end) {
                ValueAnimator animator = ValueAnimator.ofFloat(start, end);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        Object value = animation.getAnimatedValue();
                        if (null != value) {
                            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (Float) value);
                        }
                    }
                });
                return animator;
            }
        }
        return null;
    }
}

修改SecondActivity中的“Hello Wolrd”文本大小,然后把此过渡添加至前面的过渡组合中

ChangeTextSize.gif

咦?好像不是那么回事了!仔细看看,其实文本的大小,还是按预期过渡的,是一个逐渐变大变小的过程。但是,主界面到二界面时,直到过渡的最后一刻,控件的尺寸才变成实际大小,即文本变大后的实际控件尺寸。

这就是前面官方已经提到过的关于包含文本控件时的过渡局限性了。

有没有办法解决?

当然有!国外的大牛已经搞定了,膜拜之。

问题原因已经知道,就是过渡过程中,文本在变大,但是View本身的尺寸却没有跟随着变大 —— 因为过渡框架不知道应该改变控件尺寸啊!那么,就在设置终止文本大小值的时候,也把控件的尺寸修改一下可好?看看代码上怎么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);

                // Record the TextView's old width/height.
                int oldWidth = target.getMeasuredWidth();
                int oldHeight = target.getMeasuredHeight();
                
                target.setTextColor(getColor(R.color.colorAccent));
                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
                target.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.end_text_size));

                // Re-measure the TextView (since the text size has changed).
                int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                target.measure(widthSpec, heightSpec);

                // Record the TextView's new width/height.
                int newWidth = target.getMeasuredWidth();
                int newHeight = target.getMeasuredHeight();

				// Set the new bounds
                int widthDiff = newWidth - oldWidth;
                int heightDiff = newHeight - oldHeight;
                target.layout(target.getLeft(), target.getTop(),
                        target.getRight() + widthDiff, target.getBottom() + heightDiff);
            }

首先保存一个原始尺寸,设置新的文本大小后重新测量,然后根据始末尺寸差值,设置过渡结束时的控件尺寸。

Threesome.gif

嗯,原生过渡框架所谓的局限性也不再局限了,完美!

六、小结

过渡框架中的场景过渡和无场景过渡,使用起来非常方便,大家可以发散思维把它们应用到除这里讲到的更多的应用场景中去。

自定义过渡时,从原生例子入手,但是过程中失败很多,也踩了很多雷,多是因为理解不透彻,到最后搞明白的时候发现,其实官书API或者文档已经说清楚了。

路漫漫其修远兮啊!

照样,例子奉上,仅供参考。