用MotionLayout实现这些不可思议的效果

2023-10-18 06:59

本文主要是介绍用MotionLayout实现这些不可思议的效果,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


/   今日科技快讯   /

近日,据外媒报道,自从研发电动汽车的计划取消后,家电品牌戴森正对其产品路线图进行重组,承诺在人工智能、机器人和能源储存等领域进行投资。该公司表示,计划在未来5年投资27.5亿英镑(约合36.6亿美元)用于开发新技术和产品。

/   作者简介   /

大家周一好,这周开始,冬天就正式来临了,小伙伴们注意保暖哦~

本篇来自knight康康的投稿,给大家讲解MotionLayout的使用秘籍,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

knight康康的博客地址:

https://blog.csdn.net/knight1996

/   正文   /

本文探讨MotionLayout的使用,来领会一下MotionLayout的???????? 之处。

本文你将学到

  1. 使用代码操控

  2. 和其它组件配合使用

  3. 仿华为拨号界面动画效果

  4. Android 11 彩蛋制作

使用代码操控

为什么要用代码控制转场呢,xml写着不香吗???? ?xml写着很方便,但是有时我们需要动态的改变转场的效果,就需要通过代码来实现了。比如初始状态是xml描述的转场效果,当我们点击按钮时,变成另外一种转场效果,下面就来实现一下吧。

示例

实现前,先看下效果:

初始转场动画是一个简单的位移动画。

点击状态1按钮,转场动画变成除了平移转场动画外, 还有rotationY从1到180 和alpha从1到0.1的过度转场动画。点击状态2按钮,平移转场结束的位置变了。

核心代码

  //按钮1    binding.btnState1.setOnClickListener {// 0️⃣ 通过id获取ConstraintSet,id 是xml中对应idval constraintSet = binding.motionLayout.getConstraintSet(R.id.end)//1️⃣ 通过id获取ConstraintconstraintSet.getConstraint(R.id.box).apply {// 2️⃣//设置layout 的一些属性//↓↓这行代码↓↓ =>layout_constraintTop_toTopOf="parent"layout.topToTop = Constraints.LayoutParams.PARENT_IDlayout.bottomToBottom = Constraints.LayoutParams.PARENT_IDlayout.endToEnd = Constraints.LayoutParams.PARENT_ID//transform 可以改变旋转缩放等属性transform.rotationY = 180ftransform.rotationX = 0fpropertySet.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_IDlayout.endToEnd = Constraints.LayoutParams.PARENT_ID//去除约束将View 放到右上角layout.bottomToBottom = Constraints.LayoutParams.UNSETtransform.rotationX = 180ftransform.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"><Layoutandroid:layout_width="100dp"android:layout_height="100dp"motion:layout_constraintBottom_toBottomOf="parent"motion:layout_constraintEnd_toEndOf="parent"motion:layout_constraintTop_toTopOf="parent" /><Transformandroid: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_IDlayout.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方式--><includeandroid:id="@+id/motionLayout"layout="@layout/viewpager_header"/><com.google.android.material.tabs.TabLayoutandroid:id="@+id/tabs"android:layout_width="match_parent"android:layout_height="wrap_content" /><androidx.viewpager.widget.ViewPagerandroid: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"><ImageViewandroid: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"><Transitionmotion:constraintSetEnd="@+id/end"motion:constraintSetStart="@id/start"motion:duration="1000"motion:pathMotionArc="flip"></Transition><ConstraintSet android:id="@+id/start"><Constraintandroid: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"><Constraintandroid: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.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent" /><!--数组键盘--><ImageViewandroid: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" /><!-- 底部导航栏--><ImageViewandroid: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" /><!--拨号键--><ImageViewandroid: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"><Transitionmotion:constraintSetEnd="@+id/end"motion:constraintSetStart="@id/start"motion:duration="1000"><OnSwipe/></Transition><ConstraintSet android:id="@+id/start"><Constraintandroid: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"/><Constraintandroid: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"><Constraintandroid: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" /><Constraintandroid: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.ImageFilterViewandroid: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"><Constraintandroid:id="@+id/telKeyboard"…… /><Constraintandroid:id="@+id/imgPhone"……><CustomAttributemotion:attributeName="crossfade"motion:customFloatValue="0" /></Constraint></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@+id/telKeyboard"…… /><Constraintandroid:id="@+id/imgPhone"……><CustomAttributemotion:attributeName="crossfade"motion:customFloatValue="1" /><Transformandroid: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.ImageFilterViewandroid: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 = 0fset(value) {field = valuelayerDrawable?.getDrawable(0)?.alpha = (255*value).toInt()invalidate()}/*** 控制altSrc图片透明度*  value [0,1]*/var altSrcAlpha = 0fset(value) {field = valuelayerDrawable?.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 = 0layerDrawable = 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.CrossFadeImageViewandroid: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"><Transitionmotion:constraintSetEnd="@+id/end"motion:constraintSetStart="@id/start"motion:duration="1000"><OnSwipe/><!-- 2️新增code--><KeyFrameSet><KeyAttributemotion:framePosition="80"motion:motionTarget="@id/imgPhone"><CustomAttributemotion:attributeName="srcAlpha"motion:customFloatValue="0" /><CustomAttributemotion:attributeName="altSrcAlpha"motion:customFloatValue="0" /></KeyAttribute></KeyFrameSet></Transition><ConstraintSet android:id="@+id/start">……<Constraintandroid:id="@+id/imgPhone"……><!-- 0️ 新增code--><CustomAttributemotion:attributeName="srcAlpha"motion:customFloatValue="1" /><CustomAttributemotion:attributeName="altSrcAlpha"motion:customFloatValue="0" /></Constraint></ConstraintSet><ConstraintSet android:id="@+id/end">……<Constraintandroid:id="@+id/imgPhone"……><!--1️新增code--><CustomAttributemotion:attributeName="srcAlpha"motion:customFloatValue="0" /><CustomAttributemotion:attributeName="altSrcAlpha"motion:customFloatValue="1" /><Transformandroid: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π-π/4var angrad = 2 * Math.PI - Math.PI / 4f//圆的半径r = 1 / (2 * sin(Math.PI / 12))//循环计算各个点的坐标for (i in 0..10) {calculatePoint(angrad)//每个点之间的角度间隔Math.PI / 6fangrad -= 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><KeyPositionmotion:framePosition="9"motion:keyPositionType="pathRelative"motion:motionTarget="@id/controlPoint"motion:pathMotionArc="flip"motion:percentX="-0.866"motion:percentY="-0.5" /><KeyPositionmotion: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

推荐阅读:

Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

我的新书,《第一行代码 第3版》已出版!

Android上的网络抓包原来是这样工作的

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

这篇关于用MotionLayout实现这些不可思议的效果的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/230951

相关文章

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

PostgreSQL中MVCC 机制的实现

《PostgreSQL中MVCC机制的实现》本文主要介绍了PostgreSQL中MVCC机制的实现,通过多版本数据存储、快照隔离和事务ID管理实现高并发读写,具有一定的参考价值,感兴趣的可以了解一下... 目录一 MVCC 基本原理python1.1 MVCC 核心概念1.2 与传统锁机制对比二 Postg

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

OpenCV实现实时颜色检测的示例

《OpenCV实现实时颜色检测的示例》本文主要介绍了OpenCV实现实时颜色检测的示例,通过HSV色彩空间转换和色调范围判断实现红黄绿蓝颜色检测,包含视频捕捉、区域标记、颜色分析等功能,具有一定的参考... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal