距离上一篇文章「 MotionLayout:打开动画新世界大门(part I)」已经过去了很久,由于个人原因,MotionLayout 系列文章姗姗来迟。在之前的文章中,我们领略到了 MotionLayout 的魅力,了解到它继承自 ConstraintLayout,并具有它“约束布局”的特性。同时,关于如何创建和使用 MotionScene 及其内部的 KeyFrameSet 也都做了一些简单介绍。那么,本文来带大家进一步探索 KeyFrameSet 这个大家族中的“神秘宝藏”,并针对上文中留下的一些彩蛋进行讲解,来看看如何实现 MotionLayout 与其他控件的联动

再探索 KeyFrameSet

在上文中我们说到 KeyFrameSet 能够让单调的动画独树一帜,可以根据我们的意愿来描述动画运动的轨迹。之前只是比较详细介绍了 KeyFrameSet 这个大家族中的 Keyposition,那么本文就来和大家窥探一下其他宝藏的秘密吧。

首先,我们来看一张熟悉的 MotionLayout 结构图:

从上图我们可以看到,KeyFrameSet 中主要包含了KeyPositionKeyAttribute 以及 KeyCycle 三种类型的关键帧。其实除此以外,KeyFrameSet 还提供了 KeyTimeCycleKeyTrigger,具体的用法和使用场景会在后续文章进行介绍。本文中,我们先来详细看一下 KeyAttribute 以及 KeyCycle

KeyAttribute

我们知道,KeyPosition 描述的是目标 View 在某个位置的关键帧,进而改变动画的移动轨迹,至于 KeyAttribute,则是描述这个 View 在某个关键帧时所处的“状态”,即所谓的”高矮胖瘦“。前者侧重的是改变动画的轨迹,后者则是强调更改 View 自身的属性

从上图的 KeyAttribute 结构图中我们可以看到,它支持各种属性,足够我们来描述一个 View 的状态了。假如我们希望实现如下效果:

其实上面的动画实现很简单,只需要在特定位置添加一些“关键帧”就可以了:

<?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"><Transitionapp:constraintSetStart="@+id/start"app:constraintSetEnd="@+id/end"app:duration="3200"app:motionInterpolator="bounce"><KeyFrameSet><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="20"android:scaleX="1.5"android:scaleY="1.5"android:alpha="0.7"/><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="35"android:scaleX="1"android:scaleY="1"android:alpha="1"/><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="50"android:scaleX="1.5"android:scaleY="1.5"android:alpha="0.7"/><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="65"android:scaleX="1"android:scaleY="1"android:alpha="1"/><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="80"android:scaleX="1.5"android:scaleY="1.5"android:alpha="0.7"/><KeyAttributeapp:motionTarget="@+id/loading_ball"app:framePosition="95"android:scaleX="1"android:scaleY="1"android:alpha="1" /></KeyFrameSet><OnClick app:targetId="@+id/loading_ball"app:clickAction="toggle"/></Transition><ConstraintSet android:id="@+id/start"><Constraintandroid:id="@id/loading_ball"android:layout_width="32dp"android:layout_height="32dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.15"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.5"/></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@+id/loading_ball"android:layout_width="32dp"android:layout_height="32dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.85"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.5" /></ConstraintSet>
</MotionScene>

最终代码如上所示,是不是很 easy?这里我们在途经路线中添加一些特定的 keyAttribute ,并改变它们的属性状态,这里变化的属性只涉及到 scaleXscaleYalpha

考虑到 KeyAttribute 中提供的属性有限,所以,CustomAttribute 横空出世,它支持任意自定义的属性,常见的有 TextViewtextColorbackground 或者是 ImageViewsrctint 等。当然还不止这些,我们平时自定义 View 中提供的自定义属性同样支持哦。就像 GitHub 上的一个 ShapeOfView 的开源项目,可以提供给我们自定义控件形状的功能,那么结合了 MotionLayout 中的 CustomAttribute,我们就可以达到下面这种平滑转换的效果:

举个简单的例子,上面的小球加载动画我们希望它能够在运动过程中颜色也随之变化,然而 中并没有提供相关属性,这里我们就可以借助于 来实现啦。改动部分代码如下所示:

......
<ConstraintSet android:id="@+id/start"><Constraintandroid:id="@id/loading_ball"android:layout_width="32dp"android:layout_height="32dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.15"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.5"><CustomAttributeapp:attributeName="colorFilter"app:customColorValue="@android:color/holo_blue_light"/></Constraint></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@+id/loading_ball"android:layout_width="32dp"android:layout_height="32dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.85"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.5"><CustomAttributeapp:attributeName="colorFilter"app:customColorValue="@color/colorAccent"/></Constraint>
</ConstraintSet>

我们设置了 colorFilter 属性,作用相当于 tint,重新运行后,可以看到如下效果:

需要我们注意的是,这里的自定义属性的 attributeName 对应的值并不一定是在 xml 布局文件中控件对应的属性名称,而是在对应控件中拥有 setter 设置的属性名称。怎么理解呢?其实归根结底 CustomAttribute 内部还是利用的反射,从下面的部分源码中就能够察觉到:

public void applyCustomAttributes(ConstraintLayout constraintLayout) {int count = constraintLayout.getChildCount();for(int i = 0; i < count; ++i) {View view = constraintLayout.getChildAt(i);int id = view.getId();if (!this.mConstraints.containsKey(id)) {Log.v("ConstraintSet", "id unknown " + Debug.getName(view));} else {if (this.mForceId && id == -1) {throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");}if (this.mConstraints.containsKey(id)) {ConstraintSet.Constraint constraint = (ConstraintSet.Constraint)this.mConstraints.get(id);ConstraintAttribute.setAttributes(view, constraint.mCustomConstraints);}}}}......public static void setAttributes(View view, HashMap<String, ConstraintAttribute> map) {Class<? extends View> viewClass = view.getClass();Iterator var3 = map.keySet().iterator();while(var3.hasNext()) {String name = (String)var3.next();ConstraintAttribute constraintAttribute = (ConstraintAttribute)map.get(name);String methodName = "set" + name;try {Method method;switch(constraintAttribute.mType) {case COLOR_TYPE:method = viewClass.getMethod(methodName, Integer.TYPE);method.invoke(view, constraintAttribute.mColorValue);break;case COLOR_DRAWABLE_TYPE:method = viewClass.getMethod(methodName, Drawable.class);ColorDrawable drawable = new ColorDrawable();drawable.setColor(constraintAttribute.mColorValue);method.invoke(view, drawable);break;case INT_TYPE:method = viewClass.getMethod(methodName, Integer.TYPE);method.invoke(view, constraintAttribute.mIntegerValue);break;case FLOAT_TYPE:method = viewClass.getMethod(methodName, Float.TYPE);method.invoke(view, constraintAttribute.mFloatValue);break;case STRING_TYPE:method = viewClass.getMethod(methodName, CharSequence.class);method.invoke(view, constraintAttribute.mStringValue);break;case BOOLEAN_TYPE:method = viewClass.getMethod(methodName, Boolean.TYPE);method.invoke(view, constraintAttribute.mBooleanValue);break;case DIMENSION_TYPE:method = viewClass.getMethod(methodName, Float.TYPE);method.invoke(view, constraintAttribute.mFloatValue);}} catch (NoSuchMethodException var9) {Log.e("TransitionLayout", var9.getMessage());Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());Log.e("TransitionLayout", viewClass.getName() + " must have a method " + methodName);} catch (IllegalAccessException var10) {Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());var10.printStackTrace();} catch (InvocationTargetException var11) {Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());var11.printStackTrace();}}}

首先在 MotionLayout 中,如果是自定义属性,那么会执行 ConstraintSet 类中的 applyCustomAttributes 方法,接着会调用 ConstraintAttribute 类中的 setAttributes 方法,就如上代码中所写的那样,它会根据属性名称组装成对应的 set 方法,然后通过反射调用。是不是有种恍然大悟的感觉?话说,这样的机制是不是好像哪里见到过?没错,正是属性动画

KeyCycle

什么是 KeyCycle 呢?下面是来自 Gal Maoz 的总结:

A KeyCycle is a highly-detailed, custom-made interpolator for a specific view, whereas the interpolator is influencing the entire scene, with a large focus on repetitive actions (hence the cycle in the name).

简单来说,KeyCycle 是针对特定视图的非常详细的定制化插值器。它比较适合我们常说的波形或周期运动场景,比如实现控件的抖动动画或者周期性的循环动画。

如上图所示,KeyCycle 主要由以上几个属性组成,前两个相信大家都比较熟悉了,这里不必多说,另外 view properties 正如之前的 KeyAttribute 结构图中所描述的那样,代表View的各种属性,如 rotation、translation、alpha 等等。 这里主要介绍另外三个比较重要且具有特色的属性:

  • wavePeriod:这个表示在当前场景位置下需要执行动画的波(周期)的数量。这样说可能不太容易理解,别急,我们待会举个例子说明。
  • waveOffset:表示当前控件需要变化的属性的偏移量,即 view properties 所对应的初始值或者基准值。例如,如果我们在动画执行的某个位置设置了 scaleX 为 0.3,而设置了 waveOffset 值为 1,那么,动画执行到该位置,控件的实际宽度会变为 1 + 0.3 = 1.3,也就是会扩大为 1.3 倍,而不是缩小为之前的 0.3 倍。
  • waveShape:这个属性比较好理解,即波的形状,常见的值有:sin、cos、sawtooth 等,更多可参考官网API:https://developer.android.com/reference/androidx/constraintlayout/motion/widget/MotionLayout#keycycle

下面举个简单的例子帮助理解,以下面这个效果为例:

对应的 KeyFrameSet 代码如下所示:

<KeyFrameSet><KeyCycle motion:framePosition="0"motion:target="@+id/button"motion:wavePeriod="0"motion:waveOffset="1"motion:waveShape="sin"android:scaleX="0.3"/><KeyCycle motion:framePosition="18"motion:target="@+id/button"motion:wavePeriod="0"motion:waveOffset="1"motion:waveShape="sin"android:scaleX="0.3"/><KeyCycle motion:framePosition="100"motion:target="@+id/button"motion:wavePeriod="3"motion:waveOffset="1"motion:waveShape="sin"android:scaleX="0"/>
</KeyFrameSet>

根据动画效果结合代码可以知道,我们这个放大的Q弹的效果只是改变了 scaleX 这个属性,并且让它“摇摆了”大概三个来回(周期),恰好 wavePeriod 属性值为 3。也许动画不太方便察觉,这样,我们借助于 Google 提供的专门用来查看 KeyCycle 波形变化的快捷工具来查看它波形变化过程:

如此一来,我们就很直观地看到上图中描绘的波形变化过程了,的确是三个周期没有错,并且是以正弦 sin 来变化的。

关于这款工具的使用,大家可以前往:https://github.com/googlearchive/android-ConstraintLayoutExamples/releases/download/1.0/CycleEditor.jar 上下载,然后通过执行 java -jar [xx/CycleEditor.jar] 即可看到可视化界面,然后将 KeyFrameSet 部分的代码 copy 到编辑栏,然后点击 File -> parse xml 即可看到代码对应的波形走势。如下所示:

我们来看看下面这个效果:

这个Q弹的效果就是基于 KeyCycle 实现的,我们来看看它的场景实现:

<?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"><Transitionapp:constraintSetStart="@+id/start"app:constraintSetEnd="@+id/end"app:motionInterpolator="easeInOut"app:duration="5200"><KeyFrameSet><KeyCycleapp:motionTarget="@+id/image"app:framePosition="10"android:rotationY="22"app:wavePeriod="2"app:waveShape="sin"app:waveOffset="1"/><KeyCycleapp:motionTarget="@+id/image"app:framePosition="30"android:rotationX="15"app:wavePeriod="1"app:waveShape="sin"app:waveOffset="0"/><KeyCycleapp:motionTarget="@+id/image"app:framePosition="65"android:rotationY="14"app:wavePeriod="1"app:waveShape="sin"app:waveOffset="0"/><KeyCycleapp:motionTarget="@+id/image"app:framePosition="92"android:rotationY="0"android:rotationX="2"app:wavePeriod="0"app:waveShape="sin"app:waveOffset="0"/></KeyFrameSet><OnClick app:targetId="@+id/image"app:clickAction="toggle"/></Transition><ConstraintSet android:id="@+id/start"><Constraintandroid:id="@+id/image"android:layout_width="120dp"android:layout_height="120dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.76"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.45"/></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@+id/image"android:layout_width="120dp"android:layout_height="120dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.76"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.45"/></ConstraintSet>
</MotionScene>

我们在动画路径上添加一些关键帧,并稍微改变控件的旋转角度,配合 keyCycle 就能达到上面的弹性动画,大家可以自己动手尝试体验一下。

MotionLayout 的联动性

很多时候,我们的控件并不只是单一的个体,而是需要与其他控件产生“交互上的关联”,常见地,Android 的Material design components 全家桶中提供了一套“优雅灵动”的组件,相信大家都体验过了,那么,我们的 MotionLayout 可以与它们碰撞出怎样的火花呢?

一切从“头”开始

Material design 组件库中提供了一个 AppBarLayout 组件,我们经常使用它来配合 CoordinatorLayout 控件实现一些简单的交互动作,例如头部导航栏的伸缩效果,各位应该或多或少都用到过,这里不再介绍。下面我们就从 AppBarLayout 开始,看看如何实现与 MotionLayout 的联动。首先,我们先来看下面这个简单的效果:

我们知道,通过 CoordinatorLayoutAppBarLayout 也可以实现类似的交互效果,但显然 MotionLayout 会更加灵活多变。其实上面的动画效果很简单,只是在 AppBarLayout 高度变化过程中改变背景色、标题的位置和大小即可,对应的 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"xmlns:motion="http://schemas.android.com/tools"><ConstraintSet android:id="@+id/start"><Constraintandroid:id="@+id/background"android:layout_width="match_parent"android:layout_height="match_parent"motion:layout_constraintBottom_toBottomOf="parent"><CustomAttributeapp:attributeName="backgroundColor"app:customColorValue="@color/blue_magic"/></Constraint><Constraintandroid:id="@+id/tipText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:scaleY="1.6"android:scaleX="1.6"android:alpha="1.0"android:layout_marginStart="62dp"android:layout_marginTop="12dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"/></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@id/background"android:layout_width="match_parent"android:layout_height="match_parent"motion:layout_constraintBottom_toBottomOf="parent"><CustomAttributeapp:attributeName="backgroundColor"app:customColorValue="@color/bgColor_dark"/></Constraint><Constraintandroid:id="@id/tipText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="20dp"android:layout_marginBottom="12dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"/></ConstraintSet><Transitionapp:constraintSetStart="@id/start"app:constraintSetEnd="@id/end"app:duration="4000"><KeyFrameSet><KeyPositionapp:framePosition="60"app:motionTarget="@id/tipText"app:keyPositionType="parentRelative"app:percentY="0.7"/></KeyFrameSet></Transition></MotionScene>

结合以上效果图,我们很容易理解上面的场景实现代码,那么,我们再来看下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/content"android:layout_width="match_parent"android:layout_height="match_parent"android:fitsSystemWindows="false"android:background="@android:color/white"xmlns:app="http://schemas.android.com/apk/res-auto"><com.google.android.material.appbar.AppBarLayoutandroid:id="@+id/appBarLayout"android:layout_width="match_parent"android:layout_height="260dp"android:theme="@style/AppTheme.AppBarOverlay"><com.moos.constraint.widget.MotionToolBarandroid:id="@+id/motionLayout"android:layout_width="match_parent"android:layout_height="match_parent"app:motionDebug="NO_DEBUG"app:layoutDescription="@xml/motion_scene_simple_appbar"android:minHeight="52dp"app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"><Viewandroid:id="@+id/background"android:layout_width="match_parent"android:layout_height="300dp"android:background="@color/blue_magic" /><TextViewandroid:id="@+id/tipText"android:text="Time flies fast"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="20sp"android:textColor="@color/white"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"/></com.moos.constraint.widget.MotionToolBar></com.google.android.material.appbar.AppBarLayout><androidx.core.widget.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:textColor="@color/content_text_color"android:lineSpacingExtra="8dp"android:padding="12dp"android:text="@string/long_text_en"/></androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

观察上面布局文件,其实代码与传统 CoordinatorLayout & AppBarLayout 交互的代码大同小异,只不过我们在 AppBarLayout 内部添加了一个 MotionToolBar 控件,这其实是个 MotionLayout,只不过内部根据 AppBarLayout 伸缩的高度动态改变动画进度而已,我们来看下具体实现:

class MotionToolBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {Log.e("MotionToolBar", "onOffsetChanged: ----->$verticalOffset, scroll range--> ${appBarLayout?.totalScrollRange}")val seekPosition = -verticalOffset / (appBarLayout?.totalScrollRange!!.toFloat()/5*3)progress = seekPosition}override fun onAttachedToWindow() {super.onAttachedToWindow()(parent as? AppBarLayout)?.addOnOffsetChangedListener(this)}
}

代码量很少,通过在 onOffsetChanged 方法中监听 AppBarLayout 的伸缩高度,并经过换算后得到当前的进度值传递给 progress,该字段就对应着 MotionLayout 的 setProgress 方法,如此一来就能够动态的改变其动画进度了。

理解了上述代码,就不难实现下面的效果了:

具体代码就不贴了,文末会附上 GitHub 仓库地址,所有效果实现代码都能够在里面找到。

Lottie 与 MotionLayout 的双剑合璧

Lottie 想必大家都了解过,它是一个动画工具,能够将 UI 的设计动画效果转为 Json 格式的数据文件,然后各端都提供了相应的库来解析并执行动画文件,很多时候需要花费大量时间去借助于代码实现的复杂动画,如今不费吹灰之力就搞定了,很大程度上解放了我们的双手。

那么,Lottie 与 MotionLayout 一起能够碰撞出怎样的火花呢?我们以下面的一个简单效果为例:

其实简单来说,MotionLayout 能够将自身的动画过程与 Lottie 同步,就像图中的安卓机器人动画就是 MotionLayout 实现的,而下面的卡通人物眼神游离的动画则是 Lottie 动画,从图中可以看到,通过手势滑动 ViewPager 两个动画一直保持着“同步运动”。下面我们来看看如何实现的,首先是布局文件,比较简单:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/motionView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@android:color/white"xmlns:app="http://schemas.android.com/apk/res-auto"><com.moos.constraint.widget.ViewpagerHeaderandroid:id="@+id/header"android:layout_width="match_parent"android:layout_height="300dp"app:layoutDescription="@xml/motion_with_view_pager"app:layout_constraintTop_toTopOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:motionProgress="0"><com.airbnb.lottie.LottieAnimationViewandroid:id="@+id/lottieView"android:layout_width="match_parent"android:layout_height="match_parent"app:lottie_rawRes="@raw/face"/><ImageViewandroid:id="@+id/ic_robot"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/ic_launcher_foreground"/></com.moos.constraint.widget.ViewpagerHeader><com.google.android.material.tabs.TabLayoutandroid:id="@+id/tabLayout"android:layout_width="match_parent"android:layout_height="wrap_content"app:tabSelectedTextColor="@color/colorAccent"app:tabTextColor="@color/content_text_color"app:layout_constraintTop_toBottomOf="@+id/header"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"></com.google.android.material.tabs.TabLayout><androidx.viewpager.widget.ViewPagerandroid:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="0dp"app:layout_constraintTop_toBottomOf="@+id/tabLayout"app:layout_constraintBottom_toBottomOf="parent"></androidx.viewpager.widget.ViewPager>
</androidx.constraintlayout.widget.ConstraintLayout>

至于这个 ViewPagerHeader 相信大家也猜到了,其实也是个 MotionLayout :

class ViewpagerHeader @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), androidx.viewpager.widget.ViewPager.OnPageChangeListener {override fun onPageScrollStateChanged(state: Int) {}override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {val animateProgress = (position.toFloat() + positionOffset)/3Log.e("LottieMotionActivity", "viewpager scroll progress is: $animateProgress")progress = animateProgress}override fun onPageSelected(position: Int) {}
}

只不过它内部实现了 ViewPageronPageChangeListener,以监听页面的滑动状态,然后计算出此时 MotionLayout 的动画进度,这里由于 json 动画文件存在问题,所以只截取了一部分动画过程来执行。说了这么多,它的 MotionScene 是什么样的呢?其实很 easy:

<?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"xmlns:app="http://schemas.android.com/apk/res-auto"><Transitionmotion:constraintSetStart="@+id/start"motion:constraintSetEnd="@+id/end"></Transition><ConstraintSet android:id="@+id/start"><Constraintandroid:id="@id/lottieView"android:layout_width="match_parent"android:layout_height="match_parent"motion:progress="0"/><Constraintandroid:id="@id/ic_robot"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="20dp"motion:layout_constraintTop_toTopOf="parent"motion:layout_constraintStart_toStartOf="parent"><CustomAttributeapp:attributeName="colorFilter"app:customColorValue="@android:color/holo_blue_light"/></Constraint></ConstraintSet><ConstraintSet android:id="@+id/end"><Constraintandroid:id="@id/lottieView"android:layout_width="match_parent"android:layout_height="match_parent"motion:progress="1"/><Constraintandroid:id="@id/ic_robot"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="20dp"motion:layout_constraintTop_toTopOf="parent"motion:layout_constraintEnd_toEndOf="parent"><CustomAttributeapp:attributeName="colorFilter"app:customColorValue="@color/colorAccent"/></Constraint></ConstraintSet>
</MotionScene>

唯一需要值得注意的是:这里我们分别在 MotionLayout 的起始位置和终止位置设置了 motion:progress 属性为 0 和 1,由于 LottieAnimationView 内部拥有 setProgress 方法,这样做的目的就是将 Lottie 的动画过程与 MotionLayout 进行绑定,我们只需要改变这个属性,就能够间接控制 Lottie 动画啦。

最后,我们只需要在 Activity 中设置如下代码就可以成功执行啦:

 val adapter = ViewPagerAdapter(supportFragmentManager)adapter.addPage("Now", R.layout.holder_layout)adapter.addPage("Discover", R.layout.holder_layout)viewPager.adapter = adaptertabLayout.setupWithViewPager(viewPager)viewPager.addOnPageChangeListener(header as              androidx.viewpager.widget.ViewPager.OnPageChangeListener)

当然,MotionLayout 还能和很多组件进行联动,篇幅有限就不一一介绍啦,到这里,我们本篇文章内容也差不多该告一段落了,关于 MotionLayout 系列文章的所有示例代码都能够在 GitHub 仓库中找到:

https://github.com/Moosphan/MotionLayoutSamples

后续

如此一来,MotionLayout 系列已经完成两篇文章了,剩下的内容应该还需要一篇文章来容纳,后续可能还会额外提供一篇实战系列文章。下一篇文章主要介绍 KeyFrameSet 家族最后一个成员以及 MotionLayout 多状态场景的使用,同时,也会介绍如何实现与 RecyclerView “强强联合”。最后,Google 在 Android studio 4.2 终于推出了 Motion Editor 工具,下篇文章也会通过一个小实战项目来介绍其用法,拭目以待。

笔者说

最近这两篇文章都尽量做到每个重要知识点都提供一个实战的小示例,力求做到加深理解,文中很多内容都参考自 Nicolas Roard 对于 MotionLayout 的系列教程和 Android 官方文档,并加入自己的理解。从去年编撰第一篇文章时来看,国内对于 MotionLayout 的系列文章非常少,写文章的目的其实很简单,让自己消化新知识的同时,也能够让更多国人知道、认识和尝试使用 MotionLayout 这个全新的动画组件。

由于个人技术能力和表述能力有限,很多内容可能并没有讲解全面和透彻,如果有什么建议或者问题,欢迎留言区探讨,一起进步。

带你领略MotionLayout的魅力(中)相关推荐

  1. 这5部不容错过的超高评价纪录片,带你领略地球的魅力!

    全世界只有3.14 % 的人关注了 爆炸吧知识 地球是茫茫宇宙星系中唯一生机勃勃万物生存的星球,它是瞬息万变.充满自然奇观的世界.50多亿年来,地球在日复一日.沧海桑田地变化着.今天就给大家带来最顶尖 ...

  2. 3.14圆周率节,这5本书带你领略数学的魅力

    3月14日,因π这个神奇的常数,被称为"π日(Pi Day)",还在2020年成为了国际数学日.2019年11月26日,联合国教科文组织宣布3月14日为国际数学日,为"庆 ...

  3. python写一个类600行代码_带你领略算法的魅力,一个600行代码的分词功能实现(一)...

    为什么要说分词呢?其实这个话题挺大的.所以准备分几篇来写,这次先写第一篇. 写给别人看,也写给自己.毕竟,自己在思特奇也做了好久了,写点有意思的东西,结交一些有兴趣的朋友. 一是确实最近的一些实践给了 ...

  4. 贝尔机:带你领略编码的魅力

    当祖思凭一己之力开启德国现代计算机的历史,大西洋彼岸的美国也毫不示弱地完成了本土的设备升级.和前者的孤军奋战不同,后者主体是上世纪叱咤风云的贝尔实验室. 众所周知,贝尔实验室及其所属公司是做电话起家. ...

  5. python写一个类600行代码_带你领略算法的魅力,一个600行代码的分词功能实现(二)...

    从大学毕业到工作的开始几年,一直觉得大学期间学的线性代数,离散数学,概率论简直是浪费时间. 那时候实际做的代码,大部分都是数据进销存.数据输入到数据库介质中的转换,CS,BS架构都写过一些.总觉得现实 ...

  6. 手把手带你领略graphql的魅力

    背景介绍 当谈论到客户端与服务端之间的网络请求时,REST 绝对是连接两者的方案中最流行的选择.在 REST 中,所有概念都是可以通过 URL 可访问的资源演化而来的.你可以通过一个 HTTP GET ...

  7. ChatGPT+Midjourney,带你领略古诗词的魅力

    说起 Midjourney,AI 绘画界的出图质量无人怀疑,特别是 V5 版本的上线,解决了人体的手画不好的瑕疵之后,更是迎来一波好评.国外版本的话,需要用 Discord 软件与之交互,对普通用户而 ...

  8. BATJ大数据架构师带你领略实时计算框架Flink的魅力!

    你是不是经常体验或看到以下这些场景? "小张,你看能不能做个监控大屏实时查看促销活动销售额(GMV)?" "小王,我们现在搞促销活动能不能实时统计销量 Top3 啊?&q ...

  9. 高端科普系列——领略前沿科学的魅力

    导读:量子计算是什么?数据科学如何影响着人们的生活?近几十年来许多技术创新和成果都依赖于算法思想?奇点的惊人预测是否会成为现实?--华章高端科普系列带你领略前沿科学的魅力! 01 <人人可懂的量 ...

最新文章

  1. s-sed替换或者修改文件指定行,同时匹配多个字符串,替换换行符为指定字符
  2. 最“燃”研究生!浙工大 64 岁研究生毕业,老师称其毕业论文写的最好
  3. Linux内部的时钟处理机制全面剖析
  4. 文件包含中过滤了php,文件包含漏洞---php协议
  5. 如何实现windows和linux之间的文件传输
  6. 怎样获取SQL Server 视图中的内容(视图定义)
  7. 从Activity返回结果
  8. js如何调用h5的日期控价_微信公众号支付H5调用支付解析
  9. beanutil 批量copy_BeanUtils.copyProperties 需要getset方法支持
  10. OSPF邻接关系的建立步骤
  11. java获取字典所有的key_JAVA脱水学习-java集合介绍,常用集合类
  12. HBuilder创建app 基础
  13. 989. 数组形式的整数加法
  14. 【渝粤教育】国家开放大学2018年秋季 7048-21T危急重症护理学(本) 参考试题
  15. IE8_XP安装包.zip
  16. 计算机flash逐渐淡入,Flash air制作淡入淡出窗体动画效果
  17. mcgscom口针脚定义_【各种接口针脚定义]
  18. 最受欢迎的 50 个数据可视化图表
  19. 【Visual C++】游戏开发四十八 浅墨DirectX教程十六 三维地形系统的实现
  20. 统一语言为什么不能统一(一)

热门文章

  1. CoBOT Java安全漏洞检测类型与OWASP TOP 10对应关系
  2. win10计算机亮度无法调节,Win10电脑无法调节亮度怎么办 Win10系统不能调节屏幕亮度解决方法...
  3. centos网卡启动故障报错
  4. 微信c语言刷题软件,独家全能微信刷题小程序正式上线(经典秒杀口诀模块全民共享)...
  5. 计算机一级照片像素调整多少钱,怎么调整图片分辨率
  6. 10G 82599EB 网卡测试优化 ethtool
  7. HCIE-Routing Switching V3.0模拟试题.docx
  8. 我分析了b站10万条弹幕,发现了歪嘴战神的终极奥义!(文末重磅福利)
  9. 计算机c盘如何腾出空间,电脑C盘空间不足,又不知道如何清理?做完这几步,能腾出几十个G...
  10. Spring boot整合Redis(入门教程)