MotionLayout

219 阅读2分钟

待看

  1. juejin.cn/post/684490…
  2. juejin.cn/post/686031…
  3. juejin.cn/post/684490…
  4. juejin.cn/post/684490…
  5. juejin.cn/post/684490…
  6. juejin.cn/post/684490…
  7. juejin.cn/post/685457…
  8. juejin.cn/post/722069…
  9. juejin.cn/post/722142…
  10. 官方 MotionLayoutDemo

1. MotionLayout简单使用

1. 示例

<?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"
    tools:context=".Case1Activity"
    app:layoutDescription="@xml/case1_scene"
    app:showPaths="true"
    >
    <View
        android:id="@+id/view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/black"
        />
</androidx.constraintlayout.motion.widget.MotionLayout>

case1_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/view">
            //Layout描述位置
            <Layout
                android:layout_width="96dp"
                android:layout_height="96dp"
                android:layout_marginStart="96dp"
                android:layout_marginTop="96dp"
                android:layout_marginEnd="0dp"
                android:layout_marginBottom="0dp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
            <Transform
                android:rotationX="30"
                android:rotationY="30" />
            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/color1" />
            //PropertySet描述可见性,透明度
            <PropertySet android:alpha="0.3"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="192dp"
                android:layout_height="192dp"
                android:layout_marginStart="0dp"
                android:layout_marginTop="0dp"
                android:layout_marginEnd="192dp"
                android:layout_marginBottom="192dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent" />
            <Transform
                android:rotationX="70"
                android:rotationY="70" />
            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/color2" />
            <PropertySet android:alpha="0.8"/>
        </Constraint>
    </ConstraintSet>

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start"
        app:duration="4000"
        app:motionInterpolator="@string/material_motion_easing_accelerated">
        <OnClick
            app:clickAction="toggle"
            app:targetId="@+id/view" />
        <KeyFrameSet>
            <KeyPosition
                app:curveFit="spline"
                app:framePosition="30"
                app:keyPositionType="pathRelative"
                app:motionTarget="@+id/view"
                app:percentHeight="0.6"
                app:percentWidth="0.6"
                app:percentX="0.50"
                app:percentY="0.50"></KeyPosition>
            <KeyAttribute
                android:alpha="0.6"
                android:scaleX="1.2"
                android:scaleY="1.2"
                app:framePosition="30"
                app:motionTarget="@+id/view">
                <CustomAttribute
                    app:attributeName="backgroundColor"
                    app:customColorValue="@color/color4" />
            </KeyAttribute>
        </KeyFrameSet>
    </Transition>
</MotionScene>

2. MotionScene大致结构

<MotionScene ***>
    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/view">
            <Layout *** />
            <Transform *** />
            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/color1" />
            <PropertySet android:alpha="0.3"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        ***
    </ConstraintSet>

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start">
        <OnClick
            app:clickAction="toggle"
            app:targetId="@+id/view" />
        <KeyFrameSet>
            <KeyPosition
                app:framePosition="30"
                app:motionTarget="@+id/view"
                *** />
            <KeyAttribute
                app:framePosition="30"
                app:motionTarget="@+id/view"
                *** />
                <CustomAttribute
                    app:attributeName="backgroundColor"
                    app:customColorValue="@color/color4" />
            </KeyAttribute>
        </KeyFrameSet>
    </Transition>
</MotionScene>
  1. 最外层是MotionScene
  2. 内部是 start ConstraintSet + end ConstraintSet + Transition
  3. ConstraintSet内部包含多个Constraint, 每个Constraint关联MotionLayout中的1个View
    1. Constraint内部包含 Layout, Transform, CustomAttribute, PropertySet
    2. Layout描述该View的位置
    3. Transform描述该View的旋转,位移,缩放,海拔等
    4. PropertySet描述该View的可见性,透明度
    5. CustomAttribute描述其他属性
  4. Transition内部包含点击逻辑,关键帧集合
    1. KeyFrameSet内部包含多个KeyPosition和KeyAttribute
    2. KeyPosition描述该关键帧时候对应View的位置
    3. KeyAttribute描述该关键帧时候对应View的非位置属性

3. MotionLayout配合AppbarLayout

  1. 自定义MotionLayout
open class CustomMotionLayout1 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {

    override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
        val currProgress: Float = -verticalOffset / appBarLayout?.totalScrollRange?.toFloat()!!
        progress = currProgress
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        (parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
    }
}
  1. Activitiy布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    tools:context=".Case2Activity">
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <motionlayout.p1.CustomMotionLayout1
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:minHeight="100dp"
            app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"
            app:layoutDescription="@xml/case2_scene"
            >
            <ImageView
                android:id="@+id/img"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:src="@mipmap/img1"
                android:scaleType="center"
                />
            <TextView
                android:id="@+id/tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="11111111"
                android:textColor="@color/color1"
                android:textSize="32sp"
                android:transformPivotX="0dp"
                android:transformPivotY="0dp"
                />
        </motionlayout.p1.CustomMotionLayout1>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/str_long1" />
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
  1. MotionScene配置
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/img"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            app:layout_constraintBottom_toBottomOf="parent"
            />
        <Constraint
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:rotation="-90"
            app:layout_constraintBottom_toBottomOf="@id/img"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/img"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            />
        <Constraint
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:rotation="0"
            app:layout_constraintBottom_toBottomOf="@id/img"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>
    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@id/start"
        app:duration="2000"
        app:motionInterpolator="@string/material_motion_easing_accelerated" />
</MotionScene>
  1. 起止图像
20230612-190715.jpg 20230612-190720.jpg

4. MotionLayout配合ViewPager

  1. 自定义MotionLayout
open class CustomMotionLayout2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), ViewPager.OnPageChangeListener {
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val currProgress: Float = (position + positionOffset) / 2
        progress = currProgress
    }
    
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        val viewGroup = (parent as? ViewGroup)!!
        for (i in 0 until viewGroup.childCount) {
            val childView: View = viewGroup.getChildAt(i)
            if (childView is ViewPager) {
                childView.addOnPageChangeListener(this)
                break
            }
        }
    }

    override fun onPageSelected(position: Int) {
    }
    override fun onPageScrollStateChanged(state: Int) {
    }
}
  1. Activity布局文件 及 MotionScene文件 image.png
  2. Activity代码
private fun initViews() {
    vp = findViewById(R.id.vp)
    val pages = mutableListOf<View>()
    val page1: TextView = TextView(this)
    page1.textSize = 48F
    page1.layoutParams = ViewPager.LayoutParams()
    page1.text = "111"
    pages.add(page1)
    val page2: TextView = TextView(this)
    page2.textSize = 48F
    page2.layoutParams = ViewPager.LayoutParams()
    page2.text = "222"
    pages.add(page2)
    val page3: TextView = TextView(this)
    page3.textSize = 48F
    page3.layoutParams = ViewPager.LayoutParams()
    page3.text = "333"
    pages.add(page3)
    val adapter = MyPagerAdapter(pages)
    vp.adapter = adapter
}
class MyPagerAdapter : PagerAdapter {
    private var pages: List<View> = mutableListOf()
    constructor(pages: List<View>) : super() {
        this.pages = pages
    }
    override fun getCount(): Int {
        return this.pages.size
    }
    override fun isViewFromObject(view: View, `object`: Any): Boolean {
        return view === `object`
    }
    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        val page: View = this.pages.get(position)
        container.addView(page)
        return page
    }
    override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
        container.removeView(`object` as View?)
    }
}
  1. 运行截图
Screenshot_2023-06-13-13-44-49-167_motionlayout.p1.jpg Screenshot_2023-06-13-13-44-59-237_motionlayout.p1.jpg Screenshot_2023-06-13-13-44-52-933_motionlayout.p1.jpg

5. KeyTrigger

  1. KeyTrigger位于KeyFrameSet中,可以根据动画进度触发关联View的特定事件
  2. 相当于对特定View的动画进度进行监听,在指定进度时,调用该View特定方法

特定View的类

open class KeyTriggerImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    fun show() {
        visibility = VISIBLE
    }
    fun hide() {
        visibility = INVISIBLE
    }
}

xml布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ***>
    <MotionLayout ***>
        <motionlayout.p1.KeyTriggerImageView
            android:id="@+id/img"
            ***/>
    </MotionLayout>
</LinearLayout>

MotionScene

<Transition
    app:constraintSetEnd="@id/end"
    app:constraintSetStart="@+id/start"
    app:duration="10000"
    >
    <KeyFrameSet>
        <KeyTrigger
            app:framePosition="0"
            app:motionTarget="@+id/img"
            app:onCross="show" />
        <KeyTrigger
            app:framePosition="25"
            app:motionTarget="@+id/img"
            app:onCross="hide" />
        <KeyTrigger
            app:framePosition="50"
            app:motionTarget="@+id/img"
            app:onCross="show" />
        <KeyTrigger
            app:framePosition="75"
            app:motionTarget="@+id/img"
            app:onCross="hide" />
        <KeyTrigger
            app:framePosition="100"
            app:motionTarget="@+id/img"
            app:onCross="show" />
    </KeyFrameSet>
</Transition>

6. OnSwipe

<Transition
    app:constraintSetEnd="@id/end"
    app:constraintSetStart="@+id/start">
    <OnSwipe
        app:dragDirection="dragDown"
        app:touchRegionId="@+id/img" />
</Transition>
  • touchRegionId 指的是哪一个控件响应触摸事件
  • dragDirection 拖拽的方向

2. MotionLayout中阶使用

3. MotionLayout高阶使用