用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中对应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

推荐阅读:

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

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

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

欢迎关注我的公众号

学习技术或投稿

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值