玩转 MotionLayout:实战效果展示
在上一篇《MotionLayout :布局中的战斗机 》中介绍了MotionLayout 的基本使用,本文通过一些实战案例进一步展示 MotionLayout 的强大。
本文你将学到
代码控制转场
配合 ViewPager 使用
仿华为拨号界面动画效果
Android 11 彩蛋制作
1.代码控制转场
为什么要用代码控制转场呢,xml 写着不香吗😜 ?xml 写着很方便,但是有时我们需要动态的改变转场的效果,就需要通过代码来实现了。比如初始状态是xml 描述的转场效果,当我们点击按钮时,变成另外一种转场效果,下面就来实现一下吧。
示例
实现前,先看下效果
初始转场动画是一个简单的位移动画。点击状态1按钮,转场动画变成除了平移转场动画外, 还有rotationY 从1到180 和alpha 从1到0.1的过度转场动画。点击状态2按钮,平移转场结束的位置变了。
核心代码
//按钮1
binding.btnState1.setOnClickListener {
// 0️⃣ 通过id获取ConstraintSet,id 是xml中对应id
val constraintSet = binding.motionLayout.getConstraintSet(R.id.end)
//1️⃣ 通过id获取Constraint
constraintSet.getConstraint(R.id.box).apply {
// 2️⃣
//设置layout 的一些属性
//↓↓这行代码↓↓ =>layout_constraintTop_toTopOf="parent"
layout.topToTop = Constraints.LayoutParams.PARENT_ID
layout.bottomToBottom = Constraints.LayoutParams.PARENT_ID
layout.endToEnd = Constraints.LayoutParams.PARENT_ID
//transform 可以改变旋转缩放等属性
transform.rotationY = 180f
transform.rotationX = 0f
propertySet.alpha = 0.1f
}
}
binding.btnState2.setOnClickListener {
val constraintSet = binding.motionLayout.getConstraintSet(R.id.end)
constraintSet.getConstraint(R.id.box).apply {
layout.topToTop = Constraints.LayoutParams.PARENT_ID
layout.endToEnd = Constraints.LayoutParams.PARENT_ID
//去除约束将View 放到右上角
layout.bottomToBottom = Constraints.LayoutParams.UNSET
transform.rotationX = 180f
transform.rotationY = 0f
}
}
xml 的代码就不贴了
代码分析
在0️⃣ 处通过 getConstraintSet(id:Int)
方法获取转场结束时的ConstraintSet
的对象。在代码1️⃣ 处getConstraint(id:Int)
获取 ConstraintSet
的一个约束对象Constraint
,在2️⃣代码处,通过Constraint
对象来改变一些约束条件。综上所述,通过代码动态改变转场大致有以下三步 1 通过MotionLayout的getConstraintSet(id:Int)
获取要修改的的约束集ConstrainSet
val constraintSet = binding.motionLayout.getConstraintSet(R.id.end)
2 通过ConstrainSet的getConstraint(id:Int)
的方法获取对应Contraint
val constraint=constraintSet.getConstraint(R.id.box)
3 拿到Contraint 之后,利用提供的方法,就可以改成自己想要的效果了。
再说一下Constraint
前一篇文章说 Constraint 可以设置 View 的约束和属性等,例如
<Constraint android:id="@+id/box"
android:layout_width="100dp"
android:layout_height="100dp"
android:rotationX="180"
android:scaleY="0.5"
android:scaleX="0.5"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent"/>
Constraint 还有一种写法,Constraint有子级
<Constraint android:id="@+id/box">
<Layout
android:layout_width="100dp"
android:layout_height="100dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<Transform
android:rotationX="180"
android:scaleX="0.5"
android:scaleY="0.5" />
</Constraint>
下面是 Constraint 部分源码
public static class Constraint {
……
public final PropertySet propertySet = new PropertySet();
public final Motion motion = new Motion();
public final Layout layout = new Layout();
public final Transform transform = new Transform();
public HashMap<String, ConstraintAttribute> mCustomConstraints = new HashMap<>();
……
}
上面几个成员都是和Constraint 子标签对应的,通过这些成员就可以实现和xml 一样的效果
// 和 <Layout/> 对应
layout.topToTop = Constraints.LayoutParams.PARENT_ID
layout.bottomToBottom = Constraints.LayoutParams.PARENT_ID
layout.endToEnd = Constraints.LayoutParams.PARENT_ID
// 和 <Transform/> 对应
transform.rotationY = 180f
transform.rotationX = 0f
// 和 <PropertySet/> 对应
propertySet.alpha = 0.1f
……
2. 和 ViewPager 配合使用
让动画随着ViewPager的滑动而运动。效果如下
实现思路,MotionLayout 有一个
setProgress(float pos)
方法,当滑动ViewPager时,改变MotionLayout的执行进度
布局相关的核心代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ViewPagerDemoActivity">
<!--MotionLayout 作为根布局,才能使用as的预览动画,所以采用include方式-->
<include
android:id="@+id/motionLayout"
layout="@layout/viewpager_header"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
MotionLayout 作为根布局,才能使用as的预览动画,所以采用include方式
viewpager_header.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/sky"
app:layoutDescription="@xml/viewpager_header_scene"
app:showPaths="true">
<ImageView
android:id="@+id/doraemon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/doraemon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
viewpager_header_scene.xml
<?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:pathMotionArc="flip">
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/doraemon"
android:layout_width="60dp"
android:layout_height="60dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/doraemon"
android:layout_width="60dp"
android:layout_height="60dp"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>
Activity代码
……部分代码省略
binding.viewpager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
binding.motionLayout.root.progress =
(position + positionOffset) / (adapter.count - 1)
}
override fun onPageSelected(position: Int) {}
override fun onPageScrollStateChanged(state: Int) {}
})
添加viewPager的page改变监听器,在onPageScrolled 中根据位置和偏移量计算当前活动的进度,修改MotionLayout的progress的值,从而实现MotionLayout和ViewPager的联动。如果其他控件想实现联动也是类似方式。
3. 仿EMUI 11 拨号界面动画
原生效果
简单的分析一下,当我们滑动列表时,键盘和拨号键做位移动画,其他的细节先不管,这个用MotionLayout还是挺好实现的,下面我来实现一下。
MotionLayout 高仿效果
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_huawei_tel_scene"
tools:context=".HuaweiTelActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--数组键盘-->
<ImageView
android:id="@+id/telKeyboard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="2dp"
android:src="@drawable/tel_keyboard"
app:layout_constraintBottom_toTopOf="@+id/telBottomNav"
app:layout_constraintDimensionRatio="1.1" />
<!-- 底部导航栏-->
<ImageView
android:id="@+id/telBottomNav"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@android:color/white"
android:elevation="2dp"
android:src="@drawable/tel_bottom_nav"
app:layout_constraintBottom_toBottomOf="parent" />
<!--拨号键-->
<ImageView
android:id="@+id/imgPhone"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginBottom="20dp"
android:background="@drawable/bg_circle"
android:elevation="2dp"
android:padding="10dp"
android:src="@drawable/ic_baseline_local_phone_24"
app:layout_constraintBottom_toTopOf="@+id/telBottomNav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
⚠️ 这里只是演示动画,界面上非动画必要元素都用图片代替了。
activity_huawei_tel_scene.xml
<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">
<OnSwipe/>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/imgPhone"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginBottom="20dp"
android:elevation="2dp"
motion:layout_constraintBottom_toBottomOf="@+id/telKeyboard"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"/>
<Constraint
android:id="@+id/telKeyboard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="2dp"
motion:layout_constraintBottom_toTopOf="@+id/telBottomNav"
motion:layout_constraintDimensionRatio="1.1" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/telKeyboard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="2dp"
motion:layout_constraintDimensionRatio="1.1"
motion:layout_constraintTop_toTopOf="@id/telBottomNav" />
<Constraint
android:id="@+id/imgPhone"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginEnd="50dp"
android:layout_marginBottom="20dp"
android:elevation="2dp"
motion:layout_constraintBottom_toTopOf="@id/telBottomNav"
motion:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>
</MotionScene>
我们在id=start的ConstraintSet中定义数字键盘位于底部导航上方也就是
motion:layout_constraintBottom_toTopOf="@+id/telBottomNav"
,拨号按钮位于水平居中位置。在id=end的ConstraintSet 中定义数字键盘顶部和底部导航的顶部对齐 也就是motion:layout_constraintTop_toTopOf="@id/telBottomNav"
,将拨号按钮放在靠近屏幕右侧的位置。
运行效果如下
下面来处理一些细节(拨号按钮改变的过度动画) 我看到这个效果想到的是在constraintlayout v2.0 添加一个ImageFilterView
来实现这个效果 ImageFilterView
继承ImageView,他可以改变图片的饱和度、色温等,还可以做两个图片渐变过度的效果。先就用ImageFilterView
来实现拨号按钮改变的过度动画试试 布局
…… 其他代码同上
<!--拨号键-->
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/imgPhone"
android:src="@drawable/ic_baseline_local_phone_24"
app:altSrc="@drawable/ic_baseline_dialpad_24"/>
ImageFilterView 除了src外还有一个altSrc的属性,如果这个设置了,那么src和altSrc会组合成LayerDrawable作为ImageView的Drawable。其源码如下
Drawable drawable = a.getDrawable(styleable.ImageFilterView_altSrc);
if (drawable != null) {
this.mLayers = new Drawable[2];
this.mLayers[0] = this.getDrawable();
this.mLayers[1] = drawable;
this.mLayer = new LayerDrawable(this.mLayers);
this.mLayer.getDrawable(1).setAlpha((int)(255.0F * this.mCrossfade));
super.setImageDrawable(this.mLayer);
}
activity_huawei_tel_scene.xml
<?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">
…… 省略代码同上
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/telKeyboard"
…… />
<Constraint
android:id="@+id/imgPhone"
……>
<CustomAttribute
motion:attributeName="crossfade"
motion:customFloatValue="0" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/telKeyboard"
…… />
<Constraint
android:id="@+id/imgPhone"
……>
<CustomAttribute
motion:attributeName="crossfade"
motion:customFloatValue="1" />
<Transform
android:scaleX="0.9"
android:scaleY="0.9" />
</Constraint>
</ConstraintSet>
</MotionScene>
上面使用了自定义属性crossfade,crossfade是
ImageFilterView
定义用于src到altSrc过度的属性,其这部源码核心代码如下
public void setCrossfade(float crossfade) {
mCrossfade = crossfade;
if (mLayers != null) {
if (!mOverlay) {//mOverlay 为false时,crossfade变化,alt对应的图片也会改变透明度
mLayer.getDrawable(0).setAlpha((int) (255 * (1 - mCrossfade)));
}
//getDrawable(1) => altSrc 根据mCrossfade 改变透明度
mLayer.getDrawable(1).setAlpha((int) (255 * (mCrossfade)));
super.setImageDrawable(mLayer);
}
}
根据源码我们可以看出,如何想让alt 的透明度也跟着变化我们就要把mOverlay 设置成false,所以我们把布局代码改成如下
<!--拨号键-->
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/imgPhone"
……
android:src="@drawable/ic_baseline_local_phone_24"
app:altSrc="@drawable/ic_baseline_dialpad_24"
app:overlay="false"/>
运行效果如下
有点感觉了😁 ,但仔细发现还是和原效果有点差不别的,原效果是一个图片完全消失,另一个图片才开始显示,但上面我们使用ImageFilterView 实现的效果是一个图片透明度逐渐减小,另一个透明度逐渐增大,是同时进行的。因为
ImageFilterView
的setCrossfade
方法里面的逻辑就是这样写的,我们也没有办法
虽然我们没法改变ImageFilterView
的setCrossfade
方法,但是我们可以仿照这种方式改一改,下面就开干了,我们自定义一个CrossFadeImageView
,代码如下
class CrossFadeImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
private var layerDrawable: LayerDrawable? = null
/**
* 控制src图片透明度
* value [0,1]
*/
var srcAlpha = 0f
set(value) {
field = value
layerDrawable?.getDrawable(0)?.alpha = (255*value).toInt()
invalidate()
}
/**
* 控制altSrc图片透明度
* value [0,1]
*/
var altSrcAlpha = 0f
set(value) {
field = value
layerDrawable?.getDrawable(1)?.alpha = (255*value).toInt()
invalidate()
}
init {
val a = getContext().obtainStyledAttributes(attrs, R.styleable.CrossFadeImageView)
val drawable = a.getDrawable(R.styleable.CrossFadeImageView_altSrc)
a.recycle()
if (drawable != null) {
drawable.alpha = 0
layerDrawable = LayerDrawable(arrayOf(getDrawable(), drawable))
super.setImageDrawable(layerDrawable)
}
}
}
上面就是仿照
ImageFilterView
写的CrossFadeImageView,也是让src和altSrc合成一个LayerDrawable 作为ImageView的Drawable,我们写了两个属性[srcAlpha、altSrcAlpha]分别控制src 和altSrc的透明度。
修改布局文件如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_huawei_tel_scene"
tools:context=".HuaweiTelActivity">
…… 代码同上
<!--拨号键-->
<com.wkk.motionlayoutdemo.widget.CrossFadeImageView
android:id="@+id/imgPhone"
……
android:src="@drawable/ic_baseline_local_phone_24"
app:altSrc="@drawable/ic_baseline_dialpad_24" />
</androidx.constraintlayout.motion.widget.MotionLayout>
修改activity_huawei_tel_scene.xml 文件文件如下
<?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">
<OnSwipe/>
<!-- 2️⃣ 新增code-->
<KeyFrameSet>
<KeyAttribute
motion:framePosition="80"
motion:motionTarget="@id/imgPhone">
<CustomAttribute
motion:attributeName="srcAlpha"
motion:customFloatValue="0" />
<CustomAttribute
motion:attributeName="altSrcAlpha"
motion:customFloatValue="0" />
</KeyAttribute>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
……
<Constraint
android:id="@+id/imgPhone"……>
<!-- 0️⃣ 新增code-->
<CustomAttribute
motion:attributeName="srcAlpha"
motion:customFloatValue="1" />
<CustomAttribute
motion:attributeName="altSrcAlpha"
motion:customFloatValue="0" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
……
<Constraint
android:id="@+id/imgPhone"……>
<!--1️⃣ 新增code-->
<CustomAttribute
motion:attributeName="srcAlpha"
motion:customFloatValue="0" />
<CustomAttribute
motion:attributeName="altSrcAlpha"
motion:customFloatValue="1" />
<Transform
android:scaleX="0.9"
android:scaleY="0.9" />
</Constraint>
</ConstraintSet>
</MotionScene>
在0️⃣ 处动画开始的时候指定src为显示(srcAlpha=1),altSrc为隐藏(altSrcAlpha=0),在1️⃣ 处动画结束的时候指定src为隐藏(srcAlpha=0),altSrc为显示(altSrcAlpha=1),开始结束状态定义好了,但是看原效果动画是执行的过程中发生变化的,所以我们还在2️⃣ 处定义了关键帧,在动画执行80的位置指定隐藏(srcAlpha=0),altSrc为隐藏(altSrcAlpha=0),上面定义的状态如下表所示
开始(0) | 动画过程中关键帧(80) | 结束(100) | |
---|---|---|---|
srcAlpha | 1 | 0 | 0 |
altSrcAlpha | 0 | 0 | 1 |
根据上表可知,在动画0到80的过程中,srcAlpha从1 变化到0,透明度逐渐减小,altSrcAlpha从0到0 ,透明度没有变化,所以此过程是src的图片逐渐消失,altSrc还是不出现,到80的时候二者都隐藏了。从动画80到100,srcAlpha从0到0 透明度没有变化,所有src一直处于隐藏状态,而altSrc从0到1 ,透明度逐渐变大,所以在动画80到100的过程中,src一直处于隐藏状态,altSrc 渐渐显示。综上所述,整个过程就是src先逐渐消失,等src完全隐藏时,altSrc渐渐显示,这一过程和效果大致相同。
运行效果如下
这个效果就和原效果差不多,大致思路就是这样了。当然还是有很多细节很处理了,这里就不在继续细化了,感兴趣的可以下载下面提供的代码改一改。
4. Android 11 彩蛋制作
MotionLayout 高仿效果
效果用MotionLayout 还是比较简单的,就是中间白色小球的环形轨迹计算的有点麻烦。下面就说一说我对白色小球计算的思路(有点麻烦,如果有比较好的方法,欢迎大家在评论区讨论 🤝 ) 先把上面图抽象一下😁
如果我们指定开始和结束位置如上图所示,在我们没有做处理的情况下,小球肯定就走直线直接到结束位置,我们想让它绕个圈圈再到结束位置,那么就要使用关键帧,让它沿着圆走,那么我们就要计算圆上各个点的位置。
我们要想知道各个点的位置,就需要先确定坐标系,这里我采用的是pathRelative 坐标系效果如下
只是单纯旋转不会影响各个点的坐标值
但是上面坐标系,想计算各个点,还是比较麻烦,我们可以把坐标原点移动到圆心位置,这样会改变各个点的坐标值,但是这样会变的好算一些。我们可以计算出原点x方向,和y方向的偏移大小,那其它的点的偏移大小也是和原点偏移相同的,因为我们是平移了坐标原点,所以当我们在这个坐标系算到各个点之后,还要再偏移回去。
⚠️ 注意这个坐标系,x轴和y轴是相反的。
这样我们就可以计算出各个点的的坐标了,手算那是不可能的,还是写个简单的函数计算一个各个点的坐标值吧。
var r = 1.0
fun main() {
//每个点对应的弧度值,开始位置的下一个点位于2π-π/4
var angrad = 2 * Math.PI - Math.PI / 4f
//圆的半径
r = 1 / (2 * sin(Math.PI / 12))
//循环计算各个点的坐标
for (i in 0..10) {
calculatePoint(angrad)
//每个点之间的角度间隔Math.PI / 6f
angrad -= Math.PI / 6f
}
}
private val numberFormat = NumberFormat.getNumberInstance().apply { maximumFractionDigits = 3 }
/**
*根据[angrad]弧度值,打印对应的x,y坐标值
*/
fun calculatePoint(angrad: Double) {
//与常规坐标系相比,这里x轴和y轴是反的
//把圆看做单位圆,那么sin(angrad)就是x坐标,因为我们平移了原点,
// sin(Math.PI / 12f) 是原点在x轴方向的偏移量,要先得到原来的值,还要平移回去
//所以还要加上平移量。r是圆点半径值,sin(angrad) + sin(Math.PI / 12f)是我们把圆当做单位圆计算的,
//那么 r * (sin(angrad) + sin(Math.PI / 12f)) 就是真实的x坐标了
val calculateX = r * (sin(angrad) + sin(Math.PI / 12f))
//坐标y与坐标x同理
val calculateY = r * (cos(angrad) - cos(Math.PI / 12f))
//打印 x,y 坐标
println("motion:percentX=\"${numberFormat.format(calculateX)}\"")
println("motion:percentY=\"${numberFormat.format(calculateY)}\"")
println()
}
输出
motion:percentX="-0.866"
motion:percentY="-0.5"
motion:percentX="-1.366"
motion:percentY="-1.366"
……
上面函数的输出结果就是中间小球运动轨迹的关键帧位置,吧这些值复制出来贴的keyFrameSet 中
<!-- 控制小球的关键帧集,让小球转圈圈 -->
<KeyFrameSet>
<KeyPosition
motion:framePosition="9"
motion:keyPositionType="pathRelative"
motion:motionTarget="@id/controlPoint"
motion:pathMotionArc="flip"
motion:percentX="-0.866"
motion:percentY="-0.5" />
<KeyPosition
motion:framePosition="18"
motion:keyPositionType="pathRelative"
motion:motionTarget="@id/controlPoint"
motion:pathMotionArc="flip"
motion:percentX="-1.366"
motion:percentY="-1.366" />
……
</KeyFrameSet>
这样中间的白色小球就可以转个圈圈了,把白色小球处理好,整个动画就差不多了,还剩下周围的小球和数字11,只要在对应的关键帧位置,控制缩放就行了,代码这里就不贴了,下方提供的有源码地址可以查看。
总结
MotionLayout 给我提供许多方便的api,再配合Android studio的图形化工具,可以让我们很容易实现一些动画效果,不过要想高度还原效果,还是要花点心思去思考的。关于MotionLayout我用了两篇文章去讲解,但还是有很多内容没有说到,想灵活使用MotionLayout 还需多多练习,多多coding。
~ FIN ~
加好友拉你进群,技术干货聊不停
↓关注公众号↓ | ↓添加微信交流↓ |
---|---|