『Android 技能篇』优雅的转场动画之 Transition

  |     |   本文总阅读量:

前言

先直接上效果图:

相信大家在平常开发也会遇到类似的转场动画,如果想要要实现上图的效果有哪些方式呢?

首先分析一下转场过程,我们把起始 View 分别定义为 startViewendViewstartView 为常见的列表布局,左侧头像和右侧为文本介绍;endView 为详情页面,置顶的大图和详细的文本介绍。

不难发现,这些元素都是对应关系,只不过起始状态的基本属性不同:

  • 头像,位置和大小以及 scaleType 发生变化
  • 背景,颜色、位置和大小发生变化
  • 名称,字体大小、颜色和位置发生变化
  • 描述,字体大小和位置发生变化

对于此效果,有很多办法可以实现,综合其实现成本和预期效果进行最终选择,我能想到的大概有三种:

  1. 直接把上述的每个对象看做是独立个体,各自创建独立的动画对象,控制其执行和结束状态。

    这种方式,无疑是最简单粗暴的,但是实现和维护起来都很困难,更不容易拓展

  2. 使用 MotionLayout,不得不说很强大,是 Google 推崇的动画组件,基本不用编写 java 代码就可完成负责的手势和动画,后面有时间会介绍。

  3. 使用 Transition,Google 在 Android 5.0 完整引入,虽没有 MotionLayout 那么强大,但是其复用性很强,并且很容易理解,上手也很快。

今天咱们就以下面三个方向并结合对应效果来带大家了解一下 Transition。

  1. 原生提供的 Transition
  2. 自己实现 Transition
  3. Scene

原生 Transition

准备

核心关键类 TransitionManager, TransitionManager.beginDelayedTransition(ViewGroup viewGroup, Transition transition); 作为动画的开始,传入需要做转场动画的父布局或根布局,随后改变 View 的相关属性,比如 setVisible(),便可自动完成转场动画效果。

默认实现的 AutoTransition,内部集成了基础动画:

1
2
3
4
5
6
private void init() {
setOrdering(ORDERING_SEQUENTIAL);
addTransition(new Fade(Fade.OUT)).
addTransition(new ChangeBounds()).
addTransition(new Fade(Fade.IN));
}

Slide、Fade 和 Explode

这三者作为 Visibility 的三个子类,通过控制 view.setVisible() 的方式来达到具体的效果。

Fade,淡出 出场,淡入 入场

Slide,向下离开屏幕出场,向上进入屏幕入场

Explode,四边散开出场,四边汇入入场

同样,可以通过:

1
2
3
4
Fade fade = new Fade();
Slide slide = new Slide();
TransitionSet set = new TransitionSet();
set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);

达到组合的效果:

ChangeBounds

此处开始同一个页面场景的切换,ChangeBounds 当 View 的位置或者大小发生变化时触发对应的转场效果。比如:

1
2
3
4
5
6
7
8
9
10
ChangeBounds transition = new ChangeBounds();
transition.setInterpolator(new AnticipateInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view3.getLayoutParams();
if (layoutParams.leftMargin == 400) {
layoutParams.leftMargin = 50;
} else {
layoutParams.leftMargin = 400;
}
view3.setLayoutParams(layoutParams);

最终的效果:

ChangeClipBounds

当调用 view.setClipBounds() 时会触发转场效果:

1
2
3
4
5
6
7
8
9
10
11
12
ChangeClipBounds transition = new ChangeClipBounds();
transition.setInterpolator(new BounceInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
int width = view2.getWidth();
int height = view2.getHeight();
int gap = 140;
Rect rect = new Rect(0, gap, width, height - gap);
if (rect.equals(view2.getClipBounds())) {
view2.setClipBounds(null);
} else {
view2.setClipBounds(rect);
}

最终效果:

ChangeScroll

当调用 view.scrollTo() 会触发转场效果:

1
2
3
4
5
6
7
8
ChangeScroll transition = new ChangeScroll();
transition.setInterpolator(new AnticipateOvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getScrollX() == -100 && view1.getScrollY() == -100) {
view1.scrollTo(0, 0);
} else {
view1.scrollTo(-100, -100);
}

最终效果:

ChangeTransform

这个就厉害了,View 的 translationscalerotation 发生改变时都会触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ChangeTransform transition = new ChangeTransform();
transition.setInterpolator(new OvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getTranslationX() == 100 && view1.getTranslationY() == 100) {
view1.setTranslationX(0);
view1.setTranslationY(0);
} else {
view1.setTranslationX(100);
view1.setTranslationY(100);
}
if (view2.getRotationX() == 30f) {
view2.setRotationX(0);
} else {
view2.setRotationX(30);
}
if (view3.getRotationY() == 30f) {
view3.setRotationY(0);
} else {
view3.setRotationY(30);
}
if (view4.getScaleX() == 0.5f && view4.getScaleY() == 0.5f) {
view4.setScaleX(1f);
view4.setScaleY(1f);
} else {
view4.setScaleX(0.5f);
view4.setScaleY(0.5f);
}

最终效果:

自定义 Transition

介绍

其实 Transition 的原理很简单,大致的逻辑如下:

  1. 记录当前状态的属性值,比如位置大小或者自定义属性之类
  2. 创建执行动画,参数为当前值和目标值,根据对应算法来完成动画效果
  3. 根据目标状态的属性值和记录的缓存属性值,调用创建好的动画对象执行即可

那落实到代码中,首先先集成 Transition 类,会让你实现三个方法:captureStartValuescaptureEndValuescreateAnimator

  1. 定义你关心的属性值

    官方建议属性定义的规则为:package_name:transition_class:property_name.

    比如

    1
    private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color";

    我想在文本颜色发生改变时做转场动画,就可以定义上述的属性。

  2. 记录起始状态的属性;

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

    上述方法分别存储起始状态下对应的属性值:

    1
    transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
  3. 创建动画;

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

    参数值的 startValuesendValues分别可以拿到你存储的属性值,之后创建动画并返回即可,后续系统会根据你创建的动画进行转场。

是不是很简单,接下来通过几个案例带大家感受一下:

ChangeTextTransition

ChangeTextTransition.java 该类中定义了:

1
2
3
4
private static String PROPNAME_TEXT = "xiaweizi:changeText:text";
private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color";
private static String PROPNAME_TEXT_SIZE = "xiaweizi:changeTextSize:size";
private static String PROPNAME_TEXT_LEVEL = "xiaweizi:changeTextTypeface:level";

分别代表文本内容变化、文本颜色变化、文本大小变化和文本字体变化。我们只挑一个文本颜色来看一下动画是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 记录下起始状态属性值
private void captureValues(TransitionValues transitionValues) {
if (transitionValues == null || !(transitionValues.view instanceof TextView)) return;
TextView view = (TextView) transitionValues.view;
transitionValues.values.put(PROPNAME_TEXT, view.getText());
transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
transitionValues.values.put(PROPNAME_TEXT_SIZE, view.getTextSize());
transitionValues.values.put(PROPNAME_TEXT_LEVEL, view.getTag(R.id.type_face_level));
}

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
if (!(endValues.view instanceof TextView)) {
return super.createAnimator(sceneRoot, startValues, endValues);
}
TextView endView = (TextView) endValues.view;
int startTextColor = (int) startValues.values.get(PROPNAME_TEXT_COLOR);
int endTextColor = (int) endValues.values.get(PROPNAME_TEXT_COLOR);
ObjectAnimator animator = ObjectAnimator.ofArgb(endView, new TextColorProperty(), startTextColor, endTextColor);
animator.setDuration(300);
return animator;
}

看一下这四种属性发生变化时的效果:

ChangeBackgroundColorTransition

类似于文本颜色,只不过针对的是 view.setBackground(),主要的代码在于创建 Animator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
final View endView = endValues.view;
ColorDrawable startColorDrawable = (ColorDrawable) startValues.values.get(PROPNAME_COLOR);
ColorDrawable endColorDrawable = (ColorDrawable) endValues.values.get(PROPNAME_COLOR);
if (startColorDrawable == null || endColorDrawable == null) return super.createAnimator(sceneRoot, startValues, endValues);
final int startColor = startColorDrawable.getColor();
final int endColor = endColorDrawable.getColor();
ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
animator.setDuration(300);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
endView.setBackgroundColor(animatedValue);
}
});
return animator;
}

最终效果:

ChangeImageResourceTransition

有的时候发现,在切换图片的时候过度会很生硬,那可以通过在对 Viewalpha 属性从 1~0~1 的过程中替换图片,这样显得很平滑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
if (!(endValues.view instanceof ImageView)) {
return super.createAnimator(sceneRoot, startValues, endValues);
}
final ImageView endView = (ImageView) endValues.view;
final Drawable startDrawable = (Drawable) startValues.values.get(PROPNAME_IMAGE_RESOURCE);
final Drawable endDrawable = (Drawable) endValues.values.get(PROPNAME_IMAGE_RESOURCE);
ValueAnimator animator = ValueAnimator.ofFloat(0, 1f);
animator.setDuration(300);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
if (animatedValue <= 0.5f) {
endView.setImageDrawable(startDrawable);
float ratio = (0.5f - animatedValue) / 0.5f;
endView.setAlpha(ratio);
} else {
endView.setImageDrawable(endDrawable);
float ratio = (animatedValue - 0.5f) / 0.5f;
endView.setAlpha(ratio);
}
}
});
return animator;

