布景
由于项目需求要为CheckBox和RadioButton增加切换动画,以达到个性化的UI组件效果,详细来说项目需求的切换动画为杂乱动画,即无法经过简略的平移,旋转,缩放等根本图形变换来模拟。经过查找资料,发现有如下几种完结动效的办法:
- ObjectAnimator,用来完结如平移,旋转,缩放等最根本的动效,无法满足项目要求。
- 自界说View专门来完结动画,这种成本很高,别的彻底自界说也无法直接运用CheckBox的各项功用。
- VectorAnimatorDrawable,看起来很靠谱,能够完结很杂乱的动画。但最大的难题是UI无法直接输出对应的资源,UI同学一般供给的动效资源是动效json,GIF,视频等资源,想要将其转化为VectorAnimatorDrawable资源非常困难,杂乱一点的动效,要想彻底复原规划稿根本不行能。
- 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();
// ... 省掉部分无关代码
}
}
该办法直接修正了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);
}
}
在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();
}
}
剩下的作业就是填充下面两个办法,全体来说就是处理详细动画该怎样播
- animateThumbToCheckedState(checked)
- cancelAnimator()
问题2:动画该怎样播
经过前边的剖析,咱们现已选定了Lottie动画作为播映动画的计划。而且预期的CheckBox切换流程为(以uncheck -> checked为例):
- mChecked状况先变化,但不期望看到icon的突变
- 播映Lottie动画,动画播映盖在原来的icon之上
- 动画播映结束,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>
初始化作业
在确定好计划后,在完结上边两个办法前,我需求先做一些初始化作业(加载动画资源,将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!")
}
}
注:这里代码选用了kotlin,主要由于封装的工具类是用kotlin来完结的,不影响思路。
初始化作业中,咱们主要有以下几步:
-
创建LottieDrawable
-
注:下边解析不影响全体流程阅览
-
由于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.
-
-
将LottieDrawable与静态Drawable(实践类型为StateListDrawable)宽高对齐
-
设置动画播映监听,以完结动画播映的无缝切换
-
预备动画资源,这一步独自下边介绍
初始化-预备动画资源
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)
}
}
}
加载选中动画和撤销选中动画,需求留意的是,咱们在加载撤销选中动画时初始化了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)
}
}
初始化作业完结后,动画播映就很简略了,先撤销前次动画播映,然后挑选要播的动画直接播映。
注:关于挑选LAYER_TYPE_DRAWABLE,即软件烘托的办法来播映动画,主要是由于这种办法在我需求播映的动画素材中效果最流通。假如你觉得动画播映的不流通,可尝试切换烘托办法试试。(思路参阅自LottieAnimationView.playAnimation()办法)
cancelAnimator()
cancelAnimator()完结,很简略就不解析了
fun cancelAnimator() {
if (checkStateChangeDrawable.isAnimating) {
checkStateChangeDrawable.stop()
}
}
总结
至此根本完结结束。回顾一下,能够看到咱们的完结根本与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)
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);
}
}
其中,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();
}
动画播映完结中简略的完结了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;
}
}
看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;
}
总结
Switch动画完结的要点在撤销默许动画,经过反射即可完结。假如滑块动画很杂乱,理论上咱们仍然能够运用CheckBox完结时选用的LottileDrawable+LayerDrawable的完结办法。