1、前言

文章来源:https://juejin.im/post/6856743286653386760

昨晚跟往常一样,饭后开了一局王者荣耀,前中期基本焦灼,到了后期一波决定胜负的时候,我果断射箭,射中对面,配合队友直接秒杀,打赢团战一波推完基地。那叫一个精彩,队友都发出了666666的称赞,我酷酷的点了一下抱拳:多谢!嘿嘿。

赛后,手机上正在展示我的MVP动画,我不禁思考,这么精彩的团战我怎么能不记录下来?刚好最近了解到MotionLayout库,就用它实现吧。

动画效果

王者荣耀建模优化方案(使用MotionLayout实现王者荣耀团战来袭)(1)

2、功能详解

MotionLayout 是一种布局类型,可帮助您管理应用中的运动和微件动画。MotionLayout 是 ConstraintLayout 的子类,在其丰富的布局功能基础之上构建而成。

如上述介绍,MotionLayout是 ConstraintLayout的子类,相当于加了动画功能的ConstraintLayout。MotionLayout作为一个动画控件的好处就在于基本不用写java代码,全部在xml文件中搞定。而且我们只需要设定起始位置,结束位置以及一些中间状态,就能自动生成动画。

先分析下我们的团战,主要分为三个场景:

场景一

包含控件:后羿,亚瑟,鲁班,后羿的箭

动画描述:走位的亚瑟,后羿射箭

首先在布局文件中,添加第一个MotionLayout,并添加上所有的控件,后羿和鲁班由于是静止状态,所以要写上位置约束,其他包含动画的控件可以暂时不用写位置约束:

<androidx.constraintlayout.motion.widget.MotionLayout android:id="@ id/motionLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_01" app:showPaths="false" tools:showPaths="true"> <ImageView android:id="@ id/houyi" android:layout_width="66dp" android:layout_height="66dp" android:layout_marginLeft="180dp" android:src="@drawable/houyi_model" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.8" /> <ImageView android:id="@ id/houyi_arrow" android:layout_width="66dp" android:layout_height="66dp" android:src="@drawable/arrow" /> <ImageView android:id="@ id/yase" android:layout_width="66dp" android:layout_height="66dp" android:src="@drawable/yase_model" /> <ImageView android:id="@ id/luban" android:layout_width="66dp" android:layout_height="66dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.58" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.26" android:src="@drawable/luban_model" /> </androidx.constraintlayout.motion.widget.MotionLayout>

由于MotionLayout继承自ConstraintLayout,所以可以用ConstraintLayout的属性。这里可以看到有两个新的属性:

后羿射箭

接下来就可以写动画场景(MotionScene)了,新建res-xml-scene_01.xml。

<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <Transition app:constraintSetEnd="@ id/end" app:constraintSetStart="@ id/start" app:duration="2000"> </Transition> <ConstraintSet android:id="@ id/start"> <Constraint android:id="@ id/houyi_arrow" android:layout_width="66dp" android:layout_height="66dp" android:layout_marginLeft="190dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.8"> </Constraint> </ConstraintSet> <ConstraintSet android:id="@ id/end"> <Constraint android:id="@ id/houyi_arrow" android:layout_width="66dp" android:layout_height="66dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.65" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.35"> </Constraint> </ConstraintSet> </MotionScene>

可以看到,MotionScene有两个主要的标签Transition和ConstraintSet

其中Constraint属性指定了端点位置中某一个元素的位置和属性:

<Constraint android:id="@ id/button" ...> <CustomAttribute motion:attributeName="backgroundColor" motion:customColorValue="#D81B60"/> </Constraint>

attributeName属性就是与具有getter和setter方法的对象匹配,比如这里的backgroundColor就对应了view本身的基本方法getBackgroundColor() 和 setBackgroundColor()。

好了,回到后羿这边,由于后羿的箭是从后羿位置到亚瑟位置,所以我们设定好后羿箭的两个端点状态,配置好后,MotionLayout就会自动帮我们生成从起始状态到结束状态的动画了,后羿箭从后羿位置飞到了亚瑟位置。

等等,运行怎么没反应呢?动画怎么触发啊?

