用Android MotionLayout库实现王者荣耀团战效果
昨晚跟往常一样,饭后开了一局王者荣耀,前中期基本焦灼,到了后期一波决定胜负的时候,我果断射箭
,射中对面,配合队友直接秒杀,打赢团战一波推完基地。那叫一个精彩,队友都发出了666666
的称赞,我酷酷的点了一下抱拳:多谢!嘿嘿
。
赛后,手机上正在展示我的MVP动画,我不禁思考,这么精彩的团战我怎么能不记录下来?刚好最近了解到MotionLayout
库,就用它实现吧?。
动画效果
功能详解
MotionLayout 是一种布局类型,可帮助您管理应用中的运动和微件动画。MotionLayout 是 ConstraintLayout 的子类,在其丰富的布局功能基础之上构建而成。
如上述介绍,MotionLayout
是 ConstraintLayout
的子类,相当于加了动画功能的ConstraintLayout。MotionLayout
作为一个动画控件的好处就在于基本不用写java代码,全部在xml
文件中搞定。而且我们只需要设定起始位置,结束位置以及一些中间状态,就能自动生成动画。
先分析下我们的团战,主要分为三个场景:
后羿果断射大,射中在 疯狂走位
的亚瑟。妲己和钟无艳同时在草丛蹲伏,看到后羿的精彩射箭,从 草丛走出
,准备大战。妲己 2技能
晕眩住对面的鲁班七号,一套技能加上钟无艳的大招
,将对面两个英雄KO。
场景一
包含控件:后羿,亚瑟,鲁班,后羿的箭
动画描述:走位的亚瑟,后羿射箭
首先在布局文件中,添加第一个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">
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" />
android:id="@+id/houyi_arrow"
android:layout_width="66dp"
android:layout_height="66dp"
android:src="@drawable/arrow" />
android:id="@+id/yase"
android:layout_width="66dp"
android:layout_height="66dp"
android:src="@drawable/yase_model" />
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" />
由于MotionLayout
继承自ConstraintLayout,所以可以用ConstraintLayout
的属性。 这里可以看到有两个新的属性:
app:layoutDescription
,这个属性就是代表该MotionLayout对应的动画场景,引用的是一个MotionScene
(XML资源文件),其中就会包括相应布局的所有运动动画描述。app:showPaths
,这个属性代表运动进行时是否显示运动路径,也就是所有动画的路径是否显示。默认是false。代码中也是可以设置是否显示动画路径,setDebugMode
方法传入MotionLayout.DEBUG_SHOW_PATH
属性即可。
后羿射箭
接下来就可以写动画场景(MotionScene)了,新建res-xml-scene_01.xml。
xmlns:app="http://schemas.android.com/apk/res-auto">
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="2000">
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">
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">
可以看到,MotionScene有两个主要的标签Transition
和ConstraintSet
Transition
,包括运动的基本定义,其中motion:constraintSetStart
和motion:constraintSetEnd
指的是运动的起始状态和结束状态。分别就对应下面ConstraintSet所配置的。app:duration
代表完成运动所需的时间。ConstraintSet
,多个控件的端点约束集合,比如这里就有两个ConstraintSet,分别代表起始约束集和结束约束集。
其中Constraint
属性指定了端点位置中某一个元素的位置和属性:
支持所有 ConstraintLayout
属性。支持 alpha,rotation,visibility,translationX,scaleX
等view基本属性。支持自定义属性,用子标签 CustomAttribute
表示,举个小栗子:
android:id="@+id/button" ...>
motion:attributeName="backgroundColor"
motion:customColorValue="#D81B60"/>
attributeName属性就是与具有getter
和setter
方法的对象匹配,比如这里的backgroundColor
就对应了view本身的基本方法getBackgroundColor()
和 setBackgroundColor()
。
好了
,回到后裔这边,由于后羿的箭是从后羿位置到亚瑟位置,所以我们设定好后羿箭的两个端点状态,配置好后,MotionLayout
就会自动帮我们生成从起始状态到结束状态的动画了,后羿箭从后羿位置飞到了亚瑟位置。 等等,运行怎么没反应呢?动画怎么触发啊?
Motion提供了三动画触发方法:
1)onClick标签,表示点击场景中的某个控件来触发动画效果。其中有两个属性。
app:targetId
,表示要触发动画的视图app:clickAction
,表示点击的效果,例如,toggle(循环动画),transitionToStart(过渡到开始状态)
2)OnSwipe标签,表示通过用户轻触控制动画,有点手势滑动的感觉
app:touchAnchorId
,表示可以滑动并拖动的视图。app:touchAnchorSide
表示从哪边开始拖动。app:dragDirection
表示拖动的进度方向。例如,dragRight表示当向右拖动(滑动)。app:onTouchUp
表示手势抬起的时候动作。例如,stop表示手势抬起的时候view动画停止。
3)java代码控制.
motionLayout.transitionToEnd()
,过渡动画到结束位置。motionLayout.setTransitionListener
,监听动画。
这里我们就设置点击后羿触发动画:
app:clickAction="toggle"
app:targetId="@id/houyi" />
好了,运行,点击后羿,后羿的箭成功射出去了。
但是这还不够,后羿箭到亚瑟位置肯定就会消失了,怎么表示这个消失呢?用透明度
,直接设置结束位置的透明度为0就会消失了。
android:alpha="0"
看看效果:
好像还是有点不对,箭在空中的时候就消失了,我们要的效果是射到亚瑟才消失
。 这是因为生成动画的时候是按照起始点到结束点过渡的流程平均分配到每个时间点,所以他就会从一开始就慢慢线性变化透明度,直到完全消失。
怎么办呢?就要用到关键帧KeyFrameSet
了。
KeyFrameSet
关键帧,可以设定动画过程中的某个关键位置或属性。设定关键帧后,MotionLayout
会平滑地将视图从起点移至每个中间点,然后移至最终目标位置。
所以这里,我们需要设置两个关键属性
:
1)快射到亚瑟的时候,箭的透明度还是1。
2)射到亚瑟的时候,透明度改成0。
app:framePosition="98"
app:motionTarget="@id/houyi_arrow"
android:alpha="1" />
app:framePosition="100"
app:motionTarget="@id/houyi_arrow"
android:alpha="0" />
KeyAttribute就是设置关键属性的标签,其中
app:framePosition
表示该关键帧的位置,相当于百分比。app:motionTarget
表示作用于那个视图
这样设置好,后羿箭的动画也就完成了。
疯狂走位的亚瑟
到亚瑟了,亚瑟的动画效果是走位走位被射中。所以先设定好亚瑟的位置,从远处走到被射中的位置。
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">
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">
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">
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">
可以看到,一个端点状态,可以放置多个控件属性。
放好亚瑟的起始和结束状态后,再设定疯狂走位,怎么弄?——KeyCycle
KeyCycle
,循环关键帧,可以给动画添加振动,其实就是波形图,比如sin,cos
。 这里我们就设定一个sin曲线给亚瑟,作为走位的路径。也是放到关键帧KeyFrameSet
标签下。
android:translationY="50dp"
app:framePosition="70"
app:motionTarget="@id/yase"
app:wavePeriod="0.5"
app:waveShape="sin" />
app:waveShape
表示波动类型,比如sin,cosapp:wavePeriod
表示波动周期,比如0.5就表示半个sin周期,可以理解为震动0.5次。
好了,第一个场景搞定,看看效果:
场景二
包含控件:妲己,钟无艳
动画描述:从草丛走出来的妲己和钟无艳
这一个场景主要是描述在草丛蹲伏
的妲己和钟无艳,看到后羿射箭后,走出草丛准备接技能
。 直接上代码:
android:id="@+id/motionLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_02"
tools:showPaths="true">
android:id="@+id/daji"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/daji_model" />
android:id="@+id/zhongwuyan"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/zhongwuyan_model" />
xmlns:app="http://schemas.android.com/apk/res-auto">
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="2000">
app:clickAction="toggle"
app:targetId="@id/daji" />
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">
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">
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">
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">
这里,我想给钟无艳一个异形走位
,就是先在草丛里走,再出来。这时候就要用到另一个关键帧标签——KeyPosition
KeyPosition,表示关键帧的位置,也就是动画必经的一个点。该属性用于调整默认的运动路径。
1) motion:percentX、motion:percentY指定视图应到达的位置。keyPositionType 属性指定如何解释这些值。
2) keyPositionType有三种设置
parentRelative
,相对于父视图的位置,x为横轴(0左-1右),y为纵轴(0顶-1底)比如要设置位置到右端中部位置,就设定app:percentY="0.5" app:percentX="1"即可。deltaRelative
,相对于视图在整个运动序列过程中移动的距离,(0,0)为视图起始位置,(1,1)为结束位置。x为横轴,y为纵轴pathRelative
,x轴方向为视图在路径范围内移动的方向,0位视图起始位置,1为结束位置(即x轴为起点和终点的连接线)。y轴垂直于x轴,正值为路径左侧,负值为右侧。所以,这个和deltaRelative相比,就是x轴和Y轴的不同,相同的是都是按照起始位置到结束位置为参考。
这里我们给钟无艳一个parentRelative。
app:motionTarget="@id/zhongwuyan"
app:framePosition="30"
app:keyPositionType="parentRelative"
app:percentY="0"
app:percentX="0.4"
/>
最后加上两个英雄从草丛走出来,由半透明到不透明的过程:
app:framePosition="0"
app:motionTarget="@id/daji"
android:alpha="0.7" />
app:framePosition="70"
app:motionTarget="@id/daji"
android:alpha="1" />
app:framePosition="0"
app:motionTarget="@id/zhongwuyan"
android:alpha="0.7" />
app:framePosition="60"
app:motionTarget="@id/zhongwuyan"
android:alpha="1" />
场景三
包含控件:妲己的一技能,妲己的二技能,钟无艳
动画描述:钟无艳闪现到人群中使用大招转转转,妲己二技能晕眩住鲁班,一技能跟上。
钟无艳闪现,我用的是消失再出现的方式,也就是改变alpha
。 钟无艳的大招,用到的是android:rotationY
,设定绕y轴旋转。
妲己的一技能和二技能都是用的普通位置移动,注意控制透明度也就是出现和消失即可。 上代码:
xmlns:app="http://schemas.android.com/apk/res-auto">
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="4000">
app:framePosition="20"
app:motionTarget="@id/zhongwuyan2"
android:rotationY="0" />
app:framePosition="1"
app:motionTarget="@id/zhongwuyan"
android:alpha="1" />
app:motionTarget="@id/daji_2"
app:framePosition="20"
app:keyPositionType="deltaRelative"
app:percentY="0"
app:percentX="0"
/>
app:framePosition="20"
app:motionTarget="@id/daji_2"
android:alpha="1" />
app:motionTarget="@id/daji_2"
app:framePosition="60"
app:keyPositionType="deltaRelative"
app:percentY="1"
app:percentX="1"
/>
app:framePosition="40"
app:motionTarget="@id/daji_2"
android:alpha="1" />
app:framePosition="61"
app:motionTarget="@id/daji_2"
android:alpha="0" />
app:framePosition="55"
app:motionTarget="@id/daji_1"
android:alpha="1" />
app:motionTarget="@id/daji_1"
app:framePosition="55"
app:keyPositionType="deltaRelative"
app:percentY="0"
app:percentX="0"
/>
app:framePosition="85"
app:motionTarget="@id/daji_1"
android:alpha="1" />
app:clickAction="toggle"
app:targetId="@id/zhongwuyan2" />
实际应用场景
其实做下来可以发现,Motionlayout
实现动画真的很简便,大大提高了开发效率,这也是jetpack
组件开发的初心。 但是,Motionlayout
还是有缺点的,比如直接通过xml代码的情况下,无法设置动画的衔接,设定动画的先后顺序。 所以到底motionlayout
应用场景是什么呢?
motionlayout
作为一个过渡动画,应该适用于一些控件切换,界面变化之类的动画。比如DrawerLayout,viewpager
切换的时候,可以设置一些view过渡的动画。 官网有一个类似youtube
中运动动画的案例,我这边搬过来简单说下。先看看效果
效果不错吧,特别是手势滑动的那个丝滑感
,太爽了,以前做这种动画效果少说也要半个小时吧,想想就头疼。现在,MotionLayout:so easy。
来一起分析下:
包含控件:顶部布局控件topLayout
(包含顶部图片topImage,播放按钮topPlay,关闭按钮topClose),中部布局midlayout
(包含文字部分midView),下部菜单控件bottomView
动画描述(某些具体数值由代码中得知):
topLayout
从上方依附parent位置,变化到下方bottomView的上方。高度由320dp变成54dp。topImage
从满铺父布局,到最后长度不满铺(长度设置为高度2.5倍),高度距离父布局上下2dp。关键帧:到90%进度的时候,还是满铺,再慢慢缩小长度。topPlay
,topClose从不显示(alhpa为0)到最后显示完全(alhpa为1)。关键帧:到90%进度的时候,不透明还是为10%(alpha0.1),再慢慢变不透明。midlayout
,白色布局,从底部依附父布局到bottomView的上方,这个layout是为了让toplayout下来的时候更加自然,因为recycleview会变完全透明,就需要这个白色布局过渡,让动画更完整。midView
,从toplayout下方位置到最后和toplayout重合,透明度从不透明到完全透明。关键帧:到75%进度的时候,就完全透明。bottomView
,从父布局视图下面(看不到)到父布局底部(看得见)
就这么多,分析好每个布局的起始位置,结束位置,再调整一下关键帧。一个跟随手势滑动的过渡动画布局就完成了。 贴下MotionScene
关键代码,想看完整源代码可以去文末附件自取,官网案例和我的demo都包含。
xmlns:motion="http://schemas.android.com/apk/res-auto">
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start"
motion:duration="1000"
motion:motionInterpolator="linear">
motion:dragDirection="dragUp"
motion:touchAnchorId="@+id/top_image_container"
motion:touchAnchorSide="bottom" />
motion:curveFit="linear"
motion:framePosition="90"
motion:motionTarget="@id/top_image"
motion:percentWidth="0"
motion:percentX="0" />
motion:curveFit="linear"
motion:framePosition="90"
motion:motionTarget="@id/top_image_container"
motion:percentWidth="0" />
motion:curveFit="linear"
motion:framePosition="90"
motion:motionTarget="@id/recyclerview_container"
motion:percentWidth="0" />
android:alpha="0"
motion:framePosition="75"
motion:motionTarget="@id/recyclerview_front" />
android:alpha="0.10"
motion:framePosition="90"
motion:motionTarget="@id/image_clear" />
android:alpha="0.10"
motion:framePosition="90"
motion:motionTarget="@id/image_play" />
这里有几个新属性说下:
motion:curveFit
,表示用哪种线条轨迹经过该关键帧,默认是曲线(spline),更加圆滑。这是设置的linear为直线过渡,因为本身就是直线,所以没什么影响。motion:percentWidth
,表示视图相对大小,取值为0-1,0代表初始位置宽度,1代表结束位置宽度。这里为0就代表宽度到该位置还是和初始宽度一致。motion:motionInterpolator
,表示动画的插值器。这里的linear就是线性运动,还可以设置bounce弹簧运动等等。
关于过渡动画
关于过渡动画,其实之前也是存在的——TransitionManager
。TransitionManager
可以提供不同场景之间的过渡转换动画,需要设定两个场景(布局文件),然后两个场景中对应的控件id要对应上。最后通过java代码执行过渡动画。
上个代码:
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/scene_root">
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="26sp"
android:id="@+id/text_view1"
android:text="Text Line 1" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="26sp"
android:id="@+id/text_view2"
android:text="Text Line 2" />
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_view2"
android:textSize="22sp"
android:text="Text Line 2" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:id="@+id/text_view1"
android:text="Text Line 1" />
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呢?
前者(TransitionManager)无法设置关键帧,动画只有两个状态。 MotionLayout
就可以随意设置关键帧,设置不同的位置,属性等等。前者不能跟随手势滑动, MotionLayout
就丝滑的多。MotionLayout全部用 xml代码
就可以完成整个动画,不需要调用一句java代码。前者布局控件重复太多,需要不同的xml文件,写 重复的控件
。
所以MotionLayout还是很优秀的,快用起来吧!
附件
官网MotionLayout案例
https://developer.android.google.cn/training/constraint-layout/motionlayout/examples
文章相关源代码
https://github.com/JiMuzz/MotionLayoutDemo
如有收获,欢迎「分享 」
「点赞」「评论 」
看完本文有收获?请转发分享给更多人
开发者全社区