[TOC]
介绍
起源
随着Android系统的不断迭代优化,动效功能也越来越强大。Android从API 19开始引入Transition概念,用以对组件间、页面间的动效转场,这也是首次Android官方开始支持页面间的动效。
核心概念
Transition两个核心概念为:场景(scenes)和变换(transitions),场景是UI当前状态,变换则定义了在不同场景之间动画变化的过程。所以Transition主要负责两个方面的事,一是保存开始和结束场景的两种状态,二是在两种状态之间创建动画。由于场景记录了内部所有View的开始和结束状态,所以Transition动画更具连贯性。谁执行动画呢?TransitionManager负责执行动画的任务。
Scene
Scene指的是场景,内部包含一个完整的视图结构,从根布局到所有子View,还有他们的状态信息。
Transition
Transition指的是变换,从一个Scene转场到另一个Scene中,各个视图可以采用不同的Transition变换。Transition变换可以是视图任意属性的变化,可能是透明度、缩放值、高度等等。其内部也是采用属性变化来支持。
开始使用
Transition框架是一个非常简洁的框架,主要包含以下API:
Scene
Scene(下文也称场景)一般利用getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)函数生成场景
参数说明:
sceneRoot:根ViewGroup
layoutId: view的布局资源id,代表一个场景,就是要参与动效的布局
context:上下文
Transition
Transition指的是两个Scene之间视图的变化方式。其工作原理是内部会记录一个Target列表(要做变换的视图列表),然后会记录每个视图变换前后的一些特定信息,在动效期间,根据这些信息来生成各个视图的Animator,最后执行这些Animator即可。
Android系统库自带一些常见的变换效果:
- ChangeBounds:检测view的位置边界创建移动和缩放动画
- ChangeTransform:检测view的scale和rotation创建缩放和旋转动画
- ChangeClipBounds:检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
- ChangeImageTransform:检测ImageView的尺寸,位置以及ScaleType,并创建相应动画。
- Fade,Slide,Explode:根据view的visibility的状态执行渐入渐出,滑动,分解动画
在创建完Transition实例后,我们可以指定这个变换效果应用于哪些视图中。系统默认会应用于变换前后两个Scene中所有id相同的视图,当然也可以通过设置transitionName来指定两个id不同的视图。
Transition类中有以下几种方式来指定或排除视图:
- addTarget(int targetId)添加id为指定id的视图到变换中
- addTarget(String targetName)添加transitionName为指定transitionName的视图到变换中
- addTarget(Class targetType)添加class类型为指定class类型的视图到变换中
- addTarget(View targetView)添加实例视图到变换中
- excludeTarget(String targetName, boolean exclude)排除对应transitionName,不执行变换
- excludeTarget(int targetId,boolean exclude)排除对应id的视图,不执行变换
总结而言,Transition类就是变换的具体实现,内部通过属性动画,对多个View同时进行处理。
TransitionManager
TransitionManager在场景变换时控制transitions的执行。
通过TransitionManager可以添加场景和Transition变换,但为场景变化设置一个默认transitions是没有必要的,因为默认会使用AutoTransition。
TransitionManager当场景变换时开启动画的方式: beginDelayedTransition(ViewGroup sceneRoot, Transition transition) beginDelayedTransition(ViewGroup sceneRoot)
场景变换时传入场景的view根sceneRoot,和transition动画。如果不指定Transition,默认为AutoTransition。
调用场景变换有两种方式
- go(Scene scene, Transition transition)
- go(Scene scene)
go的方式需要传入scene,scene由Scene利用view生成。如果不指定Transition,则默认为AutoTransition。
举例
效果视频
我们先来看要实现的效果:
界面中有三个元素,其中底部是一个按钮,用于切换场景。左上角是一个Text,右下角是一个魔方的图片。
在点击切换按钮后,文本和图片会调换位置,这个效果用Transition来实现再合适不过!
代码实现
首先,我们来分析一下页面的布局结构。我把页面分为两部分,其中页面上方是场景容器,用于承载动效的实现;另外一部分是底部按钮,来启动变换动效。
因此,fragment中的布局代码是:
<!-- fragment_first.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<!-- 场景容器-->
<FrameLayout
android:padding="20dp"
android:id="@+id/fl_scene_root"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintTop_toTopOf="parent" />
<!-- 启动变换的按钮-->
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="50dp"
android:text="@string/change_scene"
android:textColor="@color/black"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后在fragment中,我们会初始化场景动效,进行相关准备。
在onViewCreated方法中,先找到场景容器和切换按钮的视图,然后初始化两个场景,分别是scene1和scene2,注意构造函数里的sceneRoot要传入场景容器,也就是我们之前已经定义好的FrameLayout。
初始化完毕后,调用 TransitionManager的go方法,默认先进入到场景1中。
// FirstFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// view.findViewById<Button>(R.id.button_first).setOnClickListener {
// findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
// }
changeSceneButton = view.findViewById(R.id.button_first)
//场景容器
sceneRoot = view.findViewById(R.id.fl_scene_root)
changeSceneButton.setOnClickListener {
changeScene()
}
//变换前后的两个场景
scene1 = Scene.getSceneForLayout(sceneRoot, R.layout.scene_1, context)
scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.scene_2, context)
//默认先展示场景1
TransitionManager.go(scene1)
isScene1 = true
}
在实例化Scene中,需要传入layout,在这里指的是变换前后的页面布局。
<!-- scene_1.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 文本在左上方-->
<TextView
android:id="@+id/text_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="I am the transiton text"
android:textColor="@color/colorPrimary"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 魔方图在右下方-->
<ImageView
android:id="@+id/image_1"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/rubik_cube"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- scene_2.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 文本在右下方-->
<TextView
android:id="@+id/text_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="50dp"
android:text="I am the transiton text"
android:textColor="@color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<!-- 魔方图在左上方-->
<ImageView
android:id="@+id/image_1"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/rubik_cube"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
上面两个xml分别定了动效前后的布局结构,会在按钮点击后进行切换。
切换使用 TransitionManager的go方法即可,其中toScene就是切换后的场景,changeBounds就是切换时具体的变换方式。
// FirstFragment.kt
private fun changeScene() {
val toScene = if (isScene1) scene2 else scene1
isScene1 = !isScene1
val changeBounds = ChangeBounds()
TransitionManager.go(toScene, changeBounds)
}
至此,一个简单的场景切换动效就能实现了,很简单有木有!
源码分析
基于Android SDK 29源代码,我们来进一步分析下Transition框架是如何运作的。
我们将从创建场景、变换场景两个步骤来分析源代码。
创建场景
场景,也就是Scene,一般是通过Scene的静态方法 getSceneForLayout 来创建:
// Scene.java
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
//将场景信息以<layoutId, Scene>存储在SparseArray中,通过View的tag来存储
SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(
com.android.internal.R.id.scene_layoutid_cache);
if (scenes == null) {
scenes = new SparseArray<Scene>();
sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);
}
//获取对应layoutId的Scene
Scene scene = scenes.get(layoutId);
if (scene != null) {
return scene;
} else {
//第一次调用,会新创建一个Scene实例,将sceneRoot、layoutId和context赋值到实例变量中
scene = new Scene(sceneRoot, layoutId, context);
scenes.put(layoutId, scene);
return scene;
}
}
可以看到创建场景的过程还是比较简单的,就是实例化出来,然后存储到sceneRoot的tag里面。因此一个sceneRoot可以绑定多个场景,这个从逻辑上也是非常合理的,因为场景容器就是承载场景的视图,也可以理解为就是场景的父布局。
因此,layoutId对应的根视图不能有父布局,否则在场景变换中添加场景就会有问题。
变换场景
从前面的使用中,我们可以知道,TransitionManager负责调度场景、变换场景。变换场景一般有两个方法:
// TransitionManager.java
private static Transition sDefaultTransition = new AutoTransition();
public static void go(Scene scene) {
changeScene(scene, sDefaultTransition);
}
public static void go(Scene scene, Transition transition) {
changeScene(scene, transition);
}
如果不指定Transition,就会使用默认的AutoTransition。最终都会调用到 changeScene方法中:
// TransitionManager.java
/**
* This is where all of the work of a transition/scene-change is
* orchestrated. This method captures the start values for the given
* transition, exits the current Scene, enters the new scene, captures
* the end values for the transition, and finally plays the
* resulting values-populated transition.
*
* @param scene The scene being entered
* @param transition The transition to play for this scene change
*/
private static void changeScene(Scene scene, Transition transition) {
final ViewGroup sceneRoot = scene.getSceneRoot();
//当前是否已经加入到sPendingTransitions中
if (!sPendingTransitions.contains(sceneRoot)) {
Scene oldScene = Scene.getCurrentScene(sceneRoot);
if (transition == null) {
// Notify old scene that it is being exited
if (oldScene != null) {
oldScene.exit();
}
scene.enter();
} else {
//从go方法过来,transition不为空,将sceneRoot添加到sPendingTransitions中
sPendingTransitions.add(sceneRoot);
Transition transitionClone = transition.clone();
transitionClone.setSceneRoot(sceneRoot);
if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
transitionClone.setCanRemoveViews(true);
}
//captures the start values for the given transition, exits the current Scene
sceneChangeSetup(sceneRoot, transitionClone);
//enters the new scene, captures the end values for the transition
scene.enter();
//finally plays the resulting values-populated transition
sceneChangeRunTransition(sceneRoot, transitionClone);
}
}
}
注释中已经将方法执行链路解释比较清晰了。主要是由三个执行子过程,分别是(1)抓捕开始信息、退出当前场景 ;(2)进入到新的场景;(3)抓捕结束信息、执行变换场景动效。
由于这里内容较多且复杂,我们分开来讲:
1)抓捕开始信息、退出当前场景
// TransitionManager.java
private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
// Capture current values
ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
//暂停当前进行中的变换动效
if (runningTransitions != null && runningTransitions.size() > 0) {
for (Transition runningTransition : runningTransitions) {
runningTransition.pause(sceneRoot);
}
}
//抓捕变换(transition)所需要的视图信息
if (transition != null) {
transition.captureValues(sceneRoot, true);
}
// Notify previous scene that it is being exited
Scene previousScene = Scene.getCurrentScene(sceneRoot);
if (previousScene != null) {
previousScene.exit();
}
}
变换的captureValues(ViewGroup, boolean)方法用于抓捕ViewGroup中的变换所需视图信息,其中boolean值为true则为变换前,false则为变换后。
在讲解captureValues方法的具体实现之前,我们有必要了解一下变换是如何存储视图信息的。
// TransitionValues.java
public TransitionValues(@NonNull View view) {
this.view = view;
}
/**
* The View with these values
*/
public View view;
/**
* The set of values tracked by transitions for this scene
*/
@NonNull
public final Map<String, Object> values = new ArrayMap<String, Object>();
/**
* The Transitions that targeted this view.
*/
@NonNull
final ArrayList<Transition> targetedTransitions = new ArrayList<Transition>();
其中 TransitionValues 就是用于存储视图的信息类。内部绑定了视图,同时通过map存储必要的状态信息(values),targetedTransitions中记录了视图要执行的变换。
// TransitionValuesMaps.java
class TransitionValuesMaps {
ArrayMap<View, TransitionValues> viewValues =
new ArrayMap<View, TransitionValues>();
SparseArray<View> idValues = new SparseArray<View>();
LongSparseArray<View> itemIdValues = new LongSparseArray<View>();
ArrayMap<String, View> nameValues = new ArrayMap<String, View>();
}
另外一个类是 TransitionValuesMaps,内部 viewValues 存储了对应视图的TransitionValues,idValues则用map存储了<viewId, view>的键值对,nameValues存储了<transitionName, view>的键值对,用于查找指定transitionName的视图。
因此在做变换动效时,场景中的子视图要使用不同的transtionName,否则为发生异常。
// Transition.java
private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
private TransitionValuesMaps mEndValues = new TransitionValuesMaps();
/**
* Recursive method that captures values for the given view and the
* hierarchy underneath it.
* @param sceneRoot The root of the view hierarchy being captured
* @param start true if this capture is happening before the scene change,
* false otherwise
*/
void captureValues(ViewGroup sceneRoot, boolean start) {
clearValues(start);
if ((mTargetIds.size() > 0 || mTargets.size() > 0)
&& (mTargetNames == null || mTargetNames.isEmpty())
&& (mTargetTypes == null || mTargetTypes.isEmpty())) {
for (int i = 0; i < mTargetIds.size(); ++i) {
int id = mTargetIds.get(i);
View view = sceneRoot.findViewById(id);
if (view != null) {
TransitionValues values = new TransitionValues(view);
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.targetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
}
for (int i = 0; i < mTargets.size(); ++i) {
View view = mTargets.get(i);
TransitionValues values = new TransitionValues(view);
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.targetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
} else {
captureHierarchy(sceneRoot, start);
}
//listView特殊处理
...
}
captureValues较为复杂,我们一步一步来看。首先会清除Values,也就是初始或结束视图的所有信息。判断targetId、targetName、targetType三个列表中是否有值,这里指的是我们是否给Transition单独指定过视图。
这个我们在前面也讲到过,Transition可以给指定id视图、指定transitionName视图、指定class类型视图做变换。
由于我们在实例中没有特殊指定,进入到else分支,调用了 captureHierarchy 方法。if和else分支中做的事情是差不多,我们通过分析else分支就能知道它在做什么了。
// Transition.java
private void captureHierarchy(View view, boolean start) {
if (view == null) {
return;
}
int id = view.getId();
if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
return;
}
if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
return;
}
if (mTargetTypeExcludes != null && view != null) {
int numTypes = mTargetTypeExcludes.size();
for (int i = 0; i < numTypes; ++i) {
if (mTargetTypeExcludes.get(i).isInstance(view)) {
return;
}
}
}
if (view.getParent() instanceof ViewGroup) {
TransitionValues values = new TransitionValues(view);
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.targetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
if (view instanceof ViewGroup) {
// Don't traverse child hierarchy if there are any child-excludes on this view
if (mTargetIdChildExcludes != null && mTargetIdChildExcludes.contains(id)) {
return;
}
if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) {
return;
}
if (mTargetTypeChildExcludes != null) {
int numTypes = mTargetTypeChildExcludes.size();
for (int i = 0; i < numTypes; ++i) {
if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
return;
}
}
}
ViewGroup parent = (ViewGroup) view;
for (int i = 0; i < parent.getChildCount(); ++i) {
captureHierarchy(parent.getChildAt(i), start);
}
}
}
这个方法是一个递归方法,会对view树进行递归遍历。
首先是判断了一下是否在Exclude列表当中,如果没有特殊指定,可以先忽略。
然后会根据start为true或者false来分别调用 captureStartValues 和 captureEndValues 方法。这两个方法都是抽象方法,由具体的子类来实现。一般来说方法里面会记录视图的初始和终止态信息,然后借由属性动画来实现具体的单个视图的动效。
我们以Slide 滑动变换来举例说明:
// Slide.java
private void captureValues(TransitionValues transitionValues) {
View view = transitionValues.view;
int[] position = new int[2];
view.getLocationOnScreen(position);
transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
super.captureStartValues(transitionValues);
captureValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
super.captureEndValues(transitionValues);
captureValues(transitionValues);
}
内部都是统一调用到了 captureValues 方法,内部记录了视图在屏幕上的位置信息,用于滑动动效的位移。
然后回到 captureHierarchy 方法内部,还有一处比较重要的是:
// Transition.java
static void addViewValues(TransitionValuesMaps transitionValuesMaps,
View view, TransitionValues transitionValues) {
transitionValuesMaps.viewValues.put(view, transitionValues);
int id = view.getId();
if (id >= 0) {
if (transitionValuesMaps.idValues.indexOfKey(id) >= 0) {
// Duplicate IDs cannot match by ID.
transitionValuesMaps.idValues.put(id, null);
} else {
transitionValuesMaps.idValues.put(id, view);
}
}
String name = view.getTransitionName();
if (name != null) {
if (transitionValuesMaps.nameValues.containsKey(name)) {
// Duplicate transitionNames: cannot match by transitionName.
transitionValuesMaps.nameValues.put(name, null);
} else {
transitionValuesMaps.nameValues.put(name, view);
}
}
//ListView特殊处理,暂不需要
...
}
addViewValues方法主要是把记录下来的信息存储到transitionValuesMaps的idValues和nameValues中。注意这里就有对id相同或者transitionName相同的特殊处理。
至此,关于sceneRoot视图树的所有信息记录已经分析完了,我们在回到开始分析的方法里:
// TransitionManager.java
private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
// Capture current values
ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
//暂停当前进行中的变换动效
if (runningTransitions != null && runningTransitions.size() > 0) {
for (Transition runningTransition : runningTransitions) {
runningTransition.pause(sceneRoot);
}
}
//抓捕变换(transition)所需要的视图信息
if (transition != null) {
transition.captureValues(sceneRoot, true);
}
// Notify previous scene that it is being exited
Scene previousScene = Scene.getCurrentScene(sceneRoot);
if (previousScene != null) {
previousScene.exit();
}
}
我们已经完成初始信息的记录,方法体最后一段代码就是让当前正在显示的场景退出。
2)进入到新的场景
// Scene.java
public void enter() {
// Apply layout change, if any
if (mLayoutId > 0 || mLayout != null) {
// empty out parent container before adding to it
getSceneRoot().removeAllViews();
if (mLayoutId > 0) {
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
} else {
mSceneRoot.addView(mLayout);
}
}
// Notify next scene that it is entering. Subclasses may override to configure scene.
if (mEnterAction != null) {
mEnterAction.run();
}
setCurrentScene(mSceneRoot, this);
}
场景的enter方法用于进入到场景容器中,这里就是将当时创建Scene所传入的layout 布局或者视图,添加到场景容器中(mSceneRoot)。然后将当前场景甜到加SceneRoot的tag中。
3)抓捕结束信息、执行变换动效
// TransitionManager.java
private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
final Transition transition) {
if (transition != null && sceneRoot != null) {
MultiListener listener = new MultiListener(transition, sceneRoot);
sceneRoot.addOnAttachStateChangeListener(listener);
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
}
}
在新的场景进入后,会触发视图的重新绘制。变换执行通过 MultiListener 来真正实现,其中监听了场景容器的attch状态和onPreDraw状态。
// TransitionManager.java
/**
* This private utility class is used to listen for both OnPreDraw and
* OnAttachStateChange events. OnPreDraw events are the main ones we care
* about since that's what triggers the transition to take place.
* OnAttachStateChange events are also important in case the view is removed
* from the hierarchy before the OnPreDraw event takes place; it's used to
* clean up things since the OnPreDraw listener didn't get called in time.
*/
private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
View.OnAttachStateChangeListener {
Transition mTransition;
ViewGroup mSceneRoot;
final ViewTreeObserver mViewTreeObserver;
MultiListener(Transition transition, ViewGroup sceneRoot) {
mTransition = transition;
mSceneRoot = sceneRoot;
mViewTreeObserver = mSceneRoot.getViewTreeObserver();
}
private void removeListeners() {
if (mViewTreeObserver.isAlive()) {
mViewTreeObserver.removeOnPreDrawListener(this);
} else {
mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
}
mSceneRoot.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListeners();
sPendingTransitions.remove(mSceneRoot);
ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
if (runningTransitions != null && runningTransitions.size() > 0) {
for (Transition runningTransition : runningTransitions) {
runningTransition.resume(mSceneRoot);
}
}
mTransition.clearValues(true);
}
@Override
public boolean onPreDraw() {
removeListeners();
// Don't start the transition if it's no longer pending.
if (!sPendingTransitions.remove(mSceneRoot)) {
return true;
}
// Add to running list, handle end to remove it
final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
getRunningTransitions();
ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
ArrayList<Transition> previousRunningTransitions = null;
if (currentTransitions == null) {
currentTransitions = new ArrayList<Transition>();
runningTransitions.put(mSceneRoot, currentTransitions);
} else if (currentTransitions.size() > 0) {
previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
}
currentTransitions.add(mTransition);
mTransition.addListener(new TransitionListenerAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
ArrayList<Transition> currentTransitions =
runningTransitions.get(mSceneRoot);
currentTransitions.remove(transition);
transition.removeListener(this);
}
});
mTransition.captureValues(mSceneRoot, false);
if (previousRunningTransitions != null) {
for (Transition runningTransition : previousRunningTransitions) {
runningTransition.resume(mSceneRoot);
}
}
mTransition.playTransition(mSceneRoot);
return true;
}
};
创建 MultiListener时,会将变换和场景容器传入。这里我们重点关注一下onPreDraw方法是如何真正生成动效并执行它的。
首先要判断 sPendingTransitions 中包含当前要执行的变换,否则退出。这个在之前已经添加过,所以进入到后续代码。
getRunningTransitions获取当前正在运行的变换动效ArrayList,同时将当前要运行的变换添加到 currentTransitions,设置变换结束的回调,将运行完的变换从currentTransitions中删除。
然后,又碰到我们熟悉的 captureValues方法,用于记录场景容器切换完场景,此时的视图状态信息,也就是是终止态信息(变换后的场景视图)。恢复之前暂停的动画。
最后也就是我们压轴的动效执行过程,通过Transition的playTransition方法来执行当前变换。
// Transition.java
/**
* Called by TransitionManager to play the transition. This calls
* createAnimators() to set things up and create all of the animations and then
* runAnimations() to actually start the animations.
*/
void playTransition(ViewGroup sceneRoot) {
mStartValuesList = new ArrayList<TransitionValues>();
mEndValuesList = new ArrayList<TransitionValues>();
matchStartAndEnd(mStartValues, mEndValues);
ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
WindowId windowId = sceneRoot.getWindowId();
for (int i = numOldAnims - 1; i >= 0; i--) {
Animator anim = runningAnimators.keyAt(i);
if (anim != null) {
AnimationInfo oldInfo = runningAnimators.get(anim);
if (oldInfo != null && oldInfo.view != null && oldInfo.windowId == windowId) {
TransitionValues oldValues = oldInfo.values;
View oldView = oldInfo.view;
TransitionValues startValues = getTransitionValues(oldView, true);
TransitionValues endValues = getMatchedTransitionValues(oldView, true);
if (startValues == null && endValues == null) {
endValues = mEndValues.viewValues.get(oldView);
}
boolean cancel = (startValues != null || endValues != null) &&
oldInfo.transition.isTransitionRequired(oldValues, endValues);
if (cancel) {
if (anim.isRunning() || anim.isStarted()) {
if (DBG) {
Log.d(LOG_TAG, "Canceling anim " + anim);
}
anim.cancel();
} else {
if (DBG) {
Log.d(LOG_TAG, "removing anim from info list: " + anim);
}
runningAnimators.remove(anim);
}
}
}
}
}
createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
runAnimators();
}
刚开始时会对视图做一些匹配,这里比较复杂,暂时先不管它。
getRunningAnimators获取当前正在运行的属性动画,将其取消。之后调用createAnimators方法:
// Transition.java
protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
ArrayList<TransitionValues> endValuesList) {
if (DBG) {
Log.d(LOG_TAG, "createAnimators() for " + this);
}
ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
long minStartDelay = Long.MAX_VALUE;
int minAnimator = mAnimators.size();
SparseLongArray startDelays = new SparseLongArray();
int startValuesListCount = startValuesList.size();
for (int i = 0; i < startValuesListCount; ++i) {
TransitionValues start = startValuesList.get(i);
TransitionValues end = endValuesList.get(i);
if (start != null && !start.targetedTransitions.contains(this)) {
start = null;
}
if (end != null && !end.targetedTransitions.contains(this)) {
end = null;
}
if (start == null && end == null) {
continue;
}
// Only bother trying to animate with values that differ between start/end
boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
if (isChanged) {
if (DBG) {
View view = (end != null) ? end.view : start.view;
Log.d(LOG_TAG, " differing start/end values for view " + view);
if (start == null || end == null) {
Log.d(LOG_TAG, " " + ((start == null) ?
"start null, end non-null" : "start non-null, end null"));
} else {
for (String key : start.values.keySet()) {
Object startValue = start.values.get(key);
Object endValue = end.values.get(key);
if (startValue != endValue && !startValue.equals(endValue)) {
Log.d(LOG_TAG, " " + key + ": start(" + startValue +
"), end(" + endValue + ")");
}
}
}
}
// TODO: what to do about targetIds and itemIds?
Animator animator = createAnimator(sceneRoot, start, end);
if (animator != null) {
// Save animation info for future cancellation purposes
View view = null;
TransitionValues infoValues = null;
if (end != null) {
view = end.view;
String[] properties = getTransitionProperties();
if (properties != null && properties.length > 0) {
infoValues = new TransitionValues(view);
TransitionValues newValues = endValues.viewValues.get(view);
if (newValues != null) {
for (int j = 0; j < properties.length; ++j) {
infoValues.values.put(properties[j],
newValues.values.get(properties[j]));
}
}
int numExistingAnims = runningAnimators.size();
for (int j = 0; j < numExistingAnims; ++j) {
Animator anim = runningAnimators.keyAt(j);
AnimationInfo info = runningAnimators.get(anim);
if (info.values != null && info.view == view &&
((info.name == null && getName() == null) ||
info.name.equals(getName()))) {
if (info.values.equals(infoValues)) {
// Favor the old animator
animator = null;
break;
}
}
}
}
} else {
view = (start != null) ? start.view : null;
}
if (animator != null) {
if (mPropagation != null) {
long delay = mPropagation
.getStartDelay(sceneRoot, this, start, end);
startDelays.put(mAnimators.size(), delay);
minStartDelay = Math.min(delay, minStartDelay);
}
AnimationInfo info = new AnimationInfo(view, getName(), this,
sceneRoot.getWindowId(), infoValues);
runningAnimators.put(animator, info);
mAnimators.add(animator);
}
}
}
}
if (startDelays.size() != 0) {
for (int i = 0; i < startDelays.size(); i++) {
int index = startDelays.keyAt(i);
Animator animator = mAnimators.get(index);
long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
animator.setStartDelay(delay);
}
}
这个方法对子类创建属性动画做了一些封装,首先判断初始和终止态信息是否不一致,有变化才执行变换,通过抽象方法让子类实现创建属性动画的细节。
属性动画创建完毕后添加到sRunningAnimators和mAnimators中,设置执行延时。
最后一步是通过runAnimators()来执行这些属性动画:
// Transition.java
/**
* This is called internally once all animations have been set up by the
* transition hierarchy.
*
* @hide
*/
protected void runAnimators() {
if (DBG) {
Log.d(LOG_TAG, "runAnimators() on " + this);
}
start();
ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
// Now start every Animator that was previously created for this transition
for (Animator anim : mAnimators) {
if (DBG) {
Log.d(LOG_TAG, " anim: " + anim);
}
if (runningAnimators.containsKey(anim)) {
start();
runAnimator(anim, runningAnimators);
}
}
mAnimators.clear();
end();
}
遍历mAnimators,执行这些属性动画。
最终,场景的变换通过**【场景内叠加不同变换】、【变换内叠加不同属性动画】**来完美实现。
进阶—自定义Transition
经过上述的分析,我们已经了解了Transition框架的原理和执行流程。但同时也发现对于一种特定类型的视图,具体的变换方式是完全可以由开发者来自定义的。
变换框架也提供了这种可拓展的能力,主要是通过自定义Transition来实现。Android SDK已经提供了一些常用的实现:
具体的用法就自行百度或者翻看源码即可。
自定义Transition时经常要实现的抽象方法或者公共方法通常有三个:
一、public abstract void captureStartValues(TransitionValues transitionValues);
根据自定义的变换场景,来抓捕变换前视图的信息,比如alpha、width、height等等
二、public abstract void captureEndValues(TransitionValues transitionValues);
根据自定义的变换场景,来抓捕变换后视图的信息,比如alpha、width、height等等
三、public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,TransitionValues endValues);
根据自定义的变换场景和变换前后的视图信息,来创建最终要执行的属性动画
下面我们将讲解两个出现在DiDi Food客户端和Conductor库中的自定义Transition,供抛砖引玉。
自定义字体变换分析
DiDi Food客户端在做首页进店的动效时,涉及到多种元素的共享。我们讲解一个比较简单的子动效:卡片上商家名称从小字号变换到大字号的场景变换过程。
照例,先来看效果视频:
仔细看,注意商家卡片上名称的字号从小到大的一个过程。
// TextSizeTransition.java
@TargetApi(VERSION_CODES.KITKAT)
public class TextSizeTransition extends Transition {
private static final String PROPNAME_TEXT_SIZE = "sodaglobaldidifood:transition:textsize";
private static final String[] TRANSITION_PROPERTIES = {PROPNAME_TEXT_SIZE};
private static final Property<TextView, Float> TEXT_SIZE_PROPERTY = AnimationProperty.TEXT_SIZE;
public TextSizeTransition() {
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
Float startSize = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
Float endSize = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
if (startSize == null || endSize == null ||
startSize.floatValue() == endSize.floatValue()) {
return null;
}
TextView view = (TextView) endValues.view;
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, startSize);
return ObjectAnimator.ofFloat(view, TEXT_SIZE_PROPERTY, startSize, endSize);
}
@Override
public String[] getTransitionProperties() {
return TRANSITION_PROPERTIES;
}
private void captureValues(TransitionValues transitionValues) {
if (transitionValues.view instanceof TextView) {
TextView textView = (TextView) transitionValues.view;
transitionValues.values.put(PROPNAME_TEXT_SIZE, textView.getTextSize());
}
}
}
在这个自定义字号变换中,我们在变换前后抓捕了视图的TextSize信息,同时 createAnimator 中返回了字号变化的属性动画。
是不是觉得很简单!!基本上视图中定义的属性值比如alpha、scale、translateY等等都可以这样来处理。
Conductor自定义Fab变换分析
Conductor库是非常重视视觉过渡效果的,其中就有一个比较有意思的变换效果。点击FloatingActionButton之后,弹出一个Dialog,其中的场景变换就是利用自定义Transition实现的。
限于篇幅和时间原因,没有办法在这里去解析它。感兴趣的同学可以爬下源码看一看。