布景

由于项目需求要为CheckBox和RadioButton增加切换动画,以达到个性化的UI组件效果,详细来说项目需求的切换动画为杂乱动画,即无法经过简略的平移,旋转,缩放等根本图形变换来模拟。经过查找资料,发现有如下几种完结动效的办法:

  1. ObjectAnimator,用来完结如平移,旋转,缩放等最根本的动效,无法满足项目要求。
  2. 自界说View专门来完结动画,这种成本很高,别的彻底自界说也无法直接运用CheckBox的各项功用。
  3. VectorAnimatorDrawable,看起来很靠谱,能够完结很杂乱的动画。但最大的难题是UI无法直接输出对应的资源,UI同学一般供给的动效资源是动效json,GIF,视频等资源,想要将其转化为VectorAnimatorDrawable资源非常困难,杂乱一点的动效,要想彻底复原规划稿根本不行能。
  4. Lottie动画,这也是Android开发领域较为主流的完结杂乱动画的手段,这也是我终究选用的计划。但一般Lottie动画都是直接经过的LottieAnimationView(承继自ImageView)来运用的,怎样将其与CheckBox进行结合是需求考虑的问题,下边也将详细描述Lottie动画怎样结合CheckBox来完结切换动效功用。

剖析&完结

首要上边的介绍都是关于CheckBox怎样进行动画的,但关于RadioButton其实没提到。这是由于CheckBox和RadioButton本质上是同一类切换按钮,他们完结动效的思路也根本共同。别的从Android完结的视点来说他们也都是承继自CompoundButton的,该组件的特点是有选中和未选中两种状况,会依据点击事件切换状况。后边咱们也将仅介绍基于CheckBox的动效完结,RadioButton根本能够复用该完结。

问题1. CheckBox动画该怎样做,切状况的机遇是在播动画前还是后。(借鉴Switch组件完结)

首要CheckBox切换动画为setChecked(),该办法直接在其父类CompundButton中界说

    @Override
    public void setChecked(boolean checked) {
        if (mChecked != checked) {
            mCheckedFromResource = false;
            mChecked = checked;
            refreshDrawableState();
            // ... 省掉部分无关代码
        }
    }

CheckBox/RadioButton切换动效实现

该办法直接修正了mChecked状况,并没有供给任何关于动画播映的hook点。

一个小插曲,由于此次需求中还有Switch组件动效的开发,所以经过对Switch组件的研究,找到了播映动画的切入点。Switch组件经过重写setChecked()完结了动画播映(文章最终有展开介绍,实践是在SwitchCompat类中)。

    @Override
    public void setChecked(boolean checked) {
        super.setChecked(checked);
        // Calling the super method may result in setChecked() getting called
        // recursively with a different value, so load the REAL value...
        checked = isChecked();
        if (checked) {
            setOnStateDescriptionOnRAndAbove();
        } else {
            setOffStateDescriptionOnRAndAbove();
        }
        // 假如View仍然在View树上,则播动画;否则不播,直接切换状况
        if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
            animateThumbToCheckedState(checked);
        } else {
            // Immediately move the thumb to the new position.
            cancelPositionAnimator();
            setThumbPosition(checked ? 1 : 0);
        }
    }

CheckBox/RadioButton切换动效实现

在Switch组件中咱们找到了动画播映的办法:先切换check状况,再播动画(animateThumbToCheckedState),所以现在咱们能够得出CheckBox的动画播映结构(重写setChecked())

    @Override
    public void setChecked(boolean checked) {
        super.setChecked(checked);
        checked = isChecked();
        // 假如View仍然在View树上,则播动画;否则不播,直接切换状况
        if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
            animateThumbToCheckedState(checked);
        } else {
            cancelAnimator();
        }
    }

CheckBox/RadioButton切换动效实现

剩下的作业就是填充下面两个办法,全体来说就是处理详细动画该怎样播

  1. animateThumbToCheckedState(checked)
  2. cancelAnimator()

问题2:动画该怎样播

经过前边的剖析,咱们现已选定了Lottie动画作为播映动画的计划。而且预期的CheckBox切换流程为(以uncheck -> checked为例):

  1. mChecked状况先变化,但不期望看到icon的突变
  2. 播映Lottie动画,动画播映盖在原来的icon之上
  3. 动画播映结束,icon变为状况切换后checked状况

在这里我挑选了LayerDrawable + StateListDrawable + LottieDrawable来完结功用。