Motion提供了三动画触发方法:1)onClick标签,表示点击场景中的某个控件来触发动画效果。其中有两个属性。

2)OnSwipe标签,表示通过用户轻触控制动画,有点手势滑动的感觉

3)java代码控制.

这里我们就设置点击后羿触发动画:

<OnClick app:clickAction="toggle" app:targetId="@id/houyi" />

好了,运行,点击后羿,后羿的箭成功射出去了。

但是这还不够,后羿箭到亚瑟位置肯定就会消失了,怎么表示这个消失呢?用透明度,直接设置结束位置的透明度为0就会消失了。

android:alpha="0"

看看效果:

王者荣耀建模优化方案(使用MotionLayout实现王者荣耀团战来袭)(2)

好像还是有点不对,箭在空中的时候就消失了,我们要的效果是射到亚瑟才消失。

这是因为生成动画的时候是按照起始点到结束点过渡的流程平均分配到每个时间点,所以他就会从一开始就慢慢线性变化透明度,直到完全消失。

怎么办呢?就要用到关键帧KeyFrameSet了。

KeyFrameSet关键帧,可以设定动画过程中的某个关键位置或属性。

设定关键帧后,MotionLayout会平滑地将视图从起点移至每个中间点,然后移至最终目标位置。

所以这里,我们需要设置两个关键属性:1)快射到亚瑟的时候,箭的透明度还是1。2)射到亚瑟的时候,透明度改成0。

<KeyFrameSet> <KeyAttribute app:framePosition="98" app:motionTarget="@id/houyi_arrow" android:alpha="1" /> <KeyAttribute app:framePosition="100" app:motionTarget="@id/houyi_arrow" android:alpha="0" /> </KeyFrameSet>

KeyAttribute就是设置关键属性的标签,其中

这样设置好,后羿箭的动画也就完成了。

疯狂走位的亚瑟

到亚瑟了,亚瑟的动画效果是走位走位被射中。所以先设定好亚瑟的位置,从远处走到被射中的位置。

<ConstraintSet android:id="@ id/start"> <Constraint android:id="@ id/houyi_arrow" android:layout_width="66dp" android:layout_height="66dp" android:layout_marginLeft="190dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.8"> </Constraint> <Constraint android:id="@ id/yase" android:layout_width="66dp" android:layout_height="66dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.8" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.2"> </Constraint> </ConstraintSet> <ConstraintSet android:id="@ id/end"> <Constraint android:id="@ id/houyi_arrow" android:layout_width="66dp" android:layout_height="66dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.65" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.35"> </Constraint> <Constraint android:id="@ id/yase" android:layout_width="66dp" android:layout_height="66dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.65" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.35"> </Constraint> </ConstraintSet>

可以看到,一个端点状态,可以放置多个控件属性。

放好亚瑟的起始和结束状态后,再设定疯狂走位,怎么弄?——KeyCycle

KeyCycle,循环关键帧,可以给动画添加振动,其实就是波形图,比如sin,cos。这里我们就设定一个sin曲线给亚瑟,作为走位的路径。也是放到关键帧KeyFrameSet标签下。

<KeyCycle android:translationY="50dp" app:framePosition="70" app:motionTarget="@id/yase" app:wavePeriod="0.5" app:waveShape="sin" />

好了,第一个场景搞定,看看效果:

王者荣耀建模优化方案(使用MotionLayout实现王者荣耀团战来袭)(3)

场景二

包含控件:妲己,钟无艳

动画描述:从草丛走出来的妲己和钟无艳

这一个场景主要是描述在草丛蹲伏的妲己和钟无艳,看到后羿射箭后,走出草丛准备接技能。直接上代码:

<androidx.constraintlayout.motion.widget.MotionLayout android:id="@ id/motionLayout2" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_02" tools:showPaths="true"> <ImageView android:id="@ id/daji" android:layout_width="80dp" android:layout_height="80dp" android:src="@drawable/daji_model" /> <ImageView android:id="@ id/zhongwuyan" android:layout_width="75dp" android:layout_height="75dp" android:src="@drawable/zhongwuyan_model" /> </androidx.constraintlayout.motion.widget.MotionLayout>

scene_02.xml