最终效果:

ChangeCustomTransition

除了 View 原生的属性,自定义属性同样也可以。

创建 Animator 没什么区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
if (!(endValues.view instanceof TransitionView)) {
return super.createAnimator(sceneRoot, startValues, endValues);
}
final TransitionView endView = (TransitionView) endValues.view;
final float startRatio = (float) startValues.values.get(PROPNAME_CUSTOM_RATIO);
final float endRatio = (float) endValues.values.get(PROPNAME_CUSTOM_RATIO);
ObjectAnimator animator = ObjectAnimator.ofFloat(endView, "ratio", startRatio, endRatio);
animator.setDuration(300);
return animator;
}

主要在自定义 View 的绘制逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制左边
canvas.save();
mRect.set(0, 0, (int) (getWidth() * mRatio), getHeight());
canvas.clipRect(mRect);
mTextPaint.setColor(mStartColor);
TransitionUtils.drawTextCenter(canvas, "文本三", getWidth() / 2, getHeight() / 2, mTextPaint);
canvas.restore();

// 绘制右边
canvas.save();
mRect.set((int) (getWidth() * mRatio), 0, getWidth(), getHeight());
canvas.clipRect(mRect);
mTextPaint.setColor(mEndColor);
TransitionUtils.drawTextCenter(canvas, "三本文", getWidth() / 2, getHeight() / 2, mTextPaint);
canvas.restore();
}

最终的效果:

Scene

终于开始介绍文章开头的效果是如何实现的:

有了前面的基础铺垫,实现起来就很简单。

Scene 就是为这种场景的过度而设计,不需要关注过度过程,只需要传入前后的布局,并保证各个元素的 id 保持一致即可。

  1. 创建前后 layoutlayout_scene1.xmllayout_scene2.xml 具体代码就补贴了
  2. 创建前后 Scene 对象;

    1
    2
    mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1, this);
    mScene2 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene2, this);
  3. 创建转场 Transition;我们把之前自定的组合成 TransitionSet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
       public class SceneTransition extends TransitionSet {
    public SceneTransition() {
    init();
    }
    public SceneTransition(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
    }
    private void init() {
    addTransition(new ChangeTextTransition())
    .addTransition(new ChangeScroll())
    .addTransition(new ChangeBackgroundColorTransition())
    .addTransition(new ChangeBounds());
    }
    }
  4. 开始切换场景;

    1
    2
    TransitionManager.go(mScene1, mTransition);
    TransitionManager.go(mScene2, mTransition);

总结

到此,先详细的和大家分享了系统自带的 Transition,并分析了其实现细节和原理,提供了多个自定义 Transition,接着了解了 Scene 创建过程,并通过简答的 demo 实现了从一个场景到另一个场景的过度效果,由浅入深,图文并茂,希望可以帮助到大家。

文中的项目已上传到 TransitionDemo

#rewardButton { background-color: #ea6f5a; } .btn-pay { margin-bottom: 20px; padding: 8px 25px; font-size: 16px; color: #fff; background-color: #ea6f5a; } .btn { display: inline-block; margin-bottom: 0; font-weight: 400; text-align: center; vertical-align: middle; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; padding: 6px 12px; font-size: 14px; line-height: 1.42857; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #QR img{ height: 200px; height: 200px; margin: 20px; }
文章目录
  1. 1. 前言
  2. 2. 原生 Transition
    1. 2.1. 准备
    2. 2.2. Slide、Fade 和 Explode
    3. 2.3. ChangeBounds
    4. 2.4. ChangeClipBounds
    5. 2.5. ChangeScroll
    6. 2.6. ChangeTransform
  3. 3. 自定义 Transition
    1. 3.1. 介绍
    2. 3.2. ChangeTextTransition
    3. 3.3. ChangeBackgroundColorTransition
    4. 3.4. ChangeImageResourceTransition
    5. 3.5. ChangeCustomTransition
  4. 4. Scene
  5. 5. 总结
您是第 位小伙伴 | 本站总访问量 | 已经写了 120.4k 字啦

载入天数...载入时分秒...