Android MotionLayout相关

1、概述

I / O '18提到了MotionLayout,当时还没有正式发布前段时间,在今年的6月26日正式发布了ConstraintLayout的2.0alpha版,也算正式推出了MotionLayout。 MotionLayout是ConstraintLayout的子类,它具有ConstraintLayout的所有属性。MotionLayout用来处理两个ConstraintSet之间的切换,并在根据两个ConstraintSet的CustomAttribute参数来自动生成切换动画,关于ConstraintSet下面会讨论。同时MotionLayout所增加的是可以直接通过触摸屏幕来控制动画的运行进度。也就是说MotionLayout会管理你的触摸事件通过跟踪手指的速度,并将其与系统中的视图速度相匹配。从而可以自然地在两者之间通过触摸滑动平稳过渡。并且在动画里面加入了关键帧的概念,使得其自动生成动画在运行时某一阶段会运行到关键帧的状态。同时MotionLayout支持在XML中完全描述一个复杂的动画,而不需要通过Java代码来实现。

ConstraintSet之间切换

2、ConstraintLayout动画

ConstraintLayout中的动画要借助于ConstraintSet。ConstraintSet是一个轻量级对象,表示ConstraintLayout中所有子元素的constraints,margins和padding 。当将 ConstraintSet应用于显示ConstraintLayout时,布局会使用ConstraintSet中的约束来更新对应ConstraintLayout中的。ConstraintSet仅为视图的大小和位置设置动画,不会为其他属性设置动画(例如颜色)。

下面的代码示例显示了如何动画将单个按钮移动到屏幕底部:


public class MainActivity extends AppCompatActivity {

    ConstraintLayout constraintLayout;
    Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.keyframe_one);
        constraintLayout = findViewById(R.id.constraint_layout);

        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                animateToKeyframeTwo();
            }
        });
    }

    void animateToKeyframeTwo() {

        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.load(this, R.layout.keyframe_two); //载入要更新的布局到constraintSet中
        TransitionManager.beginDelayedTransition(constraintLayout); // 开启
        constraintSet.applyTo(constraintLayout);
    }
}


ConstraintSet动画

这边要注意一下,只会运行R.layout.keyframe_two中与R.layout.keyframe_one中id对应的动画。不会出现R.layout.keyframe_two中有而R.layout.keyframe_one中没有的视图,也不会对R.layout.keyframe_one有但R.layout.keyframe_two中没有的视图有任何效果。

3、MotionLayout动画

前面已经演示了怎么对ConstraintLayout布局设置动画,现在来讨论下MotionLayout布局下的动画。

3.1 MotionScene

与通常的布局不同,MotionLayout所做的约束保存在一个单独的XML文件MotionScene中,该文件存储在您的res/xml目录中。

MotionScene包含内容

MotionScene文件可以包含指定动画所需的全部内容,例如前面提到的ConstraintSets、ConstraintSets直接的过渡、关键帧、触摸处理等等。

3.2 创建动画简单流程

3.2.1 先决条件

① Android Studio 3.2.0或更高版本

② 运行Android API等级21或更高版本的设备或模拟器

③ 添加依赖:


implementation 'com.android.support:appcompat-v7:27.0.2'

implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha1'

或者


implementation 'androidx.appcompat:appcompat:1.0.0-beta1'

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha1'

这边要说明一下androidx也是今年IO刚刚推出的一个依赖,用来替代之前的com.android.support依赖。添加了它就不用添加一大堆v4、v7、v13等依赖了。

3.2.2 定义布局

前面提到过MotionLayout是ConstraintLayout的子类,所以MotionLayout可以直接替换ConstraintLayout。因为ConstraintLayout有的功能MotionLayout都有。

3.2.3 创建MotionScene

这一步是MotionLayout的关键,在res下的xml文件夹中创建MotionScene。其实在MotionLayout中可以不用添加想进行动画的视图的约束,而将约束放在ConstraintSet中,在将ConstraintSet放在MotionScene中。


<MotionScene

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/starting_set">

        <Constraint android:id="@+id/actor"

            app:layout_constraintBottom_toBottomOf="parent"

            app:layout_constraintRight_toRightOf="parent"

            android:layout_width="60dp"

            android:layout_height="60dp"/>

    </ConstraintSet >

    <ConstraintSet  android:id="@+id/ending_set" >

        <Constraint android:id="@+id/actor"

            app:layout_constraintTop_toTopOf="parent"

            app:layout_constraintLeft_toLeftOf="parent"

            android:layout_width="60dp"

            android:layout_height="60dp"/>

    </ConstraintSet >

</MotionScene>

这边需要注意的是每个ConstraintSet 里面的元素必须始终指定所需的位置和所需的大小。它会覆盖任何以前设置的布局信息。并且里面的id和MotionLayout中的视图的id要对应才会有反应。

为了帮助MotionLayout 的视图理解必须约束集的顺序,需要创建一个Transition 元素。通过使用其直观命名 constraintSetStart 和constraintSetEnd 属性,可以指定首先应用哪个集合以及最后应用哪个集合。该Transition 元素还允许指定动画的持续时间。


// 放在上面的<MotionScene> 和</MotionScene>之中和ConstraintSet 标签平级。

<Transition

    android:id="@+id/my_transition"

    app:constraintSetStart="@+id/starting_set"

    app:constraintSetEnd="@+id/ending_set"

    app:duration="2000"/>

此时,一个简单的MotionScene完成。但是此时任然没有和MotionLayout进行绑定。需要给MotionLayout添加app:layoutDescription属性来将上面的MotionScene绑定:


