0%

动画二:布局容器动画(Transition)

布局容器动画:android.transition

Transition 是布局容器发生变化(包括其内部的控件发生变化:如添加控件,某个控件改变颜色)时应用的一种动画。

Transition 的简单调用如下

1
TransitionManager.beginDelayedTransition(sceneRoot);//默认采用A utoTransition

也可以使用 Scene

1
2
Scene scene = new Scene(sceneRoot, textView);
TransitionManager.go(scene, new Fade(Fade.IN));

此时实际并不能执行动画,只有当布局发生变化时才会执行,且一次设置只能执行一次动画发生的准确时间是在onPreDraw方法中。

Scene

首先来看Scene类,Scene 定义如下,它需要一个已经存在的容器(mSceneRoot),和一个新的控件或布局(mLayoutId或mLayout)。

1
2
3
4
5
6
7
public final class Scene {
private Context mContext;
private int mLayoutId = -1; //待加入到根布局中的布局,必须没有父控件
private ViewGroup mSceneRoot; //根布局,必须已经存在
private View mLayout; // alternative to layoutId
Runnable mEnterAction, mExitAction;
}

进入场景(Scene )的执行方法 enter 如下,实际是将容器的控件清空,添加新控件并将Scene对象自身存储进容器中。

1
2
3
4
5
6
7
8
9
public void enter() {
getSceneRoot().removeAllViews();

mSceneRoot.addView(mLayout);
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
mEnterAction.run();

setCurrentScene(mSceneRoot, this);
}

根布局对象 sceneRoot 中的 Tag 对象是一个SparseArray集合,用 layoutId 作为键 新创建的 Scene 对象存储进去,反过来也能从中获取Scene对象。

1
2
3
static Scene getCurrentScene(View view) {
return (Scene) view.getTag(com.android.internal.R.id.current_scene);
}

这样原本的根布局就埋下了所需的条件,当时机成熟时将被调用,当Transition动画播放完成后将被清除

TransitionManager

TransitionManager 是使用 Transition 的调度器,它将 Scene 和 Transition 匹配到一起,这样当某个 Scene 进入时,可以执行对应的 Transition。

1
2
3
4
5
6
7
public class TransitionManager {
private static Transition sDefaultTransition = new AutoTransition();
ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>();
ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions =
new ArrayMap<Scene, ArrayMap<Scene, Transition>>();
private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>();
}

也可以指定离开某Scene时的 Transition 集合,由集合mScenePairTransitions保存,其 key 是进入场景 toScene。

1
2
3
4
public void setTransition(Scene fromScene, Scene toScene, Transition transition) {
ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
sceneTransitionMap.put(fromScene, transition);
}

虽然 TransitionManager 保存了 Scene 和 Transition 的对应关系,但如何根据Scene获取Transition 还与所配置的 ViewGroup有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//这里Scene表示进入场景,而 sceneRoot 中存储的是当前场景,即将要离开的场景,在播放完成后要被删除;
//因此先获取的是离开场景,离开Transition播放完成后将获取进入场景。
private Transition getTransition(Scene scene) {
Transition transition = null;
ViewGroup sceneRoot = scene.getSceneRoot();
if (sceneRoot != null) {
Scene currScene = Scene.getCurrentScene(sceneRoot);
if (currScene != null) {
ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(scene);
if (sceneTransitionMap != null) {
transition = sceneTransitionMap.get(currScene);
if (transition != null) {
return transition;
}
}
}
}
transition = mSceneTransitions.get(scene);
return (transition != null) ? transition : sDefaultTransition;
}

TransitionManager 调用动画的执行过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.准备工作1:清理先前的 Transition,包括 TransitionManager 和 sceneRoot 内部存储的两部分
ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
for (Transition runningTransition : runningTransitions) {
runningTransition.pause(sceneRoot);
}
Scene previousScene = Scene.getCurrentScene(sceneRoot);
previousScene.exit();
//2.准备工作2:新的 Transition 获取初始值,并清理掉 sceneRoot内部存储的 Scene 对象
transition.captureValues(sceneRoot, true);
Scene.setCurrentScene(sceneRoot, null);
//3.在视图树中埋点
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
mTransition.captureValues(mSceneRoot, false);
mTransition.playTransition(mSceneRoot);

当视图树发生变化时将执行 OnPreDraw 方法,此时就是执行动画的时机

1
2
3
4
5
6
7
//1.清理监听器和 Transition 集合
removeListeners();
sPendingTransitions.remove(mSceneRoot);
//2.此时布局过程已经完成,绘制还没有开始,是 Transition 获取终止值的绝佳时机。
mTransition.captureValues(mSceneRoot, false);
//3.起始值均已捕获,执行动画的条件已经成熟。
mTransition.playTransition(mSceneRoot);

这里 Transition 的执行实际是放在ViewTreeObserver.OnPreDrawListener接口中进行的,以便容器 mSceneRoot 重绘前调用,在播放完成后还要进行对应清理工作。

Transition

Transition 动画的数据结构是 TransitionValues,包含了待变化的属性字典

1
2
3
4
5
public class TransitionValues {
public View view;
public final Map<String, Object> values = new ArrayMap<String, Object>();
final ArrayList<Transition> targetedTransitions = new ArrayList<Transition>();
}

Transition 中的核心就是两个 TransitionValues 集合,表示动画的起始值。

1
2
ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts

在 playTransition 方法中,首先要构建这两个集合

1
2
public abstract void captureStartValues(TransitionValues transitionValues);
public abstract void captureEndValues(TransitionValues transitionValues);

而后是遍历 sceneRoot,为每个 TransitionValues 对创建属性动画。

1
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,TransitionValues endValues)

Transition 的子类要定义如何获取起始值以及如何创建属性动画上,这都与具体的属性有关。

