『Android 技能篇』优雅的转场动画之 Transition
字数统计:
2,453 字
|
阅读时长:
11 分
| 本文总阅读量: 次
前言 先直接上效果图:
相信大家在平常开发也会遇到类似的转场动画,如果想要要实现上图的效果有哪些方式呢?
首先分析一下转场过程,我们把起始 View
分别定义为 startView
和 endView
。startView
为常见的列表布局,左侧头像和右侧为文本介绍;endView
为详情页面,置顶的大图和详细的文本介绍。
不难发现,这些元素都是对应关系,只不过起始状态的基本属性不同:
头像 ,位置和大小以及 scaleType
发生变化
背景 ,颜色、位置和大小发生变化
名称 ,字体大小、颜色和位置发生变化
描述 ,字体大小和位置发生变化
对于此效果,有很多办法可以实现,综合其实现成本和预期效果进行最终选择,我能想到的大概有三种:
直接把上述的每个对象看做是独立个体,各自创建独立的动画对象,控制其执行和结束状态。
这种方式,无疑是最简单粗暴的,但是实现和维护起来都很困难,更不容易拓展
使用 MotionLayout ,不得不说很强大,是 Google 推崇的动画组件,基本不用编写 java
代码就可完成负责的手势和动画,后面有时间会介绍。
使用 Transition
,Google 在 Android 5.0 完整引入,虽没有 MotionLayout 那么强大,但是其复用性很强,并且很容易理解,上手也很快。
今天咱们就以下面三个方向并结合对应效果来带大家了解一下 Transition。
原生提供的 Transition
类
自己实现 Transition
类
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); }
最终效果:
当调用 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 ); }
最终效果:
这个就厉害了,View 的 translation
、scale
和 rotation
发生改变时都会触发:
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
的原理很简单,大致的逻辑如下:
记录当前状态的属性值,比如位置大小或者自定义属性之类
创建执行动画,参数为当前值和目标值,根据对应算法来完成动画效果
根据目标状态的属性值和记录的缓存属性值,调用创建好的动画对象执行即可
那落实到代码中,首先先集成 Transition
类,会让你实现三个方法:captureStartValues
、captureEndValues
和createAnimator
。
定义你关心的属性值 ;
官方建议属性定义的规则为:package_name:transition_class:property_name
.
比如
1 private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color" ;
我想在文本颜色发生改变时做转场动画,就可以定义上述的属性。
记录起始状态的属性;
1 2 void captureStartValues (TransitionValues transitionValues) void captureEndValues (TransitionValues transitionValues) ;
上述方法分别存储起始状态下对应的属性值:
1 transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
创建动画;
1 Animator createAnimator (ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
参数值的 startValues
和endValues
分别可以拿到你存储的属性值,之后创建动画并返回即可,后续系统会根据你创建的动画进行转场。
是不是很简单,接下来通过几个案例带大家感受一下:
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 有的时候发现,在切换图片的时候过度会很生硬,那可以通过在对 View
的 alpha
属性从 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
保持一致即可。
创建前后 layout
;layout_scene1.xml
和 layout_scene2.xml
具体代码就补贴了
创建前后 Scene
对象;
1 2 mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1, this ); mScene2 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene2, this );
创建转场 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()); } }
开始切换场景;
1 2 TransitionManager.go(mScene1, mTransition); TransitionManager.go(mScene2, mTransition);
总结 到此,先详细的和大家分享了系统自带的 Transition
,并分析了其实现细节和原理,提供了多个自定义 Transition
,接着了解了 Scene
创建过程,并通过简答的 demo
实现了从一个场景到另一个场景的过度效果,由浅入深,图文并茂,希望可以帮助到大家。
文中的项目已上传到 TransitionDemo 。