MotionLayout_ 打开动画新世界大门 (part II),android 插件化原理
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
,并改变它们的属性状态,这里变化的属性只涉及到 scaleX
、scaleY
和 alpha
。
考虑到 KeyAttribute 中提供的属性有限,所以,CustomAttribute 横空出世,它支持任意自定义的属性,常见的有 TextView
的 textColor
、background
或者是 ImageView
的 src
、tint
等。当然还不止这些,我们平时自定义 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:[developer.android.com/reference/a…](
)
下面举个简单的例子帮助理解,以下面这个效果为例:
对应的 KeyFrameSet
代码如下所示:
<KeyFrameSet><KeyCyclemotion:framePosition="0"motion:target="@+id/button"motion:wavePeriod="0"motion:waveOffset="1"motion:waveShape="sin"android:scaleX="0.3"/><KeyCyclemotion:framePosition="18"motion:target="@+id/button"motion:wavePeriod="0"motion:waveOffset="1"motion:waveShape="sin"android:scaleX="0.3"/><KeyCyclemotion: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 来变化的。
关于这款工具的使用,大家可以前往:[github.com/googlearchi…](
) 上下载,然后通过执行 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 的联动。首先,我们先来看下面这个简单的效果:
我们知道,通过 CoordinatorLayout
和 AppBarLayout
也可以实现类似的交互效果,但显然 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>
评论