Android Transition (一)

Android Transition (一)


2017-09-21

Android 4.4.2 (API level 19) 引入 Transition 框架,之后很多 APP 上都使用该框架做出很酷炫的效果,如 Google Play Newsstand app

还有 github 上很火的 plaid

在 app 中适当得使用上 Transition 能带来较好的用户体验,视频 中介绍了该框架的基本使用以及其中核心的一些类和方法,只有学会这些基本的 API 才能在之后的 Activity/Fragment 过渡定制一些自己想要的效果。先看官网的一张关系图

图中有三个核心的类,分别是Scene、Transition和TransitionManager,下面对这个三个核心类展开分析。

Scene

Scene 场景,用于保存布局中所有 View 的属性值,创建 Scene 的方式可以通过 getSceneForLayout 方法getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)

比如:

1
2
mScene0 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene0, getContext());
mScene1 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene1, getContext());

也可以直接 new Scene(ViewGroup sceneRoot, View layout)

1
2
3
4
View view0 = inflater.inflate(R.layout.scene0, container, false);
View view1 = inflater.inflate(R.layout.scene1, container, false);
mScene0 = new Scene(mSceneRoot, view0);
mScene1 = new Scene(mSceneRoot, view1);

两种方式都需要传 SceneRoot ,即该场景的根节点。

Transition

Transition 过渡动画,前面创建了两个场景,分别保存了视图的一些属性,比如 Visibility、position 等,Transition 就是对于这些属性值的改变定义过渡的效果。从上图可以看到系统内置了一些常用的 Transition,Transition 的创建可以通过加载xml,如:

res/transition/fade_transition.xml

1
<fade xmlns:android="http://schemas.android.com/apk/res/android" />

然后在代码中:

1
2
3
Transition mFadeTransition =
TransitionInflater.from(this).
inflateTransition(R.transition.fade_transition);

或者直接在代码中:

1
Transition mFadeTransition = new Fade();

TransitionManager

TransitionManeger 用于将 Scene 和 Transition 联系起来,它提供了一系列的方法如 setTransition(Scene fromScene, Scene toScene, Transition transition) 指明起始场景和结束场景、他们的过渡动画是什么,go(Scene scene, Transition transition),到指定的场景所使用的过渡动画是什么,beginDelayedTransition(ViewGroup sceneRoot, Transition transition),在当前场景到下一帧的过渡效果是什么。比如这里使用 go() 方法,效果:

注意这里两个 Scene 中红绿两个方块除了位置和大小不一样,id 是一致的,transition 记录下两个 Scene 前后属性值,根据属性值的改变执行过渡动画,默认情况下对 SceneRoot 下的所有 View 执行动画效果,我们可以通过Transition.addTarget 和 removeTarget 方法选择性添加或移除执行动画的 View 。

常用 API

有时候我们只想改变当前已展示的视图层级中 View 的状态,可以通过 beginDelayedTransition 实现,下面列举系统内置的 Transition 的使用。

AutoTransition

AutoTransition 默认的动画效果,对应 xml tag 为autoTransition

其实是以下几个动画组合顺序执行:

1
2
3
4
5
6
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="sequential">
<fade android:fadingMode="fade_out" />
<changeBounds />
<fade android:fadingMode="fade_in" />
</transitionSet>

在代码中使用:

1
2
3
4
5
6
TransitionManager.beginDelayedTransition(mRoot, new AutoTransition());
if (mTextView.getVisibility() != View.VISIBLE) {
mTextView.setVisibility(View.VISIBLE);
} else {
mTextView.setVisibility(View.GONE);
}

ChangeBounds

ChangeBounds 对应 xml tag为changeBounds,根据前后布局界限的变化执行动画

1
2
3
4
5
6
7
8
TransitionManager.beginDelayedTransition(mRoot, new ChangeBounds());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
if ((lp.gravity & Gravity.START) == Gravity.START) {
lp.gravity = Gravity.BOTTOM | Gravity.END;
} else {
lp.gravity = Gravity.TOP | Gravity.START;
}
mTarget.setLayoutParams(lp);

ChangeClipBounds

ChangeClipBounds 对应 xml tag为changeClipBounds,作用对象:View 的 getClipBounds() 值

1
2
3
4
5
6
7
Rect BOUNDS = new Rect(20, 20, 100, 100);
TransitionManager.beginDelayedTransition(mRoot, new ChangeClipBounds());
if (BOUNDS.equals(ViewCompat.getClipBounds(mImageView))) {
ViewCompat.setClipBounds(mImageView, null);
} else {
ViewCompat.setClipBounds(mImageView, BOUNDS);
}

ChangeImageTransform

对应 xml tag为changeImageTransform,作用对象:ImageView 的 matrix

1
2
TransitionManager.beginDelayedTransition(mRoot, new ChangeImageTransform());
mImageView.setScaleType(ImageView.ScaleType.XXX);

ChangeScroll

对应 xml tag为changeScroll,作用对象:View 的 scroll 属性值

