MotionLayout 是一种布局类型,可帮助管理应用中的运动和控件动画。MotionLayout 是 ConstraintLayout 的子类,在其丰富的布局功能基础之上构建而成,可向后兼容 API 级别 14。
MotionLayout 缩小了布局转换与复杂运动处理之间的差距,同时在属性动画框架、TransitionManager 和 CoordinatorLayout 之间提供了各种功能。
除了描述布局之间的转换之外,MotionLayout 还能够为任何布局属性添加动画效果。此外,它本身就支持可交互转换。也就是说,可以根据某个条件(例如用户触控输入)立即显示转换中的任意点。MotionLayout 还支持关键帧(Keyframe),从而实现完全自定义的转换以满足需求。
MotionLayout 是完全声明性的,也就是说可以使用 XML 描述任何转换,无论复杂程度如何。
以上即为MotionLayout的结构图,MotionLayout并没有将运动的过程定义在layout文件中,而是以独立的xml文件(根标签为MotionScene)来对运动进行定义。
MotionScene如图所示,主要有三个主要成员类:
- StateSet
状态集,主要用于存储当前布局内各运动View的一些状态,存在与ConstraintSet的相互转换。 - ConstraintSet
约束集,直接在xml中引入,用以对View进行约束属性控制。一般存在起始态(start)与终止态(end),可以理解为定义了运动的初始与结束的View的布局状态。 - Transition
运动的过渡过程,是启用动画时必须定义的元素。内部的各种属性标签后续会有提到,可以理解为有了这种过渡的设置,才有对应的插值器来改变每一帧View的位置/属性值,从而执行动画。
// constraintlayout2.0+版本导入后 开箱即用 implementation ‘androidx.constraintlayout:constraintlayout:2.0.3’
只要将ConstraintLayout的版本库升级到2.0+了,就已经无感接入了MotionLayout的能力。
// res/layout/activity_sample.xml 布局文件
// res/xml/activity_sample_scene.xml 场景运动描述文件
其实MotionLayout和普通的ConstraintLayout在布局上没有啥区别,因为MotionLayout继承自后者,故在不指定layoutDescription时,所使用的布局约束属性是通用的。但一旦指定layoutDescription后,这里要注意,该属性指向的场景文件内的所有布局属性/参数属性都会覆盖原有布局。
这里定义好后,我们可以看下目前的例子。
这个时候,假设我想改变运动路径怎么办?各位看官别急,Google官方早就想到了这一点,于是推出了KeyframeSet定义,即关键帧集。
KeyframeSet关键帧集允许我们定义好几个属性,这里简单介绍几个比较常用的。
- KeyPosition
关键帧位置点,用以控制该点的位置,使动画在运行到该帧时到达定义的点位。 - KeyAttributes
关键帧属性点,用以设置该点的view属性/自定义属性。 - KeyCycle
关键帧周期点,用以定义周期运动的波幅及波形。
位置关键帧允许你对一个控件的运动路径进行 *** 纵。
- motionTarget *** 作的控件id
- framePosition 帧进度
- keyPositionType 坐标系 后续会提到
- percentX 相对x轴偏移百分比
- percentY 相对y轴偏移百分比
修改一下运动路径文件,就得到了以下的效果。
// res/xml/activity_sample_scene.xml...
KeyPositionType 坐标系
- parentRelative 是以父容器维度来定义坐标系,对应着Android View坐标系。
- deltaRelative 是以状态起始点作为坐标原点,x、y轴仍对应着Android View坐标系。
- pathRelative 是以起始点与结束点两点间的连线作为坐标横轴,并平分y轴来划分坐标系。
见名知义,这是用来定义运动路径上某个点的属性变更,包含alpha、scale、rotation等矩阵变换,还包含了一些自定义属性的定义,原理和属性动画一致,也是通过反射来设置属性值,这里以例子来说明,就不再赘述了。
KeyCycle
即关键周期点,可用于做周期动画。
- wavePeriod 即波动周期 这里0.5为一个循环周期
- waveOffset 属性偏移量
- waveShape 波形类型,可供选择现有库有sin、cos、bounce等,更多可参考官方文档:https://developer.android.com/reference/androidx/constraintlayout/motion/widget/MotionLayout#keycycle
这里同样以例子来说明。
// res/xml/activity_main_scene.xml原理浅析
这里MotionLayout动画的实现原理其实就和我们定义自定义View动画时的原理一致,就是不断地对各子View内部进行一系列插值运算更改其位置和属性,并适时重绘来完成的。
下面我们首先来看下调用的过程中的关键方法。
@Override public void loadLayoutDescription(int motionScene) { if (motionScene != 0) { try { // 根据motionScene的资源文件id来标识创建MotionScene对象 mScene = new MotionScene(getContext(), this, motionScene); // 初始化两个状态 if (mCurrentState == UNSET && mScene != null) { mCurrentState = mScene.getStartId(); mBeginState = mScene.getStartId(); mEndState = mScene.getEndId(); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || isAttachedToWindow()) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Display display = getDisplay(); mPreviouseRotation = (display == null) ? 0 : display.getRotation(); } if (mScene != null) { // 获取scene文件中的约束集 并对原有布局约束进行覆盖 ConstraintSet cSet = mScene.getConstraintSet(mCurrentState); mScene.readFallback(this); if (mDecoratorsHelpers != null) { for (MotionHelper mh : mDecoratorsHelpers) { mh.onFinishedMotionScene(this); } } if (cSet != null) { cSet.applyTo(this); } mBeginState = mCurrentState; } onNewStateAttachHandlers(); if (mStateCache != null) { if (mDelayedApply) { post(new Runnable() { @Override public void run() { mStateCache.apply(); } }); } else { mStateCache.apply(); } } else { if (mScene != null && mScene.mCurrentTransition != null) { if (mScene.mCurrentTransition.getAutoTransition() == MotionScene.Transition.AUTO_ANIMATE_TO_END) { // 如果当前默认自动完成运动过程,那么就执行动画。 transitionToEnd(); setState(TransitionState.SETUP); setState(TransitionState.MOVING); } } } } catch (Exception ex) { throw new IllegalArgumentException("unable to parse MotionScene file", ex); } } else { mScene = null; } } catch (Exception ex) { throw new IllegalArgumentException("unable to parse MotionScene file", ex); } } else { mScene = null; } }
这里MotionLayout在loadLayoutDescription方法执行时,会把定义的约束集向原有布局覆盖,且创建好对应的MotionScene并初始化各类参数以备后续使用;同时,会对该状态以StateCache的结构进行缓存。
在StateCache中,会通过apply方法中调用setTransition来对运动进行初始化。
public void setTransition(int beginId, int endId) { if (!isAttachedToWindow()) { if (mStateCache == null) { mStateCache = new StateCache(); } mStateCache.setStartState(beginId); mStateCache.setEndState(endId); return; } if (mScene != null) { mBeginState = beginId; mEndState = endId; mScene.setTransition(beginId, endId); // 初始化内部类Model mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(beginId), mScene.getConstraintSet(endId)); // 进行估值计算并重绘 内部会调用#reevaluate方法 rebuildScene(); mTransitionLastPosition = 0; transitionToStart(); } }
在内部类Model中,通过reevaluate方法,最终会在setupMotionViews方法中给每个View创建一个对应的MotionController,用以对运动过程每个View进行进度控制以及View的插值计算。由此可以推断出,MotionLayout的子View都必须有id标识。
而MotionLayout的强大之处还在于可以在用户触摸时,响应到动画的进度执行,原因就在于其触摸事件被捕获并交由TouchResponse进行处理。
public boolean onTouchEvent(MotionEvent event) { if (mScene != null && mInteractionEnabled && mScene.supportTouch()) { MotionScene.Transition currentTransition = mScene.mCurrentTransition; if (currentTransition != null && !currentTransition.isEnabled()) { return super.onTouchEvent(event); } // 交由内部TouchResponse来进行消费 mScene.processTouchEvent(event, getCurrentState(), this); return true; } return super.onTouchEvent(event); }
最终会调用TouchResponse的processTouchEvent方法来对拖动进度进行计算,并回调给MotionLayout最终通过调用touchAnimateTo方法来进行重绘更新。
分析过后,大致的流程图如下所示。
FurtherMore这里笔者也通过MotionLayout做了一些扩展菜单的小demo,比起以前通过自定义View的方式实现,代码量足足省了几百行有余,可谓真香!
https://github.com/android/views-widgets-samples/tree/master/ConstraintLayoutExamples
这里是官方推荐的一些Demo,可以发现MotionLayout的强大不仅仅在于自身动画的实现,还可以与其他控件进行联动从而实现一些惊艳的效果~
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)