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

前言

先直接上效果图:

image

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

首先分析一下转场过程,我们把起始 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,内部集成了基础动画:

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,淡出 出场,淡入 入场

image

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

image

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

image

同样,可以通过:

Fade fade = new Fade();
Slide slide = new Slide();
TransitionSet set = new TransitionSet();
set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);

达到组合的效果:

image

ChangeBounds

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

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);

最终的效果:

image

ChangeClipBounds

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

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);
}

最终效果:

image

ChangeScroll

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

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);
}

最终效果:

image

ChangeTransform

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

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);
}

最终效果:

image

自定义 Transition

介绍

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

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

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

  1. 定义你关心的属性值

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

    比如

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

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

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

    void captureStartValues(TransitionValues transitionValues)
    void captureEndValues(TransitionValues transitionValues);
    
  上述方法分别存储起始状态下对应的属性值:

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

  ```java
  Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
参数值的 `startValues`和`endValues`分别可以拿到你存储的属性值,之后创建动画并返回即可,后续系统会根据你创建的动画进行转场。

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

ChangeTextTransition

ChangeTextTransition.java 该类中定义了:

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";

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

// 记录下起始状态属性值
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;
}

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

image

ChangeBackgroundColorTransition

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

@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;
}

最终效果:

image

ChangeImageResourceTransition

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

@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;

最终效果:

image

ChangeCustomTransition

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

创建 Animator 没什么区别:

@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 的绘制逻辑:

@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();
}

最终的效果:

image

Scene

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

image

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

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

  1. 创建前后 layoutlayout_scene1.xmllayout_scene2.xml 具体代码就补贴了
  2. 创建前后 Scene 对象;
    mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1, this);
    mScene2 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene2, this);
    
3. 创建转场 `Transition`;我们把之前自定的组合成 `TransitionSet`:
  ```java
  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());
    }
  }
  1. 开始切换场景;
    TransitionManager.go(mScene1, mTransition);
    TransitionManager.go(mScene2, mTransition);
    

总结

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

文中的项目已上传到 TransitionDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342