<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <Transition app:constraintSetEnd="@ id/end" app:constraintSetStart="@ id/start" app:duration="2000"> <OnClick app:clickAction="toggle" app:targetId="@id/daji" /> </Transition> <ConstraintSet android:id="@ id/start"> <Constraint android:id="@ id/daji" android:layout_width="80dp" android:layout_height="80dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.75" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.85"> </Constraint> <Constraint android:id="@ id/zhongwuyan" android:layout_width="70dp" android:layout_height="70dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.25" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.1"> </Constraint> </ConstraintSet> <ConstraintSet android:id="@ id/end"> <Constraint android:id="@ id/daji" android:layout_width="80dp" android:layout_height="80dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.65" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.65"> </Constraint> <Constraint android:id="@ id/zhongwuyan" android:layout_width="70dp" android:layout_height="70dp" android:alpha="0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.42" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.2"> </Constraint> </ConstraintSet> </MotionScene>

这里,我想给钟无艳一个异形走位,就是先在草丛里走,再出来。

这时候就要用到另一个关键帧标签——KeyPosition

KeyPosition,表示关键帧的位置,也就是动画必经的一个点。该属性用于调整默认的运动路径。

1) motion:percentX、motion:percentY指定视图应到达的位置。keyPositionType 属性指定如何解释这些值。

2) keyPositionType有三种设置

这里我们给钟无艳一个parentRelative

<KeyPosition app:motionTarget="@id/zhongwuyan" app:framePosition="30" app:keyPositionType="parentRelative" app:percentY="0" app:percentX="0.4" />

最后加上两个英雄从草丛走出来,由半透明到不透明的过程:

<KeyAttribute app:framePosition="0" app:motionTarget="@id/daji" android:alpha="0.7" /> <KeyAttribute app:framePosition="70" app:motionTarget="@id/daji" android:alpha="1" /> <KeyAttribute app:framePosition="0" app:motionTarget="@id/zhongwuyan" android:alpha="0.7" /> <KeyAttribute app:framePosition="60" app:motionTarget="@id/zhongwuyan" android:alpha="1" />

场景三

包含控件:妲己的一技能,妲己的二技能,钟无艳

动画描述:钟无艳闪现到人群中使用大招转转转,妲己二技能晕眩住鲁班,一技能跟上。

钟无艳闪现,我用的是消失再出现的方式,也就是改变alpha。钟无艳的大招,用到的是android:rotationY,设定绕y轴旋转。

妲己的一技能和二技能都是用的普通位置移动,注意控制透明度也就是出现和消失即可。上代码:

scene_03.xml

<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <Transition app:constraintSetEnd="@ id/end" app:constraintSetStart="@ id/start" app:duration="4000"> <KeyFrameSet> <!-- 钟无艳--> <KeyAttribute app:framePosition="20" app:motionTarget="@id/zhongwuyan2" android:rotationY="0" /> <KeyAttribute app:framePosition="1" app:motionTarget="@id/zhongwuyan" android:alpha="1" /> <!-- 妲己2技能--> <KeyPosition app:motionTarget="@id/daji_2" app:framePosition="20" app:keyPositionType="deltaRelative" app:percentY="0" app:percentX="0" /> <KeyAttribute app:framePosition="20" app:motionTarget="@id/daji_2" android:alpha="1" /> <KeyPosition app:motionTarget="@id/daji_2" app:framePosition="60" app:keyPositionType="deltaRelative" app:percentY="1" app:percentX="1" /> <KeyAttribute app:framePosition="40" app:motionTarget="@id/daji_2" android:alpha="1" /> <KeyAttribute app:framePosition="61" app:motionTarget="@id/daji_2" android:alpha="0" /> <!-- 妲己1技能--> <KeyAttribute app:framePosition="55" app:motionTarget="@id/daji_1" android:alpha="1" /> <KeyPosition app:motionTarget="@id/daji_1" app:framePosition="55" app:keyPositionType="deltaRelative" app:percentY="0" app:percentX="0" /> <KeyAttribute app:framePosition="85" app:motionTarget="@id/daji_1" android:alpha="1" /> </KeyFrameSet> <OnClick app:clickAction="toggle" app:targetId="@id/zhongwuyan2" /> </Transition> //。。。 </MotionScene>