由于需求在CheckBox的icon中播动画,icon是Drawable类型,所以直接运用LottieDrawable,而且动画要盖在静态icon上,所以运用LayerDrawable来组合多个Drawable

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <selector>
            <item android:state_checked="true"
                android:drawable="@drawable/checkbox_bg_checked_normal" />
            <item
                android:drawable="@drawable/checkbox_bg_unchecked_normal" />
        </selector>
    </item>
</layer-list>

CheckBox/RadioButton切换动效实现

初始化作业

在确定好计划后,在完结上边两个办法前,我需求先做一些初始化作业(加载动画资源,将LottieDrawable动态加入到LayerDrawable中)。

    private fun initAnimation() {
        // btnDrawable为Checkbox的icon
        if (btnDrawable is LayerDrawable) {
            val layerDrawable = btnDrawable
            if (layerDrawable.numberOfLayers < 1) return
            // 创建LottieDrawable
            checkStateChangeDrawable = LottieDrawable()
            // drawable有复用机制(详见DrawableCache类),需求判断顶层drawable是否是LottieDrawable,假如是则替换相应drawable
            if (layerDrawable.getDrawable(layerDrawable.numberOfLayers - 1) is LottieDrawable) {
                layerDrawable.setDrawable(
                    layerDrawable.numberOfLayers - 1,
                    checkStateChangeDrawable
                )
            } else {
                layerDrawable.addLayer(checkStateChangeDrawable)
            }
            // innerDrawable为Checkbox切换后的静态icon资源,LottieDrawable要和innerDrawable宽高对齐
            val innerDrawable = layerDrawable.getDrawable(0)
            val innerDrawableBounds: Rect = innerDrawable.bounds
            checkStateChangeDrawable.alpha = 0
            checkStateChangeDrawable.bounds = innerDrawableBounds
            // 设置动画播映监听,开端动画时动画drawable可见,静态drawable不行见,结束时则相反,来完结动画播映的无缝切换
            checkStateChangeDrawable.addAnimatorListener(object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animation: Animator) {
                    innerDrawable.alpha = 0
                    checkStateChangeDrawable.alpha = 255
                }
                override fun onAnimationEnd(animation: Animator) {
                    innerDrawable.alpha = 255
                    checkStateChangeDrawable.alpha = 0
                }
            })
            // 预备动画资源
            prepareAnimationResource()
        } else if (btnDrawable != null) {
            Log.e(TAG, "Only support LayerDrawable for CompoundButton!")
        }
    }

CheckBox/RadioButton切换动效实现

注:这里代码选用了kotlin,主要由于封装的工具类是用kotlin来完结的,不影响思路。

初始化作业中,咱们主要有以下几步:

  1. 创建LottieDrawable

    1. 注:下边解析不影响全体流程阅览

    2. 由于drawable的复用机制(详见DrawableCache类),在退出当前页面后重新进入的场景下,重新加载btnDrawable时则会触发drawable的复用机制,导致拿到的仍然是同一个LayerDrawable(目标不同,但资源相同,也意味着,lottileDrawable现已在之前被加入到了LayerDrawable中了),这个时候不做特别处理则会导致重复增加,也会有如下错误日志。

      Invalid drawable added to LayerDrawable! Drawable already belongs to another owner but does not expose a constant state.
      

      CheckBox/RadioButton切换动效实现

  2. 将LottieDrawable与静态Drawable(实践类型为StateListDrawable)宽高对齐

  3. 设置动画播映监听,以完结动画播映的无缝切换

  4. 预备动画资源,这一步独自下边介绍

初始化-预备动画资源

    private fun prepareAnimationResource() {
        if (btnDrawable == null) return
        // 加载撤销选中动画
        LottieCompositionFactory.fromAsset(context, uncheckAnimAsset).apply {
            addListener { result: LottieComposition ->
                lottieCompositions[0] = result
                // 初始化动画size
                checkStateChangeDrawable.composition = result
                checkStateChangeDrawable.scale = btnDrawable.bounds.width().toFloat() / checkStateChangeDrawable.bounds.width()
            }
            addFailureListener {
                Log.e(TAG, "load lottie resource: $uncheckAnimAsset fail", it)
            }
        }
        // 加载选中动画
        LottieCompositionFactory.fromAsset(context, checkedAnimAsset).apply {
            addListener { result: LottieComposition ->
                lottieCompositions[1] = result
            }
            addFailureListener {
                Log.e(TAG, "load lottie resource: $checkedAnimAsset fail", it)
            }
        }
    }

CheckBox/RadioButton切换动效实现