1
2
TransitionManager.beginDelayedTransition(mRoot, new ChangeScroll());
mTarget.scrollBy(-100, -100);

ChangeTransform

对应 xml tag 为changeTransform,作用对象:View 的 scale 和 rotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TransitionManager.beginDelayedTransition(mRoot, new ChangeTransform());
if (mContainer2.getChildCount() > 0) {
mContainer2.removeAllViews();
showRedSquare(mContainer1);
} else {
mContainer1.removeAllViews();
showRedSquare(mContainer2);
mContainer2.getChildAt(0).setRotation(45);
}

private void showRedSquare(FrameLayout container) {
final View view = LayoutInflater.from(getContext())
.inflate(R.layout.red_square, container, false);
container.addView(view);
}

Explode

对应 xml tag为explode,作用对象:View 的 Visibility

1
2
3
4
5
TransitionManager.beginDelayedTransition(mRoot, new Explode());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}

Fade

对应 xml tag为fade,作用对象:View 的 Visibility

可以在初始化时指定 IN 或者 OUT 分别对应淡入和淡出,也可以通过 fade.setMode 方法设置,若不指定默认为淡入淡出效果

1
2
3
4
5
TransitionManager.beginDelayedTransition(mRoot, new Fade());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}

Slide

对应 xml tag 为slide,作用对象:View 的 Visibility

可以初始化时传入 Gravity.XX,也可以通过 slide.setSlideEdge 方法设置,默认方向为 Gravity.BOTTOM

1
2
3
4
5
TransitionManager.beginDelayedTransition(mRoot, new Slide());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}

TransitionSet

对应 xml tag 为transitionSet

可以在代码中创建 transitionSet 如:

1
2
3
4
5
6
7
8
9
10
11
12
mTransition = new TransitionSet();
mTransition.addTransition(new ChangeImageTransform());
mTransition.addTransition(new ChangeTransform());
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
if (mInnerFrame.getChildCount() > 0) {
mInnerFrame.removeAllViews();
addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
} else {
mOuterFrame.removeViewAt(1);
addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
FrameLayout.LayoutParams.MATCH_PARENT);
}

也可以通过加载 xml 布局创建 transitionSet :

xml 布局长这样:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together">
<changeImageTransform/>
<changeTransform/>
</transitionSet>

通过 transitionOrdering 属性设置动画执行的顺序,together 表示同时执行,sequential 表示顺序执行,在代码中可以调用 TransitionSet 的 setOrdering(int) 方法,属性值传 ORDERING_SEQUENTIAL 或者 ORDERING_TOGETHER

在代码中:

1
2
3
4
5
6
7
8
9
10
11
mTransition = (TransitionSet) TransitionInflater.from(getContext())
.inflateTransition(R.transition.transition);
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
if (mInnerFrame.getChildCount() > 0) {
mInnerFrame.removeAllViews();
addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
} else {
mOuterFrame.removeViewAt(1);
addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
FrameLayout.LayoutParams.MATCH_PARENT);
}

这里结合 changeImageTransform 和 changeTransform ,效果如下:

PathMotion

Transition 的辅助工具,以 path 的方式指定过渡效果,两个具体实现类 ArcMotion 和 PatternPathMotion,看下ArcMotion 的效果

1
2
3
4
5
6
7
8
9
10
mTransition = new AutoTransition();
mTransition.setPathMotion(new ArcMotion());
TransitionManager.beginDelayedTransition(mRoot, mTransition);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
if ((lp.gravity & Gravity.START) == Gravity.START) {
lp.gravity = Gravity.END | Gravity.BOTTOM;
} else {
lp.gravity = Gravity.START | Gravity.TOP;
}
mTarget.setLayoutParams(lp);

自定义 Transition

除了系统内置的 Transition,我们还可以自定义 Transition 效果,需要继承 Transition

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomTransition extends Transition {
@Override
public void captureStartValues(TransitionValues values) {}

@Override
public void captureEndValues(TransitionValues values) {}

@Override
public Animator createAnimator(ViewGroup sceneRoot,
TransitionValues startValues,
TransitionValues endValues) {}
}

其工作原理是在 captureStartValues 和 captureEndValues 中分别记录 View 的属性值,官网建议确保属性值不冲突,属性值的命名格式参考:

1
package_name:transition_name:property_name

在 createAnimator 中创建动画,对比属性值的改变执行动画效果,如自定义修改颜色动画效果:

在两个 Scene 中使用自定义过渡动画,效果如下:

Note

  1. Android 版本在 4.0(API Level 14) 到 4.4.2(API Level 19) 使用 Android Support Library’s

  2. 对于 SurfaceView 可能不起效果,因为 SurfaceView 的实例是在非 UI 线程更新的,因此会造成和其他视图动画不同步。

  3. 某些特定的转换类型在应用到 TextureView 时可能不会产生所需的动画效果。

  4. 继承自 AdapterView 的如 ListView,与该框架不兼容。

  5. 不要对包含文本的视图的大小进行动画

Thanks to

  1. Google Demo

  2. Github Demo传送门