下面通过子类实例说说如何定义 Transition。

Visibility

Visibility 类能够响应控件可见性的变化, 这与他定义的三个属性有关,这些属性的数值完全由 View 提供,其名称和类型为

1
2
3
4
5
6
//1.表示 View 的可见性,int类型
static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
//2.表示 View 的 Parent,View类型
static final String PROPNAME_PARENT = "android:visibility:parent";
//3.表示 View 的屏幕位置,int[]类型
static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";

这样在获取属性时,就可以将这些属性值写入属性字典中去

1
2
3
4
5
6
7
8
private void captureValues(TransitionValues transitionValues, int forcedVisibility) {
int visibility = transitionValues.view.getVisibility();
transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
int[] loc = new int[2];
transitionValues.view.getLocationOnScreen(loc);
transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
}

创建动画的方法委托给了子类,以 Fade 为例,它添加了一个透明度属性(由view.getAlpha()提供)

1
static final String PROPNAME_ALPHA = "fade:alpha";

创建关于 View透明度的属性动画,变化区间从当前透明度到 1。

1
2
3
4
5
6
@Override
public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
TransitionValues endValues) {
float startAlpha = getStartAlpha(startValues, 0);
return ObjectAnimator.ofFloat(view, View.ALPHA, startAlpha, 1);
}

同理子类 Scale 创建了关于 scaleX 和 scaleY 的属性动画,子类 Slide 创建了关于 TRANSLATION_X 和 TRANSLATION_Y的属性动画,在确立起始点时要考虑屏幕位置,它使用的属性动画要建立 Path,通过改变Path可以改变滑动的轨迹

1
ObjectAnimator.ofFloat(view, View.TRANSLATION_X, View.TRANSLATION_Y, path);

其他Transition

类 ChangeBounds 将改变 View 的 Bound 属性,同时也要处理父容器的 Bound 改变。它有如下属性

1
2
3
4
5
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";
  • ChangeScroll : scrollX/scrollY 属性
  • ChangeTransform : AnimationMatrix 属性
  • ChangeClipBounds : clipBounds 属性
  • ChangeImageTransform :ImageView 的 animateTransform 属性

TransitionSet

装饰器 TransitionSet 代表着 Transition 集合,并能够为其指定播放顺序。默认实现 AutoTransition 采用顺序播放,包括三个 Transition,即 Fade.OUT,ChangeBounds,Fade.IN。

单个 Transition 可以添加或排除某个 View,排除的规则可以使用 id,transitionname字符串,class等手段。

二维路径插值器(PathMotion)

PathMotion 负责在起始点之间进行插值,它返回的是一个路径。

1
public abstract Path getPath(float startX, float startY, float endX, float endY);

某些改变位置的 Transition 可以使用,如ChangeBounds

1
new ChangeBounds().setPathMotion(new ArcMotion())

Transition 的实践

活动间的转场动画

使用 ActivityOptions可以得到活动间的转场动画,这里要通过 transitionName 属性来指定采用动画的View。

1
2
3
String transitionName = context.getResources().getString(R.string.transition_app_icon);
ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(activity, appIcon, transitionName);
context.startActivity(intent, transitionActivityOptions.toBundle());

1.执行这个动画需要设置 Window.FEATURE_ACTIVITY_TRANSITIONS。

ActivityOptions 创建的各种动画只是记录参数到 Bundle 中去,对于 SceneTransition 主要记录 View 和 String(transitionName) 的对应关系,以便于选取 Transition。

2.使用转场动画时,可以使用下列的一对方法来控制转场动画。

1
2
3
void postponeEnterTransition(); //使得动画阻塞
//Thread.sleep(3000); //可以利用这段时间做准备工作,进行数据的加载
void startPostponedEnterTransition(); //启动动画,否则动画将一直阻塞下去,新Activit无法启动

3.更简单的使用场景动画的方式是

1
2
startActivity(intent);
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);

overridePendingTransition 分别决定了起始页和跳转页使用的属性动画。

这一点同样可以用在离开时

1
2
3
4
5
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(R.anim.slide_out_right, R.anim.slide_in_left);
}

4.使用 Transition 监听器

活动的 Transition 是设置在 Window 上的,采用转场动画应该尽量在动画播放完成后再加载数据,避免处理控件(如RecyclerView)的滑动等。

1
getWindow().getEnterTransition().addListener(this);

自定义Transition

自定义Transition的核心是要改变容器布局中哪些 View 的哪些属性,为此需要以下步骤来确定(以改变TextView的字体大小为例)

1.确定属性名称

1
public static final String PRO_NAME_TEXT_SIZE = "text:size";

2.如何获取起始值TransitionValues

1
2
3
4
5
6
7
@Override
public void captureStartValues(TransitionValues transitionValues) {
if(transitionValues.view instanceof TextView){
TextView tv = (TextView) transitionValues.view;
transitionValues.values.put(PRO_NAME_TEXT_SIZE, tv.getTextSize());
}
}

3.创建关于字体大小的动画

1
2
3
4
5
6
7
8
9
10
11
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if(endValues.view instanceof TextView){
TextView tv = (TextView) endValues.view;
float start = ((TextView) startValues.view).getTextSize(); //初始值
float end = ((TextView) endValues.view).getTextSize(); //终止值
PropertyValuesHolder holder = PropertyValuesHolder.ofFloat(PROPERTY_TEXT_SIZE, start, end);
return ObjectAnimator.ofPropertyValuesHolder(tv, holder);
}
return null;
}

这样当设置字体时就会播放动画。

TransitionPlayer

TransitionPlayer 中有一个精彩的引导页示例, 也是 Transition 动画的使用范例。

Material-Animations

StarWars.Android

img