加载选中动画和撤销选中动画,需求留意的是,咱们在加载撤销选中动画时初始化了checkStateChangeDrawable的scale。这主要是由于LottieDrawable的动画巨细只能由scale操控(简略的Drawable.setBounds()无法修正巨细),无法直接设置宽高,而设置scale时必须先设置好动画资源,scale的设置才会生效。所以咱们挑选在动画资源加载后来设置scale。

animateThumbToCheckedState(checked)

初始化作业完结后,接下来该完结动画播映了,即完结animateThumbToCheckedState(checked)办法。

   private fun animateCheckedStateChange(newState: Boolean) {
        cancelAnimator()
        val animIndex = if (newState) 1 else 0
        val lottieComposition = lottieCompositions[animIndex] ?: return
        checkStateChangeDrawable.composition = lottieComposition
        checkStateChangeDrawable.start()
        // 实践中software_layer动画效果最好
        if (View.LAYER_TYPE_SOFTWARE != compoundButton.layerType) {
            setLayerType(View.LAYER_TYPE_SOFTWARE, null)
        }
    }

CheckBox/RadioButton切换动效实现

初始化作业完结后,动画播映就很简略了,先撤销前次动画播映,然后挑选要播的动画直接播映。

注:关于挑选LAYER_TYPE_DRAWABLE,即软件烘托的办法来播映动画,主要是由于这种办法在我需求播映的动画素材中效果最流通。假如你觉得动画播映的不流通,可尝试切换烘托办法试试。(思路参阅自LottieAnimationView.playAnimation()办法)

cancelAnimator()

cancelAnimator()完结,很简略就不解析了

    fun cancelAnimator() {
        if (checkStateChangeDrawable.isAnimating) {
            checkStateChangeDrawable.stop()
        }
    }

CheckBox/RadioButton切换动效实现

总结

至此根本完结结束。回顾一下,能够看到咱们的完结根本与CheckBox无直接关联,是对CompoundButton的改造,这也意味着关于RadioButton仍能彻底选用该思路来完结。下边附上代码完结中的类字段清单,以便利理解上述代码。

代码完结中用到的类字段清单

    // CheckBox的icon对应的drawable
    private val btnDrawable: Drawable?,
    // 选中对应的动画资源文件途径(assets目录下)
    private val checkedAnimAsset: String,
    // 撤销选中对应的动画资源文件途径
    private val uncheckAnimAsset: String,
    // 播映动画的LottieDrawable
    private var checkStateChangeDrawable: LottieDrawable = LottieDrawable()
    // 动画资源加载后lottie资源实体列表
    private val lottieCompositions = arrayOf<LottieComposition?>(null, null)

CheckBox/RadioButton切换动效实现

Switch组件动画完结(可忽略)

至此已不归于本文标题描述内容,读者可挑选不读。设置此节主要是由于Switch组件的完结难点不多,并不想多开一篇,也就在此同时记录下,以便后续自查。

默许动画剖析

上面剖析中咱们现已提到了Switch组件(实践在SwitchCompat类中)重写了CompoundButton的setChecked()办法,并基于此来完结动画。

    @Override
    public void setChecked(boolean checked) {
        super.setChecked(checked);
        // Calling the super method may result in setChecked() getting called
        // recursively with a different value, so load the REAL value...
        checked = isChecked();
        if (checked) {
            setOnStateDescriptionOnRAndAbove();
        } else {
            setOffStateDescriptionOnRAndAbove();
        }
        // 假如View仍然在View树上,则播动画;否则不播,直接切换状况
        if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
            animateThumbToCheckedState(checked);
        } else {
            // Immediately move the thumb to the new position.
            cancelPositionAnimator();
            setThumbPosition(checked ? 1 : 0);
        }
    }

CheckBox/RadioButton切换动效实现

其中,animateThumbToCheckState(checked)办法中即完结了动画的播映

    private static final Property<SwitchCompat, Float> THUMB_POS =
        new Property<SwitchCompat, Float>(Float.class, "thumbPos") {
            @Override
            public Float get(SwitchCompat object) {
                return object.mThumbPosition;
            }
            @Override
            public void set(SwitchCompat object, Float value) {
                object.setThumbPosition(value);
            }
        };
    private void animateThumbToCheckedState(final boolean newCheckedState) {
        final float targetPosition = newCheckedState ? 1 : 0;
        mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
        mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
        if (Build.VERSION.SDK_INT >= 18) {
            mPositionAnimator.setAutoCancel(true);
        }
        mPositionAnimator.start();
    }

CheckBox/RadioButton切换动效实现

