@flyouting
2014-03-18T15:39:44.000000Z
字数 5734
阅读 4834
Android KitKat系统中一个最大的亮点是全新的Transitions框架,它提供了一个非常方便的API,以不同的状态之间进行动画的UI。
该Transitions框架让我很好奇它是如何在UI状态之间编排布局边框和动画。这篇文章记录了我在略读了一下源代码后,对 transitions 在Androiod中的实现过程的理解。我在文章中贴出一些源代码链接,方便查询。
虽然这个帖子确实包含一些开发技巧,这不是关于如何使用Transitions的教程。如果你在寻找那类文章,我建议你阅读Mark Allison的教程。
Android中的Transitions Framework本质上说是一种机制,包含布局变化动画化,例如,添加,删除,移动,改变大小,显示,隐藏。
该框架是围绕三个核心元素:场景根,场景和转换。场景根是一个普通视图组,其定义在UI各层次如何转换。一个场景是一个瘦包装器,代表一个特定场景的布局状态。
最后,也是最重要的是,一个Transition是负责捕获布局的差异,并产生动画切换UI状态的组件。任何Transition的执行始终遵循以下步骤:
该过程作为一个整体是由TransitionManager管理的,但大多数的上述步骤(除步骤2)通过transition表现。步骤2可能是一个场景转变或任意的布局变化。
让我们通过一个最简单的方式引发一个transition,看看其中发生什么。这里有一个小的代码示例:
TransitionManager.beginDelayedTransition(sceneRoot);
View child = sceneRoot.findViewById(R.id.child);
LayoutParams params = child.getLayoutParams();
params.width = 150;
params.height = 25;
child.setLayoutParams(params);
这段代码触发一个AutoTransition,在给定场景根动画改变子view的大小。
TransitionManager要在beingDelayedTransition() 做的第一件事是检查在同一场景根中是否有等待延迟的transition,如果有一个,取出执行。这意味着只有第一次beingDelayedTransition()调用并伴随同样的渲染帧才生效。
接着,它会重复使用一个静态 AutoTransition 实例。你也可以通过一个扩展方法提供你自己的 transition。在任何情况下,在任何情况下,它会一直克隆那个给定的 transition 实例,
final Transition transitionClone = transition.clone();
@Override
public Transition clone() {
Transition clone = null;
try {
clone = (Transition) super.clone();
clone.mAnimators = new ArrayList<Animator>();
clone.mStartValues = new TransitionValuesMaps();
clone.mEndValues = new TransitionValuesMaps();
} catch (CloneNotSupportedException e) {}
return clone;
}
以确保一个新的开始,从而使你可以放心地在beingDelayedTransition()方法过程中重用 Transition 实例。
然后它会去捕捉启动状态。
if (transition != null) {
transition.captureValues(sceneRoot, true);
}
如果您在transition中设置了目标视图的ID,它只会捕捉这些view的值。
if (mTargetIds.size() > 0 || mTargets.size() > 0) {
if (mTargetIds.size() > 0) {
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);
}
if (start) {
mStartValues.viewValues.put(view, values);
if (id >= 0) {
mStartValues.idValues.put(id, values);
}
} else {
mEndValues.viewValues.put(view, values);
if (id >= 0) {
mEndValues.idValues.put(id, values);
}
}
}
}
}
否则,它会递归地捕捉场景根的下所有视图的启动状态。因此,在所有的transitions中我们都要设置目标视图,特别是如果我们的场景层次很深,包含了很多子view的情况下。
这里有一个有趣的细节:Transition中捕获状态的代码对于使用有稳定IDs的adapters的listview有特别的处理
sceneChangeSetup(sceneRoot, transitionClone);
它会标记ListView的元素拥有过渡状态,以避免他们在过渡期间被回收。这意味着您可以在一个ListView中添加或删除项目时,很容易进行转换。更新您的适配器之前,只需要调用beginDelayedTransition(),AutoTransition会做剩余的事情,可以参考这个例子
参与transition的每一个view的状态都被存在TransitionValues实例中,本质上说,这个TransitionValues实例就是关联view的map,这是api的一部分,也许TransitionValues能被封装的更好点。
Transition子类在TransitionValues实例中填满了它们所需要的View的状态,例如,更改边界transition,会捕获view的边界(上下左右)和在屏幕中的位置。
private void captureValues(TransitionValues values) {
View view = values.view;
values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
view.getRight(), view.getBottom()));
values.values.put(PROPNAME_PARENT, values.view.getParent());
values.view.getLocationInWindow(tempLocation);
values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
}
一旦启动状态被捕获,beginDelayedTransition()将退出任何一个场景^code
// Notify previous scene that it is being exited
Scene previousScene = Scene.getCurrentScene(sceneRoot);
if (previousScene != null) {
previousScene.exit();
}
设置当前场景为null^code(因为这不是一个场景切换)
Scene.setCurrentScene(sceneRoot, null);
最后等待下一个渲染帧^code。
TransitionManager通过添加一个OnPreDrawListener来等待下一个渲染帧^code
observer.addOnPreDrawListener(listener);
这个OnPreDrawListener在所有的view被正确的测量和布局,准备好在屏幕上绘制时触发(步骤2)。换句话说,当OnPreDrawListener被触发时,所有的涉及这个transition的view都获得了他们的目标大小和布局位置。这意味着是时候捕获这些view的结束状态了(步骤3)^code
transition.captureValues(sceneRoot, false);
逻辑同捕获开始状态一样。
有了所有view的起始和结束状态,transition现在有足够的数据来实现view动画^code。
transition.playTransition(sceneRoot);
它会先更新或取消任何正在运行的当前view的transition^code
ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
然后创建新的动画与新TransitionValues(步骤4)^code。
createAnimators(sceneRoot, mStartValues, mEndValues);
transition将在每个view动画展现结束状态之前“重置”其用户界面到其原始状态,即起始状态,这是唯一可能,因为这段代码在OnPreDrawListener中绘制下一渲染帧之前就执行了。
最后,动画发生器按照定义的顺序(一起或者有序)和逻辑在屏幕中展现^code。
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();
}
切换场景的代码路径跟beginDelayedTransition()很相似,主要的不同就是布局的变化是如何发生的。
调用go()或者transitonTo()的区别仅在于如何得到他们的transition实例,前者只是用一个AutoTransition,后者将得到有TransitionManager定义的transition以及toScene,fromScene属性。
也许场景转换最相关的方面是,它们有效地更换场景根的内容。当一个新的场景被进入,它会删除所有的位于场景根的view,然后把自身加入场景根^code。
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);
}
因此,当你切换到一个新的场景时,请确保你更新了所有保持视图引用的类成员(在你的Activity,Fragment,自定义视图等)。你还需要重建所有之前场景持有的动态状态。例如,如果你从云端加载图像到上一个场景的一个ImageView,你必须把这个状态转移到新场景来。
这里有些关于某些特定的转换实现细节值得一提。
ChangeBounds Transition的动画很有意思,正如它的名字所暗示的,视图边界,但它是如何在渲染帧之间不触发布局而实现效果的呢?它动画展现触发了大小改变的视图框,但是每次 layout()的调用,视图框都会被重置。这使得transition变得不可靠。ChangeBounds通过当transition执行时抑制布局变化来避免上述问题。
parent.suppressLayout(true);
统观Transition框架,架构其实挺简单。最复杂的地方就在于Transition子类处理布局变化和边缘情况。
在OnPreDrawListener之前或者之后捕获起始和结束状态的小技巧可以非常简单的被实现。这不像某些ViewGroup‘s supressLayout()之类的api有访问限制。
做一个快速的实验,我实现了一个Linearlayout动画展现布局变化,但是这只是简单的实现,不要在项目中使用。代码
翻译 @flyouting
2014 年 03月 18日
源地址:这里