3、实际应用场景

其实做下来可以发现,Motionlayout实现动画真的很简便,大大提高了开发效率,这也是jetpack组件开发的初心。

但是,Motionlayout还是有缺点的,比如直接通过xml代码的情况下,无法设置动画的衔接,设定动画的先后顺序。

所以到底motionlayout应用场景是什么呢?

motionlayout作为一个过渡动画,应该适用于一些控件切换,界面变化之类的动画。

比如DrawerLayout,viewpager切换的时候,可以设置一些view过渡的动画。官网有一个类似youtube中运动动画的案例,我这边搬过来简单说下。先看看效果

王者荣耀建模优化方案(使用MotionLayout实现王者荣耀团战来袭)(4)

效果不错吧,特别是手势滑动的那个丝滑感,太爽了,以前做这种动画效果少说也要半个小时吧,想想就头疼。

现在,MotionLayout:so easy。

来一起分析下:

包含控件:顶部布局控件topLayout(包含顶部图片topImage,播放按钮topPlay,关闭按钮topClose),中部布局midlayout(包含文字部分midView),下部菜单控件bottomView。

动画描述(某些具体数值由代码中得知):

就这么多,分析好每个布局的起始位置,结束位置,再调整一下关键帧。一个跟随手势滑动的过渡动画布局就完成了。

贴下MotionScene关键代码,想看完整源代码可以去文末附件自取,官网案例和我的demo都包含。

<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:motion="http://schemas.android.com/apk/res-auto"> <Transition motion:constraintSetEnd="@ id/end" motion:constraintSetStart="@ id/start" motion:duration="1000" motion:motionInterpolator="linear"> <OnSwipe motion:dragDirection="dragUp" motion:touchAnchorId="@ id/top_image_container" motion:touchAnchorSide="bottom" /> <KeyFrameSet> <KeyPosition motion:curveFit="linear" motion:framePosition="90" motion:motionTarget="@id/top_image" motion:percentWidth="0" motion:percentX="0" /> <KeyPosition motion:curveFit="linear" motion:framePosition="90" motion:motionTarget="@id/top_image_container" motion:percentWidth="0" /> <KeyPosition motion:curveFit="linear" motion:framePosition="90" motion:motionTarget="@id/recyclerview_container" motion:percentWidth="0" /> <KeyAttribute android:alpha="0" motion:framePosition="75" motion:motionTarget="@id/recyclerview_front" /> <KeyAttribute android:alpha="0.10" motion:framePosition="90" motion:motionTarget="@id/image_clear" /> <KeyAttribute android:alpha="0.10" motion:framePosition="90" motion:motionTarget="@id/image_play" /> </KeyFrameSet> </Transition> </MotionScene>

这里有几个新属性说下:

4、关于过渡动画

关于过渡动画,其实之前也是存在的——TransitionManager。TransitionManager可以提供不同场景之间的过渡转换动画,需要设定两个场景(布局文件),然后两个场景中对应的控件id要对应上。最后通过java代码执行过渡动画。

上个代码:

//两个场景的布局 <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@ id/scene_root"> <include layout="@layout/a_scene" /> </FrameLayout> //场景一 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@ id/scene_container" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" android:id="@ id/text_view1" android:text="Text Line 1" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" android:id="@ id/text_view2" android:text="Text Line 2" /> </LinearLayout> //场景二 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@ id/scene_container" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@ id/text_view2" android:textSize="22sp" android:text="Text Line 2" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="22sp" android:id="@ id/text_view1" android:text="Text Line 1" /> </LinearLayout> //获取场景,开始场景间的动画,从场景一变化为场景二 val sceneRoot: ViewGroup = findViewById(R.id.scene_root) val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this) val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this) titletv.setOnClickListener { TransitionManager.go(anotherScene) }

咦,跟MotionLayout还是蛮像的,思路也差不多,都是通过不同场景的控件完成过渡动画。那么问题来了,既然有为什么还要出个MotionLayout呢?

所以MotionLayout还是很优秀的,快用起来吧!

今天就到这里了,可以到我的 GitHub:https://github.com/AndroidCot/Android

发现更多的好东西哦。

,