[关闭]
@flyouting 2014-03-18T15:39:44.000000Z 字数 5734 阅读 4827

Android transitions 如何工作


此处输入图片的描述

Android KitKat系统中一个最大的亮点是全新的Transitions框架,它提供了一个非常方便的API,以不同的状态之间进行动画的UI。

该Transitions框架让我很好奇它是如何在UI状态之间编排布局边框和动画。这篇文章记录了我在略读了一下源代码后,对 transitions 在Androiod中的实现过程的理解。我在文章中贴出一些源代码链接,方便查询。

虽然这个帖子确实包含一些开发技巧,这不是关于如何使用Transitions的教程。如果你在寻找那类文章,我建议你阅读Mark Allison的教程

The framework

Android中的Transitions Framework本质上说是一种机制,包含布局变化动画化,例如,添加,删除,移动,改变大小,显示,隐藏。

该框架是围绕三个核心元素:场景根,场景和转换。场景根是一个普通视图组,其定义在UI各层次如何转换。一个场景是一个瘦包装器,代表一个特定场景的布局状态。

最后,也是最重要的是,一个Transition是负责捕获布局的差异,并产生动画切换UI状态的组件。任何Transition的执行始终遵循以下步骤:

  1. 捕获启动状态
  2. 进行布局的变化
  3. 捕获结束状态
  4. 运行动画

该过程作为一个整体是由TransitionManager管理的,但大多数的上述步骤(除步骤2)通过transition表现。步骤2可能是一个场景转变或任意的布局变化。

如何工作

让我们通过一个最简单的方式引发一个transition,看看其中发生什么。这里有一个小的代码示例:

  1. TransitionManager.beginDelayedTransition(sceneRoot);
  2. View child = sceneRoot.findViewById(R.id.child);
  3. LayoutParams params = child.getLayoutParams();
  4. params.width = 150;
  5. params.height = 25;
  6. child.setLayoutParams(params);

这段代码触发一个AutoTransition,在给定场景根动画改变子view的大小。

TransitionManager要在beingDelayedTransition() 做的第一件事是检查在同一场景根中是否有等待延迟的transition,如果有一个,取出执行。这意味着只有第一次beingDelayedTransition()调用并伴随同样的渲染帧才生效。

接着,它会重复使用一个静态 AutoTransition 实例。你也可以通过一个扩展方法提供你自己的 transition。在任何情况下,在任何情况下,它会一直克隆那个给定的 transition 实例,

  1. final Transition transitionClone = transition.clone();
  1. @Override
  2. public Transition clone() {
  3. Transition clone = null;
  4. try {
  5. clone = (Transition) super.clone();
  6. clone.mAnimators = new ArrayList<Animator>();
  7. clone.mStartValues = new TransitionValuesMaps();
  8. clone.mEndValues = new TransitionValuesMaps();
  9. } catch (CloneNotSupportedException e) {}
  10. return clone;
  11. }

以确保一个新的开始,从而使你可以放心地在beingDelayedTransition()方法过程中重用 Transition 实例。

然后它会去捕捉启动状态。

  1. if (transition != null) {
  2. transition.captureValues(sceneRoot, true);
  3. }

如果您在transition中设置了目标视图的ID,它只会捕捉这些view的值。

  1. if (mTargetIds.size() > 0 || mTargets.size() > 0) {
  2. if (mTargetIds.size() > 0) {
  3. for (int i = 0; i < mTargetIds.size(); ++i) {
  4. int id = mTargetIds.get(i);
  5. View view = sceneRoot.findViewById(id);
  6. if (view != null) {
  7. TransitionValues values = new TransitionValues();
  8. values.view = view;
  9. if (start) {
  10. captureStartValues(values);
  11. } else {
  12. captureEndValues(values);
  13. }
  14. if (start) {
  15. mStartValues.viewValues.put(view, values);
  16. if (id >= 0) {
  17. mStartValues.idValues.put(id, values);
  18. }
  19. } else {
  20. mEndValues.viewValues.put(view, values);
  21. if (id >= 0) {
  22. mEndValues.idValues.put(id, values);
  23. }
  24. }
  25. }
  26. }
  27. }

否则,它会递归地捕捉场景根的下所有视图的启动状态。因此,在所有的transitions中我们都要设置目标视图,特别是如果我们的场景层次很深,包含了很多子view的情况下。

这里有一个有趣的细节:Transition中捕获状态的代码对于使用有稳定IDs的adapters的listview有特别的处理

  1. sceneChangeSetup(sceneRoot, transitionClone);

它会标记ListView的元素拥有过渡状态,以避免他们在过渡期间被回收。这意味着您可以在一个ListView中添加或删除项目时,很容易进行转换。更新您的适配器之前,只需要调用beginDelayedTransition(),AutoTransition会做剩余的事情,可以参考这个例子

参与transition的每一个view的状态都被存在TransitionValues实例中,本质上说,这个TransitionValues实例就是关联view的map,这是api的一部分,也许TransitionValues能被封装的更好点。

Transition子类在TransitionValues实例中填满了它们所需要的View的状态,例如,更改边界transition,会捕获view的边界(上下左右)和在屏幕中的位置。

  1. private void captureValues(TransitionValues values) {
  2. View view = values.view;
  3. values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
  4. view.getRight(), view.getBottom()));
  5. values.values.put(PROPNAME_PARENT, values.view.getParent());
  6. values.view.getLocationInWindow(tempLocation);
  7. values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
  8. values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
  9. }

