Android Switch Loading 效果定制

场景

切换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();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值