我真是要吐槽下 CSDN 网站,我在家里编辑了草稿发布了文章,在公司点写文章的时候,为何你TM还有我的草稿?搞得我删完草稿,重新发布了一篇新文章,原文章就被覆盖了,我心里真是一群草泥马奔腾而过。还好我在掘金网站投稿,他们帮我备份了,否则我TM的花了几天功夫写的文章就这样没了。
还有,网页各种B广告,真是TM的恶心。
概要
在 Android 4.4 的时候,引入了 Transition Framework ,其原理是利用属性动画,来实现 View 在不同布局的转换效果。
在 Android 5.0 的时候 ,基于 Transition Framework 又引入了 Activity/Fragment Transition 和 Shared Elements Transition。
刚开始用 Transition Framework 的时候,用官网上一些例子试了下,感觉好简单,但是当我在实际中开始使用的时候,却发现效果不尽如人意,那么,从长远的目光看,我觉得还是有必要来真正的了解下 Transition Framework 的原理。
例子
既然是探究原理,所以本文并不会去详细介绍过渡动画如何使用,所以只以一个小例子做引,然后一步一步探究源码,首先看一个效果图
首先,从第一印象看,好像不是属性动画,但是如果再想想如何用属性动画实现的时候,却似乎有点棘手。 然而,这个效果用过渡动画来实现,却非常的简单。
先看几个布局,首先是 Activity 的布局文件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scene_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/scene1"/>
</RelativeLayout>
然后看看 scene1.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
<android.support.design.widget.FloatingActionButton
android:id="@+id/transition_fab"
android:onClick="onFabClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="15dp"
android:layout_marginRight="15dp"
android:src="@android:drawable/ic_dialog_email"
app:fabSize="normal"/>
</RelativeLayout>
最后就是 scene2.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
<ImageView
android:id="@+id/scene2_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/colorPrimary"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/transition_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/scene2_image"
android:layout_alignEnd="@id/scene2_image"
android:layout_marginBottom="-26dp"
android:layout_marginEnd="15dp"
android:onClick="onFabClick"
android:src="@android:drawable/ic_dialog_email"
app:fabSize="normal"/>
</RelativeLayout>
Activity 主要代码如下
public class MainActivity extends AppCompatActivity {
private Scene mScene1;
private Scene mScene2;
private boolean mIsBack;
private Transition mChangeBounds = new ChangeBounds();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewGroup sceneRoot = (ViewGroup) findViewById(R.id.scene_root);
mScene1 = Scene.getSceneForLayout(sceneRoot, R.layout.scene1, this);
mScene2 = Scene.getSceneForLayout(sceneRoot, R.layout.scen2, this);
}
public void onFabClick(View view) {
if (!mIsBack) {
mIsBack = true;
TransitionManager.go(mScene2, mChangeBounds);
} else {
mIsBack = false;
TransitionManager.go(mScene1, mChangeBounds);
}
}
}
代码很简单,我也不多做说明 ,接下来让我们来看看源码如何实现的。
源码实现
在分析之前,先来看2个贯穿整个源码的类,这2个类都是用来保存数据的,所以我们先要搞清楚它们是如何保存数据的。
第一个类,TransitionValues
public class TransitionValues {
/**
* The View with these values
*/
public View view;
/**
* The set of values tracked by transitions for this scene
*/
public final Map<String, Object> values = new ArrayMap<String, Object>();
/**
* The Transitions that targeted this view.
*/
final ArrayList<Transition> targetedTransitions = new ArrayList<Transition>();
// ...
}
TransitionValues 类比较简单,就是用这三个变量来保存数据
- view:代表哪个 View 是需要做动画的
- values:类型为 Map< String,Object>,其中 String 是 - Transition 的实现类定义的属性名,Object 是 Transition 的实现类定义的要保存的数据,这个我们在后面会看到。
- targetedTransitions:类型为 ArrayList< Transition>,来保存 View 需要使用的各种 Transition。
第二个类,TransitionValuesMaps
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>();
}
这个类,说简单点,就相当于 4 个 map,那么这 4 个“map”作用是什么呢?
- viewValues :保存 View 和 TransitionValues 对
- idValues :保存 view.getId() 和 View 对
- itemIdValues:这个是针对 ListView的,保存 ListView 的 Item id 和 View 对
- nameValues :保存 View.getTransitionName() 和 View 对
创建Scene
Scene.getSceneForLayout()
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(
R.id.transition_scene_layoutid_cache);
if (scenes == null) {
scenes = new SparseArray<Scene>();
// 为 sceneRoot 创建一个 SparseArray(相当于map),保存 com.android.internal.R.id.scene_layoutid_cache 和 scenes 键值对
sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);
}
Scene scene = scenes.get(layoutId);
if (scene != null) {
return scene;
} else {
// 创建Scene对象的过程只是简单的赋值,分别赋值到 mSceneRoot, mContext, mLayoutId
scene = new Scene(sceneRoot, layoutId, context);
scenes.put(layoutId, scene);
return scene;
}
}
第 8行,为 sceneRoot 创建一个 tag,这个 tag 是 SparseArray 类型(相当于 map),并保存了一对数据,key 为 R.id.transition_scene_layoutid_cache,value 为 mScenes(类型为 SparseArray< Scene>), 而这个 mScenes 保存了所有用 sceneRoot 创建的 Scene。
执行过渡动画
TransitionManager.go()
private static Transition sDefaultTransition = new AutoTransition();
public static void go(@NonNull Scene scene) {
changeScene(scene, sDefaultTransition);
}
private static void changeScene(Scene scene, Transition transition) {
final ViewGroup sceneRoot = scene.getSceneRoot();
if (!sPendingTransitions.contains(sceneRoot)) {
// 把 sceneRoot 加入到 sPendingTranstions (类型为 ArrayList) 中
sPendingTransitions.add(sceneRoot);
// 做一次深拷贝
Transition transitionClone = null;
if (transition != null) {
transitionClone = transition.clone();
transitionClone.setSceneRoot(sceneRoot);
}
// 从sceneRoot的tag中获取 key 为 com.android.internal.R.id.current_scene 的值
Scene oldScene = Scene.getCurrentScene(sceneRoot);
if (oldScene != null && transitionClone != null &&
oldScene.isCreatedFromLayoutResource()) {
transitionClone.setCanRemoveViews(true);
}
// step1: 保存 sceneRoot 及其 children 的信息到 mStartValues/mEndValues
sceneChangeSetup(sceneRoot, transitionClone);
// step2: 主要做的是,先移除 sceneRoot 中的所有 views,然后把新的布局加载进来
scene.enter();
// step3: 创建 Animator 并运行
sceneChangeRunTransition(sceneRoot, transitionClone);
}
}
代码中注释可以看出,调用 TransitionManager.go() 来完成 transition animation 之前,经历了三步,先看 step1,保存信息。
Step1->保存视图信息
TransitionManager 的 sceneChangeSetup()
private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
// 获取当前正在运行的 Transition
ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
if (runningTransitions != null && runningTransitions.size() > 0) {
for (Transition runningTransition : runningTransitions) {
// 如果有正在运行的 Transition ,就停止,也就是让 Animator 暂停
runningTransition.pause(sceneRoot);
}
}
if (transition != null) {
// 捕获sceneRoot下children的相关值,参数 true 表示这个捕获动作是发生在场景转换前
transition.captureValues(sceneRoot, true);
}
// Notify previous scene that it is being exited
// 获取sceneRoot相关的SparseArray中key为com.android.internal.R.id.current_scene的值
Scene previousScene = Scene.getCurrentScene(sceneRoot);
if (previousScene != null) {
previousScene.exit();
}
}
现在只需要关注 15 行,这一行就把当前的 sceneRoot 数据保存到 transition 中。注意 transition.captureValues(sceneRoot, true) 的第二个参数,true 表示转换发生前,false 表示转换发生后,shit ! 这个方法会针对这个参数分别做出不同的行为,那么我只在这里分析一次,后面不再给出分析。
Transition 的 captureValues() 方法
void captureValues(ViewGroup sceneRoot, boolean start) {
// start 为 true/false,清空 mStartValues/mEndValues
clearValues(start);
// mTargetIds 是调用 Transition.addTargetIds()/removeTargetIds() 后才有值的
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();
values.view = 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();
values.view = 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 {
// 捕获sceneRoot,以及children的信息并保存到 mStartValues/mEndValues
captureHierarchy(sceneRoot, start);
}
// mNameOverrides 是针对 Fragment shared elements transitions
// TODO: 后面分析
if (!start && mNameOverrides != null) {
int numOverrides = mNameOverrides.size();
ArrayList<View> overriddenViews = new ArrayList<View>(numOverrides);
for (int i = 0; i < numOverrides; i++) {
String fromName = mNameOverrides.keyAt(i);
overriddenViews.add(mStartValues.nameValues.remove(fromName));
}
for (int i = 0; i < numOverrides; i++) {
View view = overriddenViews.get(i);
if (view != null) {
String toName = mNameOverrides.valueAt(i);
mStartValues.nameValues.put(toName, view);
}
}
}
}
第 3 行,清空 mStartValues/mEndValues,这两个变量都是 TransitionValuesMaps 类型(文章开头已经给出)。
由于我在例子中并没有为 Transition 设置 target,所以这段代码,关注47行,captureHierarchy()。这个方法是用来保存 sceneRoot 的信息,以及遍历 sceneRoot 的 children ,并且递归调用 captureHierarchy() 来保存所有 children 的信息。
Transition 的 captureHierarchy()
private void captureHierarchy(View view, boolean start) {
if (view == null) {
return;
}
int id = view.getId();
// mTargetIdExcludes 是在调用 Transition.excludeTarget(int targetId, boolean exclude) 后才有值
if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
return;
}
// mTargetExcludes 是在调用 Transition.excludeTarget(View target, boolean exclude) 后才有值
if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
return;
}
// mTargetTypeExcludes 是在调用 Transition.excludeTarget(Class type, boolean exclude) 后才有值
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();
values.view = view;
if (start) {
// 为 TransitionValues.values存值,保存起始场景View的属性值,自定义Transition类,这个方法要复写
captureStartValues(values);
} else {
// 为 TransitionValues.values存值,保存结束场景View的属性值,自定义Transition类,这个方法要复写
captureEndValues(values);
}
// 把当前 Transition 保存到 TransitionValues.targetedTransitions
values.targetedTransitions.add(this);
if (start) {
// 为 mStartValues 的 4 个 map 存值
addViewValues(mStartValues, view, values);
} else {
// 为 mEndValues 的 4 个 map 存值
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;
}
}
}
// 遍历,递归调用,直到根部局下所有的 child 都保存了值
ViewGroup parent = (ViewGroup) view;
for (int i = 0; i < parent.getChildCount(); ++i) {
captureHierarchy(parent.getChildAt(i), start);
}
}
}
26 到 45 行,用来保存数据的。
63 到 66 行,是遍历 sceneRoot 的 children,递归调用 captureHierarchy() 方法来保存数据。 所以整个 captureHierarchy() 方法就中用来保存 View(包括 sceneRoot) 的数据。
那么保存数据关键的地方就是 29 到 35 行, captureStartValues()/captureEndValues() ,分别用来保存转换 前/后 布局的信息,这2个方法,是在 Transtion 类中是抽象方法,所以我们要用一个 Transition 实现类来分析,那么这里就用例子中的 ChangeBounds 类的 captureStartValues() 方法来分析。
ChangeBounds.java 的 captureStartValues()
@Override
public void captureStartValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
private static final String PROPNAME_CLIP = "android:changeBounds:clip";
private static final String PROPNAME_PARENT = "android:changeBounds:parent";
private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
private void captureValues(TransitionValues values) {
View view = values.view;
if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
view.getRight(), view.getBottom()));
values.values.put(PROPNAME_PARENT, values.view.getParent());
if (mReparent) {
values.view.getLocationInWindow(tempLocation);
values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
}
if (mResizeClip) {
values.values.put(PROPNAME_CLIP, view.getClipBounds());
}
}
}
这段代码解释下,先看第 20 行,view.isLaidOut() 是说这个 view 已经至少被 layout() 了一次。其实这整行的意思就是 view 是否能获得宽高的值(不为0)。
第25行,mReparent 是由 setReparent() 设置的,mReparent 的作用是重新定义 View 的 parent ,不过这个功能已经在 changeTransform 类中实现,所以这里已经是 deprecated,默认为 false,而我们例子中也没有设置,所以就不探究了。
第31行,mResizeClip 是由 setResizeClip() 设置的,这个作用是,用 View 的 clip bounds 来实现动画,默认为 flase,同样,例子中并没有设置这个,所以也不用去探究。
那么整个代码,其实我们只需要关注的就是 21 到 23 行,从这里就可以看到 TransitionValues.values 保存的是什么数据。
那么,再回到 captureHierarchy() 方法的 37行,把当前的 transtion 保存到 values.targetedTranstions 中(如果不知道这是什么,看前面)。
再来看captureHierarchy() 方法的 38 到 44行 的 addViewValues() 方法,这个方法是把刚刚创建的局部变量 values( TransitionValues 对象 ) 的值保存到 mStartValues/mEndValues (TranstionValuesMaps 对象,前面提到过)中。
Transition 的 addViewValues() 方法
private static void addViewValues(TransitionValuesMaps transitionValuesMaps,
View view, TransitionValues transitionValues) {
// step1
transitionValuesMaps.mViewValues.put(view, transitionValues);
int id = view.getId();
if (id >= 0) {
if (transitionValuesMaps.mIdValues.indexOfKey(id) >= 0) {
// Duplicate IDs cannot match by ID.
// 如果有重复的 id,那么就不去匹配了,直接把 id 和 null 对应
transitionValuesMaps.mIdValues.put(id, null);
} else {
// step2
transitionValuesMaps.mIdValues.put(id, view);
}
}
String name = ViewCompat.getTransitionName(view);
if (name != null) {
if (transitionValuesMaps.mNameValues.containsKey(name)) {
// Duplicate transitionNames: cannot match by transitionName.
// 如果有重复的 transitionName,那么就不去匹配了,直接把 transitionName和 null 对应
transitionValuesMaps.mNameValues.put(name, null);
} else {
// step3
transitionValuesMaps.mNameValues.put(name, view);
}
}
// 关于 ListView 的
if (view.getParent() instanceof ListView) {
ListView listview = (ListView) view.getParent();
if (listview.getAdapter().hasStableIds()) {
int position = listview.getPositionForView(view);
long itemId = listview.getItemIdAtPosition(position);
if (transitionValuesMaps.mItemIdValues.indexOfKey(itemId) >= 0) {
// Duplicate item IDs: cannot match by item ID.
View alreadyMatched = transitionValuesMaps.mItemIdValues.get(itemId);
if (alreadyMatched != null) {
ViewCompat.setHasTransientState(alreadyMatched, false);
transitionValuesMaps.mItemIdValues.put(itemId, null);
}
} else {
ViewCompat.setHasTransientState(view, true);
// step4
transitionValuesMaps.mItemIdValues.put(itemId, view);
}
}
}
}
从注释中的 step1, step2, step3, step4 可知,分为 4 步保存数据,其中我们需要注意就是,同一个布局中不要出现重复的 id 或 重复的 transitionname,不然是不能实现动画的。 最后一个关于 ListView 的暂时还没研究是怎么回事。反正关注前三步就可以了。
估计有人看到这里有点懵逼了,会问自己我们到哪了?都干了点啥?那么现在总结下,到此为止,我们完成了 TransitionManager.go() 的 step1,我们已经将转换前的布局中的每个 view(包括 sceneRoot),保存信息了,保存到了 mStartValues (TransitionValuesMaps)中。
Step2->布局的切换
现在我们来分析 TransitionManager.go( scene ) 中的 step2,scene.enter(),这个方法主要的作用是先 remove sceneRoot 中 的children,然后再把 scene 的 layoutId 加载到 sceneRoot 中。
public void enter() {
// Apply layout change, if any
// Scene 对象的创建方式有2中,一种传入 layoutId,一种传入 view,这里有满足
if (mLayoutId > 0 || mLayout != null) {
// empty out parent container before adding to it
getSceneRoot().removeAllViews();
// 加载进 mSceneRoot 中
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.
// mEnterAction 是一个 Runnable 对象,如果为 Scene 设置了 setEnterAction() 会调用
if (mEnterAction != null) {
mEnterAction.run();
}
// 为 mSceneRoot 的 tag(SparseArray对象) 的 key 为 R.id.transition_current_scene 设置相应的 value, 这个 value 就是当前的 end scene
setCurrentScene(mSceneRoot, this);
}
代码已经注释很清楚了,不多说了。
到此,已经完成了 TransitionManager.go(scene) 中的 step2,这一步比较简单。
Step3->生成并执行动画
再看 TransitionManager.go(scene) 的step3,sceneChangeRunTransition(),这一步是创建 Animator,并运行。
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);
}
}
private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
View.OnAttachStateChangeListener {
Transition mTransition;
ViewGroup mSceneRoot;
MultiListener(Transition transition, ViewGroup sceneRoot) {
mTransition = transition;
mSceneRoot = sceneRoot;
}
private void removeListeners() {
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();
// TransitionManager.go() 的时候 add 过一次
sPendingTransitions.remove(mSceneRoot);
// 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) {
// 如果不存在正在运行的 transition,就初始化
currentTransitions = new ArrayList<>();
runningTransitions.put(mSceneRoot, currentTransitions);
} else if (currentTransitions.size() > 0) {
// 如果有正在运行的 transtion, 就用 previousRunningTransitions 保存
previousRunningTransitions = new ArrayList<>(currentTransitions);
}
currentTransitions.add(mTransition);
mTransition.addListener(new Transition.TransitionListenerAdapter() {
@Override
public void onTransitionEnd(@NonNull Transition transition) {
ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
currentTransitions.remove(transition);
}
});
// step1 保存 end scene 的信息
mTransition.captureValues(mSceneRoot, false);
if (previousRunningTransitions != null) {
for (Transition runningTransition : previousRunningTransitions) {
// 继续运行之前暂停的 transition
runningTransition.resume(mSceneRoot);
}
}
// step2 创建并运行动画
mTransition.playTransition(mSceneRoot);
return true;
}
}
在 step2 的时候,remove , add children 必然会导致视图刷新,那么现在关注 46 行,在 redraw 之前,调用 onPreDraw()。
在前面介绍过 sceneChangeSetup() 方法的时候,我们获取过一次运行的动画,如果有,就会暂停,代码在 sceneChangeSetup() 的第 6 到 11 行。那么这里的代码的 51 到 62,以及 72 到 78 行,就恢复之前暂停的动画。
那么现在把目光聚焦到 72 行,mTransition.captureValues(mSceneRoot, false); 这个方法我前面介绍过,总结下就是保存数据信息到 mEndValues(TransitionValuesMaps 对象),不熟悉的自己再过一遍。
那么现在把目光放到第 80 行,这里就是重点,创建动画,并执行动画。
void playTransition(ViewGroup sceneRoot) {
// mStartValuesList/mEndValuesList 这里才开始初始化
mStartValuesList = new ArrayList<TransitionValues>();
mEndValuesList = new ArrayList<TransitionValues>();
// 过滤掉不匹配做动画的的View
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();
}
注意第3,4行的,这里才初始化了两个全局变量 mStartValuesList 和 mEndValuesList,类型为 ArrayList< TransitionValues >。
第7行,过滤掉不能匹配做动画的 View,看下matchStartAndEnd() 方法
int[] mMatchOrder = DEFAULT_MATCH_ORDER;
private static final int[] DEFAULT_MATCH_ORDER = {
MATCH_NAME,
MATCH_INSTANCE,
MATCH_ID,
MATCH_ITEM_ID,
};
private void matchStartAndEnd(TransitionValuesMaps startValues,
TransitionValuesMaps endValues) {
// 获取 mStartValues 和 mEndValues 的 viewValues
ArrayMap<View, TransitionValues> unmatchedStart =
new ArrayMap<View, TransitionValues>(startValues.viewValues);
ArrayMap<View, TransitionValues> unmatchedEnd =
new ArrayMap<View, TransitionValues>(endValues.viewValues);
// 按照 mMathOrder 的顺序找到匹配的项
for (int i = 0; i < mMatchOrder.length; i++) {
switch (mMatchOrder[i]) {
case MATCH_INSTANCE:
// step2
matchInstances(unmatchedStart, unmatchedEnd);
break;
case MATCH_NAME:
// step1
matchNames(unmatchedStart, unmatchedEnd,
startValues.nameValues, endValues.nameValues);
break;
case MATCH_ID:
// step3
matchIds(unmatchedStart, unmatchedEnd,
startValues.idValues, endValues.idValues);
break;
case MATCH_ITEM_ID:
// step4
matchItemIds(unmatchedStart, unmatchedEnd,
startValues.itemIdValues, endValues.itemIdValues);
break;
}
}
//
addUnmatched(unmatchedStart, unmatchedEnd);
}
第17行,按照 mMatchOrder 数组的顺序,开始过滤不匹配的 View,并保存匹配的 View,先看 step1 ,利用 transitionName 来过滤,也就是说设置了 transitionName 就可以有 transition animation。
/**
* Match start/end values by Adapter transitionName. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
* startNames and endNames as a guide for which Views have unique transitionNames.
*/
private void matchNames(ArrayMap<View, TransitionValues> unmatchedStart,
ArrayMap<View, TransitionValues> unmatchedEnd,
ArrayMap<String, View> startNames, ArrayMap<String, View> endNames) {
int numStartNames = startNames.size();
for (int i = 0; i < numStartNames; i++) {
View startView = startNames.valueAt(i);
if (startView != null && isValidTarget(startView)) {
// 获取与 startNames 中有相同 transitionName 的 View
View endView = endNames.get(startNames.keyAt(i));
if (endView != null && isValidTarget(endView)) {
// 注意:进入这里的条件是 startView 和 endView 不为空,且 transitionName 相同
// 获取 transitionName 匹配成功的 startView 和 endView 的 TransitionValues
TransitionValues startValues = unmatchedStart.get(startView);
TransitionValues endValues = unmatchedEnd.get(endView);
// 如果找到 startValues 和 endValues 不为空
if (startValues != null && endValues != null) {
// 把得到的 startValues 和 endValues 放入到 mStartValuesList 和 mEndValuesList 中
mStartValuesList.add(startValues);
mEndValuesList.add(endValues);
// 把匹配成功的 View 从 unmatchedStart/unmatchedEnd 中移除
unmatchedStart.remove(startView);
unmatchedEnd.remove(endView);
}
}
}
}
}
首先我们要搞清楚,这个方法的四个参数到底是什么。
unmatchedStart 和 unmatchedEnd 是复制 mStartValues 和 mEndValues 的变量 viewValues 的值,注意是复制,而不是原本的值。
而 startNames 和 endValues 就是 mStartValues 和 mEndValues 的 nameValues 的值,这里就是原本的值了。
其实这个方法的注释我没有删除 ,因为它对这个方法描述的很清楚,如果还不明白就看下注释,所以这里就跳过了。
现在看看 matchStartAndEnd() 方法中的 step2
/**
* Match start/end values by View instance. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd.
*/
private void matchInstances(ArrayMap<View, TransitionValues> unmatchedStart,
ArrayMap<View, TransitionValues> unmatchedEnd) {
for (int i = unmatchedStart.size() - 1; i >= 0; i--) {
// 获取 unmatchedStart 的第 i 个 View
View view = unmatchedStart.keyAt(i);
if (view != null && isValidTarget(view)) {
// 从 unmatchedEnd 找到相应 View 的 TransitionValues 值,并移除 View
TransitionValues end = unmatchedEnd.remove(view);
// 如果找到的 end (TransitionValues对象) 不为空,且 View 有效
if (end != null && end.view != null && isValidTarget(end.view)) {
// 移除 View 并获取 TransitionValues 值
TransitionValues start = unmatchedStart.removeAt(i);
// 匹配成功,添加到 mStartValuesList 和 mEndValuesList 中
mStartValuesList.add(start);
mEndValuesList.add(end);
}
}
}
}
这个代码逻辑比较简单的,但是我这里确实有个疑问,前后两个 layout ,在怎样的情况下,用的是同一个 View? 这个我还没想出来,如果有人知道,还请赐教。
现在看看 matchStartAndEnd() 方法中的 step3
/**
* Match start/end values by Adapter view ID. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
* startIds and endIds as a guide for which Views have unique IDs.
*/
private void matchIds(ArrayMap<View, TransitionValues> unmatchedStart,
ArrayMap<View, TransitionValues> unmatchedEnd,
SparseArray<View> startIds, SparseArray<View> endIds) {
int numStartIds = startIds.size();
for (int i = 0; i < numStartIds; i++) {
View startView = startIds.valueAt(i);
if (startView != null && isValidTarget(startView)) {
View endView = endIds.get(startIds.keyAt(i));
if (endView != null && isValidTarget(endView)) {
TransitionValues startValues = unmatchedStart.get(startView);
TransitionValues endValues = unmatchedEnd.get(endView);
if (startValues != null && endValues != null) {
mStartValuesList.add(startValues);
mEndValuesList.add(endValues);
unmatchedStart.remove(startView);
unmatchedEnd.remove(endView);
}
}
}
}
}
写注释我发现也是挺累的,所以我就这里直接来描述下,先是找到第 i 个 startView,如果 startView 不为 null,就找到与 startView 相同 id 的 endView,如果 endView 也不为空,那么就找出相应的 TransitionValues,并把它添加到 mStartValuesList 和 mEndValuesList 中,并且把匹配到 View 从 unmatchedStart 和 unmatchedEnd 移除。
而 matchStartAndEnd() 方法中的 step4 是针对 ListView 的,这里我就不分析了,我现在几乎没有用 ListView,如果还有用 ListView 的兄弟,可以根据我这篇文章继续深入分析。
那么现在回头整理下发型,哦不,是整理思路,我们现在已经分析完了 matchStartAndEnd() 的 step1 到 step3,那么现在 unmatchedStart 和 unmatchedEnd 中就剩下找不到妹子的 View 了, 找不到怎么办呢,new 一个呗,而 addUnmatched() 方法中 new 的这个妹子是 null。
private void addUnmatched(ArrayMap<View, TransitionValues> unmatchedStart,
ArrayMap<View, TransitionValues> unmatchedEnd) {
// Views that only exist in the start Scene
for (int i = 0; i < unmatchedStart.size(); i++) {
final TransitionValues start = unmatchedStart.valueAt(i);
if (isValidTarget(start.view)) {
mStartValuesList.add(start);
mEndValuesList.add(null);
}
}
// Views that only exist in the end Scene
for (int i = 0; i < unmatchedEnd.size(); i++) {
final TransitionValues end = unmatchedEnd.valueAt(i);
if (isValidTarget(end.view)) {
mEndValuesList.add(end);
mStartValuesList.add(null);
}
}
}
我相信这段代码很好懂了吧,我就班门开斧了。
那么现在,我们再来总结下,根据 transitionName,id,View Instance,listView 的 item id 成功匹配到的 View , 把它们的 TransitionValues 加入到了 mStartValuesList 和 mEndValuesList 中了,而没有匹配成功的就用 null 来代替。
现在我们已经分析到了 playTransition() 方法的 46 行,这是就是创建动画,createAnimators()
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;
}
// 过滤同时为 null 的情况
if (start == null && end == null) {
continue;
}
// Only bother trying to animate with values that differ between start/end
// 如果 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?
// 为匹配到的 View 创建动画
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 (view != null && properties != null && properties.length > 0) {
infoValues = new TransitionValues();
infoValues.view = 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) {
// 如果调用过 setPropagation(),就获取每个 Transition 的动画延迟时间
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);
}
}
}
重点看51行,51行前都是一些条件过滤,createAnimator() 在 Transition 类中是一个抽象方法,所以需要子类去实现,我还是挑选 ChangeBounds 类来说明 ,由于代码比较多,只显示 ChangeBounds 用默认的方式 new ChangeBounds() 创建动画的代码。
@Override
public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
TransitionValues endValues) {
// 只有 startValues 和 endValues 都不为空才创建动画
if (startValues == null || endValues == null) {
return null;
}
// ...
// 注意,这是获取的是 end scene 的 View
final View view = endValues.view;
// 如果不设置 mReparent 为 true,parentMatches() 默认返回为 true
if (parentMatches(startParent, endParent)) {
Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
final int startLeft = startBounds.left;
final int endLeft = endBounds.left;
final int startTop = startBounds.top;
final int endTop = endBounds.top;
final int startRight = startBounds.right;
final int endRight = endBounds.right;
final int startBottom = startBounds.bottom;
final int endBottom = endBounds.bottom;
final int startWidth = startRight - startLeft;
final int startHeight = startBottom - startTop;
final int endWidth = endRight - endLeft;
final int endHeight = endBottom - endTop;
// 如果设置 mResizeClip = true ,并且为 View 调用过 setClipBounds(),这里才有值
Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
int numChanges = 0;
if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
if (startLeft != endLeft || startTop != endTop) ++numChanges;
if (startRight != endRight || startBottom != endBottom) ++numChanges;
}
if ((startClip != null && !startClip.equals(endClip)) ||
(startClip == null && endClip != null)) {
++numChanges;
}
if (numChanges > 0) {
Animator anim;
if (!mResizeClip) {
// 运行动画之前,先设置 view 的坐标为 start scene 的坐标
view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom);
// 如果不用 clip bounds 实现动画
if (numChanges == 2) {
// 如果宽高相等,只创建一个动画就可以达到整体平移的效果
if (startWidth == endWidth && startHeight == endHeight) {
Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
endTop);
anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
topLeftPath);
}
// 如果宽高有一个不相等,就需要创建两个动画来达到效果
else {
final ViewBounds viewBounds = new ViewBounds(view);
Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
endLeft, endTop);
ObjectAnimator topLeftAnimator = ObjectAnimator
.ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
endRight, endBottom);
ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds,
BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
AnimatorSet set = new AnimatorSet();
// 两个动画是同时进行的
set.playTogether(topLeftAnimator, bottomRightAnimator);
anim = set;
set.addListener(new AnimatorListenerAdapter() {
// We need a strong reference to viewBounds until the
// animator ends.
private ViewBounds mViewBounds = viewBounds;
});
}
} else if (startLeft != endLeft || startTop != endTop) {
// ...
} else {
// ...
}
} else {
// ...
}
// 如果 Transition 运行期间,设置状态,不让 view 的 parent 刷新布局,Transition 结束后再恢复状态
if (view.getParent() instanceof ViewGroup) {
final ViewGroup parent = (ViewGroup) view.getParent();
parent.suppressLayout(true);
TransitionListener transitionListener = new TransitionListenerAdapter() {
boolean mCanceled = false;
@Override
public void onTransitionCancel(Transition transition) {
parent.suppressLayout(false);
mCanceled = true;
}
@Override
public void onTransitionEnd(Transition transition) {
if (!mCanceled) {
parent.suppressLayout(false);
}
}
@Override
public void onTransitionPause(Transition transition) {
parent.suppressLayout(false);
}
@Override
public void onTransitionResume(Transition transition) {
parent.suppressLayout(true);
}
};
addListener(transitionListener);
}
return anim;
}
} else {
// ...
}
return null;
}
注释已经写的很清楚了,我这里就简单总结下,先是获取 end view,然后把这个 view 设置到转换前的的位置,然后再设置相应的动画来达到位移的效果。其实到这里,我们已经能把画面脑补出来了!
那么 createAnimators() 后面的代码就是保存创建的动画到 mAnimators,以及为动画设置延迟时间 ,大家自己看注释就行了,
。
那么现在,我们已经分析到了 playTransition() 的最后一行,runAnimators()。
protected void runAnimators() {
if (DBG) {
Log.d(LOG_TAG, "runAnimators() on " + this);
}
// 调用 Transition 的 TransitionListener 的 onTransitionStart()
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();
// 调用 Transition 的 TransitionListener 的 onTransitionEnd()
end();
}
protected void start() {
if (mNumInstances == 0) {
if (mListeners != null && mListeners.size() > 0) {
ArrayList<TransitionListener> tmpListeners =
(ArrayList<TransitionListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionStart(this);
}
}
mEnded = false;
}
mNumInstances++;
}
protected void end() {
--mNumInstances;
if (mNumInstances == 0) {
if (mListeners != null && mListeners.size() > 0) {
ArrayList<TransitionListener> tmpListeners =
(ArrayList<TransitionListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionEnd(this);
}
}
for (int i = 0; i < mStartValues.itemIdValues.size(); ++i) {
View view = mStartValues.itemIdValues.valueAt(i);
if (view != null) {
view.setHasTransientState(false);
}
}
for (int i = 0; i < mEndValues.itemIdValues.size(); ++i) {
View view = mEndValues.itemIdValues.valueAt(i);
if (view != null) {
view.setHasTransientState(false);
}
}
mEnded = true;
}
}
我们把目光放到第6行,这里就是运行刚刚创建的动画了。
private void runAnimator(Animator animator,
final ArrayMap<Animator, AnimationInfo> runningAnimators) {
if (animator != null) {
// TODO: could be a single listener instance for all of them since it uses the param
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mCurrentAnimators.add(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
runningAnimators.remove(animation);
mCurrentAnimators.remove(animation);
}
});
animate(animator);
}
}
protected void animate(Animator animator) {
// TODO: maybe pass auto-end as a boolean parameter?
if (animator == null) {
end();
} else {
if (getDuration() >= 0) {
animator.setDuration(getDuration());
}
if (getStartDelay() >= 0) {
animator.setStartDelay(getStartDelay() + animator.getStartDelay());
}
if (getInterpolator() != null) {
animator.setInterpolator(getInterpolator());
}
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
end();
animation.removeListener(this);
}
});
animator.start();
}
}
啊,看到最后一行,动画就真的跑起来了,一切就到此为止了,我们框架分析也到此为止了。不过其他的细节,我就不细讲了,有兴趣的可以自己研究下代码的逻辑。
总结
本文通过一个例子做引子,从而探究了源码如何实现。然而本文只是抛砖引玉,让大家能了解 Transition Framework 是如何运作的,那么以后如果遇到什么问题,就可以追根溯源了。
Transition 的实现类有很多,例如 Slide , ChangeTransform, Explode 等等,具体这些效果是如何实现的,就留给大家自己分析了。
最后,如果我还想实现自己的动画,我们就需要自己实现 Transition 类,这将是一个非常有挑战的任务。