MotionLayout初探

MotionLayout初探,第1张

MotionLayout初探 简介 MotionLayout

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
    关键帧周期点,用以定义周期运动的波幅及波形。
KeyPosition

位置关键帧允许你对一个控件的运动路径进行 *** 纵。

  • 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轴来划分坐标系。
KeyAttributes

见名知义,这是用来定义运动路径上某个点的属性变更,包含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的强大不仅仅在于自身动画的实现,还可以与其他控件进行联动从而实现一些惊艳的效果~

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5563455.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存