app:layoutDescription="@xml/my_scene"

3.2.4 启动动画

运行应用程序时,MotionLayout 视图将自动将constraintSetStart 属性中指定的约束集设置到自己身上。因此,要启动动画,需要做的就是调用transitionToEnd() 方法从而实现ConstraintSet之间的转换:


motion_container.transitionToEnd();

动画效果

3.2.5 动画执行进度监听

可以通过给MotionLayout 设置监听器来监听动画进度,和动画完成时的回调:


motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {

            @Override

            public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {

                seekBar.setProgress((int)(v*100));

            }

            @Override

            public void onTransitionCompleted(MotionLayout motionLayout, int i)             {

            }

});

上面对进度的监听通过seekbar表示出来

动画进度监听

3.3 关键帧(Key Frames)

在上面的动画中,Button小部件看起来像在直线的路径中移动。这是因为MotionLayout 的视图此时其实只有两个关键帧:起始帧Button位于屏幕的右下角,终点帧Button位于屏幕的左上角。如果要改变路径的形状,则必须提供一些介于起点和终点之间关键。

在开始创建关键帧之前,必须将KeyFrameSet 标签添加到MotionScene之中。可以自由创建任意数量的关键帧。


<KeyFrameSet >

            ...

</KeyFrameSet >

MotionLayout 视图支持许多不同类型的关键帧。这里使用其中两种类型:KeyPosition 和KeyCycle 。

3.3.1 KeyPosition

KeyPosition 可以帮助视图改变运动路径的形状。创建它们时,请确保提供目标视图的ID,沿时间轴的位置,可以是0到100之间的任意数字,以及指定X或Y坐标已经运行到的百分比。可以设置type参数指出坐标是相对于实际的X或Y轴,还是相对于路径本身。


<KeyFrameSet >

    <KeyPosition

    app:target="@+id/button"

    app:framePosition="30"

    app:type="deltaRelative"

    app:percentX="0.85"/>

    <KeyPosition

    app:target="@+id/button"

    app:framePosition="60"

    app:type="deltaRelative"

    app:percentX="1"/>

</KeyFrameSet>

上面第一个KeyPosition代表button按钮在运行道30%的时候,相对于运行轨迹x已经运行了85%了。第二个KeyPosition代表button按钮在运行道60%的时候,相对于运行轨迹x已经运行了100%了.效果如下,这样就可以避开和seekbar的冲突了:

KeyPosition关键帧

3.3.2 KeyCycle

KeyCycle用来给动画添加振动。可以通过提供诸如要使用的波形和波形周期等详细信息来配置KeyCycle。下面是KeyCycle支持的各种振动波形:

KeyCycle波形

在上述动画中加入如下KeyCycle


<KeyCycle

    app:target="@+id/button"

    app:framePosition="30"

    android:rotation="50"

    app:waveShape="sin"

    app:wavePeriod="1"/>

KeyCycle关键帧

3.4 交互式动画

上面的动画运行我都是通过对Button按钮设置点击监听事件,然后调用motion_container.transitionToEnd();方法来使他运行的。其实完全不必这么麻烦,因为MotionLayout的视图允许开发者将触摸事件直接附加到视图中。截止到现在,它支持点击和滑动事件。要实现上面实现的点击事件可以在MotionScene中增加代码如下:


<OnClick

    app:target="@+id/button"

    app:mode="transitionToEnd"/>

而可以通过给MotionScene增加OnSwipe标签来使视图通过在屏幕滑动而大运行。在创建该标签时,必须确保提供正确的拖动方向以及应作为拖动控制柄的视图的边。可以这么理解,相对于初始位置,如果想往上滑起到增加动画进度就设置为dragUp,想往下滑起到增加动画进度就设置为dragDown,左右同样道理。至于touchAnchorSide这个参数的本意应该设置拉目标视图的边,但我发现就算不设置touchAnchorSide这个参数或者设置成任意值top bottom或者left right,对动画都没有影响。这可能是MotionLayout的一个bug毕竟现在还只是alpha版。


<OnSwipe

    app:touchAnchorId="@+id/actor"

    app:dragDirection="dragUp"/>

OnSwipe动画

5、MotionEditor

之前一篇讨论ConstraintLayout的文章,基本上都是在布局编辑器中进行操作。这也是ConstraintLayout的一大优点,MotionLayout作为其子类,官方也为它专门提供了强大的可视化编辑器。不过可惜的是,到目前为止还不能使用,下面是MotionEditor的官方预告片的一个节选:

MotionEditor编辑动画

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 8,425评论 2 45
  • 紫荆花又落了一地,身边的你又在何方。 昨天下午花花约我去她家,让我帮忙给正在读高一的燕燕讲解数学。 花花是我小时候...
    安晓晓阅读 72评论 0 2
  • 起風了,唯有努力試著生存。 from「海濱墓園」 一整天都在外頭跑,老人餃子會實在不想說。午後糾結了一下還是決定坐...
    阿飛阅读 54评论 0 1
  • 2018年3月3日 星期六 天气:晴 晚上朋友来电话,说一起出去吃饭。我没有推迟,叫上老婆孩子就去了。 我这个朋友...
    呜呜呜呜呜呜呜呜阅读 24评论 0 0
  • 原创:王漫 左宗棠,湖南湘阴人,晚清中兴儒将,精通韬略,足智多谋。其一生对中华民族,甚至对自己,最大贡献就是率军抵...
    武商路漫漫阅读 978评论 34 28