MotionLayout系列之五

1,182 阅读4分钟

这里介绍一下motionLayout与现有控件结合使用

  • 与CoordinateLayout一起使用: 与其说是与coordinateLayout一起使用,倒不如说是与APPBarLayout一起使用,因为他是充当coordinateLayout中的header的,比如如下
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="false"
    android:background="@color/contentBackground">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:theme="@style/AppTheme.AppBarOverlay">

        <include layout="@layout/motion_09_coordinatorlayout_header"/>

    </com.google.android.material.appbar.AppBarLayout>

    <include layout="@layout/content_scrolling" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

然后看这个include的布局

<com.google.androidstudio.motionlayoutexample.utils.CollapsibleToolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/scene_09"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minHeight="50dp"
    android:fitsSystemWindows="false"
    app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed">

    <ImageView
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorAccent"
        android:scaleType="centerCrop"
        android:src="@drawable/monterey"/>

    <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:transformPivotX="0dp"
        android:transformPivotY="0dp"
        android:text="Monterey"
        android:textColor="#FFF"
        android:textSize="32dp" />

</com.google.androidstudio.motionlayoutexample.utils.CollapsibleToolbar>

这里的根布局是一个自定义的view,在这里面有两个view,同时在根节点声明了layoutDescription这个属性,大致可以判断这个根view是MotionLayout的子类,那么我们仔细看一下

class CollapsibleToolbar @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {

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

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        (parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
    }
}

这里不仅是MotionLayout的子类,他还实现了APPBarLayout的接口,在它setOffsetChange中通过motionLayout的setProgress方法把当前动画完成度传递给MotionLayout。至于这里的动画,motionScene倒是挺简单

<MotionScene
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000"
        motion:motionInterpolator="linear">
        <OnSwipe
            motion:touchAnchorId="@+id/background"
            motion:touchAnchorSide="bottom"
            motion:dragDirection="dragUp" />

        <ConstraintSet android:id="@+id/end">
            <Constraint
                android:id="@id/background"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:alpha="0.2"
                motion:layout_constraintBottom_toBottomOf="parent"/>
            <Constraint
                android:id="@id/label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginBottom="8dp"
                android:rotation="0.0"
                motion:layout_constraintBottom_toBottomOf="@+id/background"
                motion:layout_constraintStart_toStartOf="parent" />
        </ConstraintSet>

        <ConstraintSet android:id="@+id/start">
            <Constraint
                android:id="@+id/background"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:alpha="1.0"
                motion:layout_constraintBottom_toBottomOf="parent"/>
            <Constraint
                android:id="@+id/label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:rotation="-90.0"
                motion:layout_constraintBottom_toBottomOf="@+id/background"
                motion:layout_constraintStart_toStartOf="parent"/>
        </ConstraintSet>
    </Transition>

</MotionScene>
  • 与DrawerLayout一起使用:与DrawerLayout一起使用既可以是content中有动画,也可以使menu中有动画
<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/motionLayout"
    android:background="@color/colorPrimaryDark">

    <include layout="@layout/motion_12_drawerlayout_content"/>

    <include layout="@layout/motion_12_drawerlayout_menu"/>

</androidx.drawerlayout.widget.DrawerLayout>

两个include分别为content和menu,首先看content中的动画

<com.google.androidstudio.motionlayoutexample.utils.DrawerContent
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:layoutDescription="@xml/scene_12_content">

    <include layout="@layout/activity_scrolling"/>

</com.google.androidstudio.motionlayoutexample.utils.DrawerContent>

从这里可以看出根布局的自定义view同样是MotionLayout的子类

class DrawerContent @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), DrawerLayout.DrawerListener {
    override fun onDrawerStateChanged(newState: Int) {
    }

    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        progress = slideOffset
    }

    override fun onDrawerClosed(drawerView: View) {
    }

    override fun onDrawerOpened(drawerView: View) {
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        (parent as? DrawerLayout)?.addDrawerListener(this)
    }
}

通过这个可以看出来是将DrawerLayout的slideOffset值通过MotionLayout的setProgress传递给MotionLayout,从而达到动画效果,比如这里的动画为改变content的大小及margin

<MotionScene
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="250"
        motion:motionInterpolator="linear">

        <ConstraintSet android:id="@+id/start">
            <Constraint
                android:id="@id/content"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                motion:layout_constraintWidth_default="percent"
                motion:layout_constraintWidth_percent="1"
                motion:layout_constraintLeft_toRightOf="@+id/button"
                motion:layout_constraintTop_toTopOf="parent"/>
        </ConstraintSet>

        <ConstraintSet android:id="@+id/end">
            <Constraint
                android:id="@id/content"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                motion:layout_constraintWidth_default="percent"
                motion:layout_constraintWidth_percent="1"
                android:scaleX="0.8"
                android:scaleY="0.8"
                android:layout_marginLeft="180dp"
                motion:layout_constraintLeft_toLeftOf="parent"
                motion:layout_constraintTop_toTopOf="parent"/>
        </ConstraintSet>
    </Transition>

</MotionScene>

这里的0.8最终效果为

如果改为0.5则最终效果为

同样可以将这个动画应用于menu

  • 与viewPager一起使用:其实就是viewpager的滑动决定MotionLayout的动画执行度,也就是viewPager的onPageScrolled中将值传递给MotionLayout,所以定义一个view如下
class ViewpagerHeader @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), ViewPager.OnPageChangeListener {

    override fun onPageScrollStateChanged(state: Int) {
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val numPages = 3
        progress = (position + positionOffset) / (numPages - 1)
    }

    override fun onPageSelected(position: Int) {
    }
}

这里写死三页,接下来看整体布局

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <include layout="@layout/motion_15_parallax" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@+id/motionLayout">

    </com.google.android.material.tabs.TabLayout>

    <androidx.viewpager.widget.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@+id/tabs"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

在动画layout中我们使用刚才定义好的view

<com.google.androidstudio.motionlayoutexample.utils.ViewpagerHeader xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/scene_15"
    android:layout_width="match_parent"
    android:layout_height="230dp">

    <ImageView
        android:id="@+id/mountains"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@drawable/mountains" />

    <ImageView
        android:id="@+id/trees1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/trees"
        tools:layout_editor_absoluteX="23dp"
        tools:layout_editor_absoluteY="130dp" />

    <ImageView
        android:id="@+id/trees2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/trees"
        tools:layout_editor_absoluteX="7dp"
        tools:layout_editor_absoluteY="98dp" />

    <ImageView
        android:id="@+id/car"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/car"
        tools:layout_editor_absoluteX="7dp"
        tools:layout_editor_absoluteY="194dp" />

</com.google.androidstudio.motionlayoutexample.utils.ViewpagerHeader>

至于动画的实现就是几个view的移动,就省略了,然后在activity中关联这个viewPager

if (motionLayout != null) {
    pager.addOnPageChangeListener(motionLayout as ViewPager.OnPageChangeListener)
}