查看原文
其他

玩转 MotionLayout:实战效果展示

knight康康 AndroidPub 2022-07-13

在上一篇《MotionLayout :布局中的战斗机 》中介绍了MotionLayout 的基本使用,本文通过一些实战案例进一步展示 MotionLayout 的强大。

本文你将学到

  1. 代码控制转场 

  2. 配合 ViewPager 使用 

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

  4. 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 实现的效果是一个图片透明度逐渐减小,另一个透明度逐渐增大,是同时进行的。因为ImageFilterViewsetCrossfade方法里面的逻辑就是这样写的,我们也没有办法

虽然我们没法改变ImageFilterViewsetCrossfade方法,但是我们可以仿照这种方式改一改,下面就开干了,我们自定义一个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)
srcAlpha100
altSrcAlpha001

根据上表可知,在动画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 ~


推荐阅读
关于Java字节码,了解这些就够了
【Kotlin协程】Channel 与 Flow 深入解析
FragmentFactory 在 Koin 中的应用
打造一个 Kotlin Flow 版的 EventBus


加好友拉你进群,技术干货聊不停


↓关注公众号↓↓添加微信交流↓


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存