场景
切换Switch开关时,开关的生效可能需要一定的时间来生效
问题
对于生效较慢的开关,可能开关还没有生效,Switch的已经切换到了状态,跟实际效果不一致
解决方案
1、点击Switch后切换前,拦截Switch状态切换的操作
2、执行加载动画,同时执行切换开关的保管,
3、切换完成后,停止加载动画,执行Switch的状态切换
疑问点
1、如何拦截状态切换的保管
通过在onCheckedChanged方法中打印调用栈,了解到,占击Switch后,刷新状态的入口,是通过toggle方法来触发的
所以只要继承Switch后,重写toggle方法,就能实现点击Switch后状态切换的拦截
2、如何实现加载动效
初步想,可能通过嵌套一层Switch来实现,但这样不是很优雅。
在阅读了Switch的源码后,发现Switch的 track 和 thumb 是通过Drawable绘制的方法来实现的,因此我们可以参考 thumb 与 track的方式,把loading的资源作为参数传进来,然后通过Drawable绘制的方式来实现。
绘制的位置,可以直接取thumb的位置,使loading正好叠加在thumb上面。
完整实现
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoadingSwitch">
<!-- loading 资源 -->
<attr name="loading" format="integer"/>
</declare-styleable>
</resources>
LoadingSwitch.java
package com.example.customswitch;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RotateDrawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.view.animation.LinearInterpolator;
import android.widget.Switch;
public class LoadingSwitch extends Switch {
private static final String TAG = "LoadingSwitch";
private SwitchDelegate mSwitchDelegate;
/**
* 等待开关切换准备操作完成
*/
private boolean pendingReady = false;
/**
* 加载中的动画对像
*/
private ObjectAnimator mLoadingAnimation;
/**
* 加载的绘制对像,绘制区域和thumbDrawable一致
*/
private RotateDrawable mLoadingDrawable;
// loading 的旋转角度
private float mLoadingAngle;
private boolean showLoading = false;
public LoadingSwitch(Context context) {
super(context);
init(context, null);
}
public LoadingSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs == null) {
return;
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoadingSwitch);
Drawable drawable = a.getDrawable(R.styleable.LoadingSwitch_loading);
mLoadingDrawable = new RotateDrawable();
mLoadingDrawable.setDrawable(drawable);
mLoadingDrawable.setPivotXRelative(true);
mLoadingDrawable.setPivotX(0.5F);
mLoadingDrawable.setPivotYRelative(true);
mLoadingDrawable.setPivotY(0.5F);
mLoadingDrawable.setFromDegrees(0);
mLoadingDrawable.setToDegrees(360);
a.recycle();
mLoadingAnimation = ObjectAnimator.ofFloat(this, LOADING_ROTATE, 0, 360);
mLoadingAnimation.setRepeatCount(ValueAnimator.INFINITE);
mLoadingAnimation.setDuration(500);
mLoadingAnimation.setInterpolator(new LinearInterpolator());
mLoadingAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
invalidate();
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
invalidate();
}
@Override
public void onAnimationRepeat(Animator animation) {
super.onAnimationRepeat(animation);
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
invalidate();
}
@Override
public void onAnimationPause(Animator animation) {
super.onAnimationPause(animation);
}
@Override
public void onAnimationResume(Animator animation) {
super.onAnimationResume(animation);
}
});
}
public void setSwitchDelegate(SwitchDelegate switchDelegate) {
mSwitchDelegate = switchDelegate;
}
@Override
public void toggle() {
Log.i(TAG, "toggle");
if (mSwitchDelegate == null) {
super.toggle();
return;
}
if (pendingReady) {
Log.i(TAG, "toggle, skip for pending switch");
return;
}
if (isChecked()) {
Log.i(TAG, "toggle, ready for uncheck");
boolean isReady = mSwitchDelegate.readyUncheck();
if (isReady) {
super.toggle();
} else {
pendingReady = true;
}
} else {
Log.i(TAG, "toggle, ready for check");
boolean isReady = mSwitchDelegate.readyCheck();
if (isReady) {
super.toggle();
} else {
pendingReady = true;
}
}
}
public void checkReady() {
pendingReady = false;
if (mLoadingAnimation != null) {
mLoadingAnimation.cancel();
}
super.toggle();
}
public void uncheckReady() {
pendingReady = false;
if (mLoadingAnimation != null) {
mLoadingAnimation.cancel();
}
super.toggle();
}
/**
* 展示关闭的加载动画
*/
public void showLoading() {
showLoading = true;
if (mLoadingAnimation.isRunning()) {
return;
}
mLoadingAnimation.start();
}
/**
* 展示关闭的加载动画
*/
public void hideLoading() {
showLoading = false;
if (mLoadingAnimation.isRunning()) {
mLoadingAnimation.cancel();
}
}
public interface SwitchDelegate {
/**
* 打开开关是否准备好了
* 默认值准备好了
*
* @return isReady
*/
default boolean readyCheck() {
return true;
}
/**
* 关闭开关是否准备好了
* 默认值准备好了
*
* @return isReady
*/
default boolean readyUncheck() {
return true;
}
}
private float getLoadingAngle() {
return mLoadingAngle;
}
private void setLoadingAngle(float loadingAngle) {
this.mLoadingAngle = loadingAngle;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "onDraw pendingSwitch=" + pendingReady + ", angle=" + mLoadingAngle + ", mLoading" + mLoadingDrawable);
// 绘制加载动画
if (showLoading) {
if (mLoadingDrawable != null) {
mLoadingDrawable.setLevel((int)(mLoadingAngle * 10000 / 360));
mLoadingDrawable.setBounds(getThumbDrawable().getBounds());
mLoadingDrawable.draw(canvas);
}
}
}
private static final FloatProperty<LoadingSwitch> LOADING_ROTATE = new FloatProperty<LoadingSwitch>("angle") {
@Override
public Float get(LoadingSwitch object) {
return object.getLoadingAngle();
}
@Override
public void setValue(LoadingSwitch object, float value) {
object.setLoadingAngle(value);
}
};
}
使用
MainActivity.java
package com.example.customswitch;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.Switch;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
LoadingSwitch mSwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSwitch = findViewById(R.id.switch4);
mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.w(TAG, "onCheckChanged", new Throwable());
}
});
mSwitch.setSwitchDelegate(new LoadingSwitch.SwitchDelegate() {
@Override
public boolean readyCheck() {
Log.i(TAG, "readyCheck");
checkReady();
mSwitch.showLoading();
return false;
}
@Override
public boolean readyUncheck() {
Log.i(TAG, "readyCheck");
uncheckReady();
mSwitch.showLoading();
return false;
}
});
findViewById(R.id.loading_on).setOnClickListener(v -> {
mSwitch.setChecked(true);
});
findViewById(R.id.loading_off).setOnClickListener(v -> {
mSwitch.setChecked(false);
});
findViewById(R.id.loading_start).setOnClickListener(v -> {
mSwitch.showLoading();
});
findViewById(R.id.loading_stop).setOnClickListener(v -> {
mSwitch.hideLoading();
});
}
private void checkReady() {
Log.i(TAG, "checkReady");
new Thread(()->{
Log.i(TAG, "ready start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "ready finish");
runOnUiThread(()->{
mSwitch.hideLoading();
mSwitch.checkReady();
});
}).start();
}
private void uncheckReady() {
Log.i(TAG, "uncheckReady");
new Thread(()->{
Log.i(TAG, "ready start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "ready finish");
runOnUiThread(()->{
mSwitch.hideLoading();
mSwitch.uncheckReady();
});
}).start();
}
}