一旦启动状态被捕获,beginDelayedTransition()将退出任何一个场景^code

  1. // Notify previous scene that it is being exited
  2. Scene previousScene = Scene.getCurrentScene(sceneRoot);
  3. if (previousScene != null) {
  4. previousScene.exit();
  5. }

设置当前场景为null^code(因为这不是一个场景切换)

  1. Scene.setCurrentScene(sceneRoot, null);

最后等待下一个渲染帧^code

TransitionManager通过添加一个OnPreDrawListener来等待下一个渲染帧^code

  1. observer.addOnPreDrawListener(listener);

这个OnPreDrawListener在所有的view被正确的测量和布局,准备好在屏幕上绘制时触发(步骤2)。换句话说,当OnPreDrawListener被触发时,所有的涉及这个transition的view都获得了他们的目标大小和布局位置。这意味着是时候捕获这些view的结束状态了(步骤3)^code

  1. transition.captureValues(sceneRoot, false);

逻辑同捕获开始状态一样。

有了所有view的起始和结束状态,transition现在有足够的数据来实现view动画^code

  1. transition.playTransition(sceneRoot);

它会先更新或取消任何正在运行的当前view的transition^code

  1. ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();

然后创建新的动画与新TransitionValues​​(步骤4)^code

  1. createAnimators(sceneRoot, mStartValues, mEndValues);

transition将在每个view动画展现结束状态之前“重置”其用户界面到其原始状态,即起始状态,这是唯一可能,因为这段代码在OnPreDrawListener中绘制下一渲染帧之前就执行了。

最后,动画发生器按照定义的顺序(一起或者有序)和逻辑在屏幕中展现^code

  1. protected void runAnimators() {
  2. if (DBG) {
  3. Log.d(LOG_TAG, "runAnimators() on " + this);
  4. }
  5. start();
  6. ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
  7. // Now start every Animator that was previously created for this transition
  8. for (Animator anim : mAnimators) {
  9. if (DBG) {
  10. Log.d(LOG_TAG, " anim: " + anim);
  11. }
  12. if (runningAnimators.containsKey(anim)) {
  13. start();
  14. runAnimator(anim, runningAnimators);
  15. }
  16. }
  17. mAnimators.clear();
  18. end();
  19. }

场景转换

切换场景的代码路径跟beginDelayedTransition()很相似,主要的不同就是布局的变化是如何发生的。

调用go()或者transitonTo()的区别仅在于如何得到他们的transition实例,前者只是用一个AutoTransition,后者将得到有TransitionManager定义的transition以及toScene,fromScene属性。

也许场景转换最相关的方面是,它们有效地更换场景根的内容。当一个新的场景被进入,它会删除所有的位于场景根的view,然后把自身加入场景根^code

  1. public void enter() {
  2. // Apply layout change, if any
  3. if (mLayoutId > 0 || mLayout != null) {
  4. // empty out parent container before adding to it
  5. getSceneRoot().removeAllViews();
  6. if (mLayoutId > 0) {
  7. LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
  8. } else {
  9. mSceneRoot.addView(mLayout);
  10. }
  11. }
  12. // Notify next scene that it is entering. Subclasses may override to configure scene.
  13. if (mEnterAction != null) {
  14. mEnterAction.run();
  15. }
  16. setCurrentScene(mSceneRoot, this);
  17. }

因此,当你切换到一个新的场景时,请确保你更新了所有保持视图引用的类成员(在你的Activity,Fragment,自定义视图等)。你还需要重建所有之前场景持有的动态状态。例如,如果你从云端加载图像到上一个场景的一个ImageView,你必须把这个状态转移到新场景来。

额外

这里有些关于某些特定的转换实现细节值得一提。
ChangeBounds Transition的动画很有意思,正如它的名字所暗示的,视图边界,但它是如何在渲染帧之间不触发布局而实现效果的呢?它动画展现触发了大小改变的视图框,但是每次 layout()的调用,视图框都会被重置。这使得transition变得不可靠。ChangeBounds通过当transition执行时抑制布局变化来避免上述问题。

  1. parent.suppressLayout(true);

结束语

统观Transition框架,架构其实挺简单。最复杂的地方就在于Transition子类处理布局变化和边缘情况。
在OnPreDrawListener之前或者之后捕获起始和结束状态的小技巧可以非常简单的被实现。这不像某些ViewGroup‘s supressLayout()之类的api有访问限制。
做一个快速的实验,我实现了一个Linearlayout动画展现布局变化,但是这只是简单的实现,不要在项目中使用。代码

翻译 @flyouting
2014 年 03月 18日
源地址:这里

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注