Material Design - TransitionMana

作者: Arnold_J | 来源:发表于2018-01-03 15:11 被阅读107次

    材料设计其实在上一篇图片和色彩之后,应该是已经结束了。但是后来又发现忘了动画,这是让材料设计焕发光彩的重点啊。所以又翻开了文档和 google,开始了检索和阅读。

    谷歌在 api 19 中添加了 android.transition 这个包,用于优化安卓动画的体验。但是事实上,api 19 中 transition 中的类寥寥无几,大部分的类都是在 api 21 之后新添加的。那么有人会说了,现在的工程最少也要兼容到 api 19 吧?这样的类,并没有什么使用价值。

    话糙理不糙,我还是先放上 github 上有几千 star 的兼容库链接:这是链接

    这位前辈是真厉害,据说谷歌工程师解决部分 transition 包中 bug 的解决方案都是直接从这里来的。虽然有现成的库可以使用,但是原来的东西还是需要会用。所以来一起看看吧。

    今天,主要看一下这里最基础的 TransitionManager


    google 文档开头就提出了三个类,TransitionManager 、Transition、 Scene。
    根据英文名称不难看出各个类的基本作用:

    • TransitionManager:动画的管理类,其中封装了 Transition 和 Scene
    • Scene:场景,它记录了 ViewTree 的某个时刻的关键帧,它通常作为动画的起始帧和最终帧使用
    • Transition:过渡,它代表了这个动画的过渡方式,包括渐变透明(Fade)、滑动(Slide) 等等

    介绍就到这里,如果需要更多参考信息,可以移步 google 文档


    一、TransitionManager API

    去除参数,只看方法名一共有以下几种:

    方法名 作用 备注
    beginDelayedTransition 以当前帧为起始帧,直到下一次绘制后为结束帧,补齐中间的过渡动画 Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame.
    endTransitions 结束所有过渡动画 Ends all pending and ongoing transitions on the specified scene root.
    go 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 Convenience method to simply change to the given scene using the given transition.
    setTransition 根据传入参数确认起始帧和结束帧,补齐中间的过渡动画 Sets a specific transition to occur when the given pair of scenes is exited/entered.
    transitionTo 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 using the appropriate transition for this particular scene change (as specified to the TransitionManager, or the default if no such transition exists)

    beginDelayedTransition 是一种特别方便,又好用的方法。

    查看源码可以看到这个方法将会发生变化的 ViewGroup 缓存起来,并给它添加了 再次绘制的监听器:

    public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
        if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
            //debug 模式日志
            if (Transition.DBG) {
                Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
                        sceneRoot + ", " + transition);
            }
            //rootView 缓存
            sPendingTransitions.add(sceneRoot);
            if (transition == null) {
                transition = sDefaultTransition;
            }
            //过渡方式
            final Transition transitionClone = transition.clone();
            //设置切换到当前场景的过渡
            sceneChangeSetup(sceneRoot, transitionClone);
            //设置 rootView 为当前场景
            Scene.setCurrentScene(sceneRoot, null);
            //添加绘制监听,在合适时机确认最终帧,并实现过渡
            sceneChangeRunTransition(sceneRoot, transitionClone);
        }
    }
    
    private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
            final Transition transition) {
        if (transition != null && sceneRoot != null) {
            //封装了 OnAttachStateChangeListener 和 OnPreDrawListener
            MultiListener listener = new MultiListener(transition, sceneRoot);
            sceneRoot.addOnAttachStateChangeListener(listener);
            sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
        }
    }
    

    beginDelayedTransition 可以在代码中直接应用过渡, View 变换大小后,它会在下一次绘制的时候执行过渡,从而使得这个过程不那么突兀。

    TransitionManager.beginDelayedTransition(mRootView);
    ViewGroup.LayoutParams layoutParams = mSquareView.getLayoutParams();
    layoutParams.height = newSize;
    layoutParams.width = newSize;
    mSquareView.setLayoutParams(layoutParams);
    
    GIF.gif

    上面的例子是直接使用 LinearLayout 设置背景色获取的方块。当然普通 view 本身的大小变化也可以获得过渡效果,但是如果你使用的是自定义 View 可能你获得的过渡效果和想象的会有所不同。

    这里我们用自定义 View 圆点视图为例,为了让效果更明显,我给这个 PointView 的容器添加了背景,获取效果为:

    GIF.gif

    产生这样的效果,是因为默认的 AutoTransition 是 Fade 和 ChangeBound 的组合,其中大小变化由 ChangeBound 完成,它能达到的效果只对 ViewGroup 生效。

    TransitionManager 后面的方法都与 Scene 相关,看得出 Scene 也是个很重要的类。因此,接下来让我们了解一下 Scene 的用法。

    三、Scene 场景

    文档阅读 : 链接

    我们通常把 Scene 译为场景,这个翻译其实还是挺好的。一个过渡动画其实就是从一个场景到另一个场景的过渡,这里的两个场景我们取名为 起始帧结束帧。而过渡动画,就是将场景中所有的子视图从起始帧移动到结束帧的运动效果。

    1)创建 Scene 对象
    根据文档,我们可以看到,一共有两种方式可以获取到我们需要的 Scene 对象。

    //1.构造器
    Scene(ViewGroup rootView)// 没有过渡信息,使用时需要自行添加过渡动画处理
    Scene(ViewGroup rootView, View layout)//当这个场景进入时,会移除 rootView 中所有的子视图
    //2.静态方法
    getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)
    

    其中对于构造器创建的 Scene 对象,需要注意的事项都已经备注在上面了,一般情况下,我们还是会使用静态方法获取。

    2)一个例子
    我们先来看一个例子:


    起始帧 结束帧

    这是我写的两个布局文件 scene1.xml 和 scene2.xml ,这个布局比较简单,所以也就不贴了,需要注意的是,这里只有一层 ViewGroup ,所有 ImageView 都在同一个层级下,且视图的 id 需要一一对应

    按钮代码:

    mRootView = ((ViewGroup) findViewById(R.id.activity_scene_rootview));
    ((RadioGroup) findViewById(R.id.activity_scene_radiogroup)).setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
            switch (checkedId) {
                case R.id.activity_scene_radiobtn1:
                    Scene scene1 = Scene.getSceneForLayout(mRootView, R.layout.scene1, SceneActivity.this);
                    TransitionManager.go(scene1);
                    break;
                case R.id.activity_scene_radiobtn2:
                    Scene scene2 = Scene.getSceneForLayout(mRootView, R.layout.scene2, SceneActivity.this);
                    TransitionManager.go(scene2);
                    break;
            }
        }
    });
    

    来看一下效果:


    效果图

    3)其他方法
    在文档中,Scene 有 enter() 、exit() 、setEnterAction() 、 setExitAction()
    enter 和 exit 方法就不多说了,在 TransitionManager 中,切换 Scene 的方法中也是调用了它们。关于后面两个方法,我没有找到合适使用他们的场景,但是文档说明中指出,这两个方法用于没有使用布局资源或层次结构定义的场景,或者在这些层次结构更改后需要执行附加步骤的场景。
    有点抽象,之后如果有遇到合适的场景,再看吧。

    四、Transition
    效果图-g.png

    上面是 google 文档的截图,可以看到 Transition 的子类非常丰富。实现不同接口的子类,组合出了多种多样的过渡效果。

    1)介绍
    Transition 类承载了切换到目标场景所有的动画效果,它的子类可以实现一组动画,也可以自定义实现动画。Transition 有两个核心任务:1.记录特定的属性;2.根据记录属性的变化执行过渡动画。

    2)声明一个 TransitionSet 对象
    TransitionSet 对象可以在 xml 文件中初始化,路径为:res/transition
    例如我们创建一个带有 explode、changeBounds、changeTransform、changeClipBounds、changeImageTransform 的过渡动画。

    <transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
        <explode/>
        <changeBounds/>
        <changeTransform/>
        <changeClipBounds/>
        <changeImageTransform/>
    </transitionSet>
    

    相应的,各个 Transition 有多种属性,这里就不再贴出,需要的时候可以浏览文档。

    3)利用 TransitionManager 将动画应用到指定的 View 上

    4)如果 explode、changeBounds 等自带的 Transition 并没有完成你需要的效果,那么你也可以用 transition 标签来声明:

    <transition class="com.arno.CustomTransition"/>
    

    CustomTransition 是一个自定义的动画。自定义动画和自定义 View 很像,它需要继承 Transition 类,并实现三个方法:

    // 记录动画起始帧
    public void captureStartValues(TransitionValues transitionValues)
    // 记录动画终结帧
    public void captureEndValues(TransitionValues transitionValues)
    // 根据记录的起始帧和终结帧的属性,创建动画
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
    

    这里,我们简单实现一个改变布局高度的动画。
    首先在captureStartValues方法中获取动画起始高度属性:

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        if (transitionValues == null) {
            return;
        }
        transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
    }
    

    然后在captureEndValues中获取动画结束高度属性:

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        if (transitionValues == null) {
            return;
        }
        transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
    }
    

    最后在createAnimator中创建动画:

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) { return null;}
    
        final View endView = endValues.view;
    
        final int startHeight = (int) startValues.values.get(VIEW_HEIGHT);
        final int endHeight = (int) endValues.values.get(VIEW_HEIGHT);
    
        ValueAnimator sizeAnimator = ValueAnimator.ofInt(startHeight, endHeight);
        sizeAnimator.setDuration(500);
        sizeAnimator.setInterpolator(new LinearInterpolator());
    
        sizeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int current = (int) valueAnimator.getAnimatedValue();
                endView.getLayoutParams().height = current;
                endView.requestLayout();
            }
        });
    
        AnimatorSet set = new AnimatorSet();
        set.play(sizeAnimator);
    
        return set;
    }
    
    GIF.gif

    之后会再看看把 Share Element 和 Transition 结合,做页面跳转动画。

    以上。

    感谢:
    Material-Animations

    相关文章

      网友评论

      本文标题:Material Design - TransitionMana

      本文链接:https://www.haomeiwen.com/subject/jtvmwxtx.html