动画播映完结中简略的完结了Switch的滑块移动动画。基于此剖析,咱们想要自界说Switch切换动画,那首要就得先撤销默许动画,然后再播映咱们自己完结的动画。由于本次需求中咱们自己完结的Switch动画不具备通用性,所以下边介绍中,咱们将要点介绍怎样撤销默许动画,并简略的完结一个自界说动画。

撤销默许动画

首要基于上述剖析,咱们应该重写setChecked()办法,别的由于SwitchCompat在完结setChecked()办法时还做了一些额外作业,别的咱们还想复用CompoundButton中的setChecked()完结,所以咱们想仅仅撤销掉默许动画,并仍然需求调用super.setChecked()。现在遇到一个问题,咱们调用不到SwitchCompat的cancelAnimator()办法,该办法并不对子类敞开。可是这难不倒咱们,调不到咱们就反射调!

    @Override
    public void setChecked(boolean checked) {
        super.setChecked(checked);
        // 撤销SwitchCompat动画,选用自己完结
        if (reflectManager != null && reflectManager.cancelSwitchCompatAnimate()) {
            checked = isChecked();
            if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
                animateThumbToCheckedState(checked);
            } else {
                // Immediately move the thumb to the new position.
                cancelPositionAnimator();
                reflectManager.setThumbPosition(checked ? 1 : 0);
            }
        }
    }
    // 全体处理反射调用
    private static class ReflectManager {
        private final SwitchCompat switchView;
        private boolean canReflect = true;
        private Method cancelPositionAnimatorMethod = null;
        private Field mThumbPositionField = null;
        public ReflectManager(SwitchCompat switchView) {
            this.switchView = switchView;
            init();
        }
        private void init() {
            try {
                cancelPositionAnimatorMethod = SwitchCompat.class.getDeclaredMethod("cancelPositionAnimator");
                cancelPositionAnimatorMethod.setAccessible(true);
                mThumbPositionField = SwitchCompat.class.getDeclaredField("mThumbPosition");
                mThumbPositionField.setAccessible(true);
            } catch (Exception e) {
                canReflect = false;
            }
        }
        // 反射调用SwitchCompat组件的cancelPositionAnimator()办法
        public boolean cancelSwitchCompatAnimate() {
            init();
            if (!canReflect || cancelPositionAnimatorMethod == null) return false;
            try {
                cancelPositionAnimatorMethod.invoke(switchView);
            } catch (Exception e) {
                canReflect = false;
            }
            return canReflect;
        }
    }

CheckBox/RadioButton切换动效实现

看setChecked(state)的全体结构,咱们仍然选用SwitchCompat中的动画完结思路,但在播映自界说动画前,反射撤销了默许动画。

完结自界说动画

这里介绍下咱们需求中需求完结的自界说动画,需求在切换时滑块自界说滑动速度,并进行滑块色彩突变。所以咱们的完结能够根本按照默许完结进行,仅需求调整动画插值器,并监听动画进度调整滑块色彩。很简略就不再剖析了。

    private void animateThumbToCheckedState(final boolean newCheckedState) {
        final float targetPosition = newCheckedState ? 1 : 0;
        mPositionAnimator = ObjectAnimator.ofFloat(this, new Property<SwitchCompat, Float>(Float.class, "thumbPos") {
            @Override
            public Float get(SwitchCompat object) {
                return reflectManager.getThumbPosition();
            }
            @Override
            public void set(SwitchCompat object, Float value) {
                int color = (int) argbEvaluator.evaluate(value, 0xFF797980, 0xFF8C32FF);
                getThumbDrawable().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
                reflectManager.setThumbPosition(value);
            }
        }, targetPosition);
        mPositionAnimator.setInterpolator(PathInterpolatorCompat.create(0.55f, 0f, 0.35f, 1f));
        mPositionAnimator.setDuration(250);
        mPositionAnimator.setAutoCancel(true);
        mPositionAnimator.start();
    }
    // ------- 下边办法在ReflectManager类中 -------
    public float getThumbPosition() {
        try {
            return (float) mThumbPositionField.get(switchView);
        } catch (Exception e) {
            canReflect = false;
        }
        return 0f;
    }
    public boolean setThumbPosition(float f) {
        try {
            mThumbPositionField.set(switchView, f);
            switchView.invalidate();
        } catch (Exception e) {
            canReflect = false;
        }
        return canReflect;
    }

CheckBox/RadioButton切换动效实现

总结

Switch动画完结的要点在撤销默许动画,经过反射即可完结。假如滑块动画很杂乱,理论上咱们仍然能够运用CheckBox完结时选用的LottileDrawable+LayerDrawable的完结办法。