舒服了~玩转Android转场动画!

先直接上效果图:

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

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

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

  • 头像,位置和大小以及 scaleType 发生变化

  • 背景,颜色、位置和大小发生变化

  • 名称,字体大小、颜色和位置发生变化

  • 描述,字体大小和位置发生变化

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

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

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

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

  4. 使用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,淡出 出场,淡入 入场

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

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

同样,可以通过:

Fade fade =  new Fade();

Slide slide =  new Slide();

TransitionSet set =  new TransitionSet();

set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);

达到组合的效果:

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

最终的效果:

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

}

最终效果:

ChangeScroll

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

ChangeScroll transition =  new ChangeScroll();

transition.setInterpolator( new AnticipateOvershootInterpolator());

TransitionManager.beginDelayedTransition(mRoot, transition);

if (view1.getScrollX() == – 100 && view1.getScrollY() == – 100 ) {

view1.scrollTo( 00 );

else {

view1.scrollTo(- 100 , – 100 );

}

最终效果:

ChangeTransform

这个就厉害了,View 的 translation、scale 和 rotation 发生改变时都会触发:

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 类,会让你实现三个方法:captureStartValues、captureEndValues和createAnimator。

定义你关心的属性值

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

比如

private static String PROPNAME_TEXT_COLOR =  “xiaweizi:changeTextColor:color” ;

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

记录起始状态的属性

void captureStartValues (TransitionValues transitionValues)

void   captureEndValues

(TransitionValues transitionValues)

;

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

transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());

创建动画

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

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

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

ChangeTextTransition

ChangeTextTransition.java地址如下所示:

https://github.com/xiaweizi/TransitionDemo/blob/master/app/src/main/java/com/xiaweizi/transitiondemo/transition/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

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

}

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

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 ==  nullreturn 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

有的时候发现,在切换图片的时候过度会很生硬,那可以通过在对 View 的 alpha 属性从 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( 01f );

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 没什么区别:

@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( 00 , ( 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.创建前后 layout;layout_scene1.xml 和 layout_scene2.xml 具体代码就补贴了。

2.创建前后 Scene 对象:

mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1,  this );

this

);

3.创建转场 Transition;我们把之前自定的组合成 TransitionSet:

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.开始切换场景:

TransitionManager.go(mScene1, mTransition);

TransitionManager.go(mScene2, mTransition);

总结

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

文中的项目已上传到github上,地址如下所示:

https://github.com/xiaweizi/TransitionDemo

·················END·················

你好,我是刘望舒, 腾讯云TVP国内Android领域第一人,著有畅销书《Android进阶之光》《Android进阶解密》《Android进阶指北》,电子工业出版社2017、2018、2019、2020年度优秀作者。

前华为面试官,现大厂技术负责人。

欢迎添加我的微信 henglimogan  ,备注:BATcoder,加入BATcoder交流群。

明天见(。・ω・。)