/ 今日科技快讯 /
近日,据外媒报道,自从研发电动汽车的计划取消后,家电品牌戴森正对其产品路线图进行重组,承诺在人工智能、机器人和能源储存等领域进行投资。该公司表示,计划在未来5年投资27.5亿英镑(约合36.6亿美元)用于开发新技术和产品。
/ 作者简介 /
大家周一好,这周开始,冬天就正式来临了,小伙伴们注意保暖哦~
本篇来自knight康康的投稿,给大家讲解MotionLayout的使用秘籍,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
knight康康的博客地址:
https://blog.csdn.net/knight1996
/ 正文 /
本文探讨MotionLayout的使用,来领会一下MotionLayout的???????? 之处。
本文你将学到
使用代码操控
和其它组件配合使用
仿华为拨号界面动画效果
Android 11 彩蛋制作
使用代码操控
为什么要用代码控制转场呢,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对象来改变一些约束条件。综上所述,通过代码动态改变转场大致有以下三步。
通过MotionLayout的getConstraintSet(id:Int)获取要修改的的约束集ConstrainSet:
val constraintSet = binding.motionLayout.getConstraintSet(R.id.end)
通过ConstrainSet的getConstraint(id:Int)的方法获取对应Contraint。
val constraint=constraintSet.getConstraint(R.id.box)
拿到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
……
和其它组件配合使用
和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的联动。如果其他控件想实现联动也是类似方式。
仿EMUI 11 拨号界面动画
原效果:
简单的分析一下,当我们滑动列表时,键盘和拨号键做位移动画,其他的细节先不管,这个用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的过程中,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渐渐显示,这一过程和效果大致相同。
运行效果如下:
这个效果就和原效果差不多,大致思路就是这样了。当然还是有很多细节很处理了,这里就不在继续细化了,感兴趣的可以下载下面提供的代码改一改。
Android 11 彩蛋制作
效果展示
效果用MotionLayout 还是比较简单的,就是中间白色小球的环形轨迹计算的有点麻烦。下面就说一说我对白色小球计算的思路(有点麻烦,如果有比较好的方法,欢迎大家在评论区讨论 ???? )
先把上面图抽象一下????。
如果我们指定开始和结束位置如上图所示,在我们没有做处理的情况下,小球肯定就走直线直接到结束位置,我们想让它绕个圈圈再到结束位置,那么就要使用关键帧,让它沿着圆走,那么我们就要计算圆上各个点的位置。
image.png我们要想知道各个点的位置,就需要先确定坐标系,这里我采用的是pathRelative 坐标系效果如下image.png。
只是单纯旋转不会影响各个点的坐标值。
但是上面坐标系,想计算各个点,还是比较麻烦,我们可以把坐标原点移动到圆心位置,这样会改变各个点的坐标值,但是这样会变的好算一些。我们可以计算出原点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。
本文相关源码(github)
https://github.com/wkk-knight/knight_blog_demo/tree/master/MotionLayoutDemo
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注