Android 自定义 ToggleButton实践一

一、前言

ToggleButton 是常用的Android常用的组件,早期由于有些版本存在差异,很难做出想要的效果,本篇通过自定义方式,来温习一下事件的滑动,同时提供一个骨架,方便后续改造。

二、实现

事件的处理上一定要做到及时消费,这和定义签名View或者手绘View一个道理,由于存在时间差,Move点并不连续,因此不需要担心保存太多的点。

做这类View的定义,需要了解以下特征,基本可以顺手拿来:

  • DOWN事件捕获,拦截

  • 起点

  • 移动方向

  • MOVE事件

  • UP/CANCEL事件处理

  • volecity

代码实现

ini 复制代码
  @Override
    public boolean onTouchEvent(MotionEvent event) {

        int action = event.getAction();
        int lineWidthPixies = (int) dpTopx(mLineWidth);


        switch (action) {
            case MotionEvent.ACTION_DOWN:
              //记录起点
                startX = event.getX();
                startY = event.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                return true; //捕获事件
            case MotionEvent.ACTION_MOVE:

                float currentX = event.getX();
                float currentY = event.getY();

                float dy = Math.abs(currentY - startY);
                float dx = Math.abs(currentX - startX);

                if (dy <= dx && dx >= mTouchSlop) {
                    isTouchMove = true;
                }

                if (isTouchMove) {
                    offsetX = (int) (currentX - slideBarRadius);
                    startX = currentX;
                    startY = currentY;
                  

                    if (offsetX < (lineWidthPixies)) {
                        offsetX = 0; //最左边
                    } else if (offsetX > contentWidth - slideBarRadius * 2) { //最右边
                        offsetX = contentWidth - slideBarRadius * 2;
                    }                     //消费
                    postInvalidateOnAnimation();
                }

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:

                getParent().requestDisallowInterceptTouchEvent(false);
                startX = event.getX();
                if (startX <= getWidth() / 2) {
                    if(isTouchMove){
                        offsetX = 0;
                        mState = STATUS_LEFT;
                    }else{
                        startSlideBarAnimation(offsetX,0,STATUS_LEFT);
                    }
                } else {
                    if(isTouchMove){
                        mState = STATUS_RIGHT;
                        offsetX = contentWidth - 2 * slideBarRadius;
                    }else{
                        startSlideBarAnimation(offsetX,contentWidth - 2 * slideBarRadius,STATUS_RIGHT);
                    }
                }
                isTouchMove = false;
                postInvalidateOnAnimation();
                break;
        }


        return super.onTouchEvent(event);
    }

全部代码

ini 复制代码
public class ToggleButton extends View {


    private int mLineWidth = 5;
    private int mTextSize = 18;
    private TextPaint mTextPaint = null;
    private int mTouchSlop = 0;
    private boolean isTouchMove = false;
    private int offsetX = 0;
    private int contentWidth = 0;
    private int contentHeight = 0;
    private int slideBarRadius;
    float startX = 0f;
    float startY = 0f;
    private int STATUS_LEFT = 0;
    private int STATUS_RIGHT = 1;

    private int mState = STATUS_LEFT;

    private ValueAnimator mSlideAnimator = null;

    public ToggleButton(Context context) {
        this(context, null);
    }

    public ToggleButton(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ToggleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //设置此项true,否则无法滑动
        mTextPaint = initPaint();
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() / 2;

        setClickable(true);
        setFocusable(true);
        setFocusableInTouchMode(true);

        setBackgroundColor(0xffffffff);

    }

    private TextPaint initPaint() {
        // 实例化画笔并打开抗锯齿
        TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);
        paint.setStrokeWidth(dpTopx(mLineWidth));
        paint.setTextSize(dpTopx(mTextSize));
        return paint;
    }

    private float dpTopx(int dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = getPaddingTop() + getPaddingBottom() + 100;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            int disire = getPaddingTop() + getPaddingBottom() + 100;
            heightSize = Math.min(disire, heightSize);
        }

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);


        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = getPaddingTop() + getPaddingBottom() + 280;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            int disire = getPaddingTop() + getPaddingBottom() + 280;
            widthSize = Math.min(disire, widthSize);
        }

        setMeasuredDimension(widthSize, heightSize);

    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int w = getWidth();
        int h = getHeight();

        int lineWidthPixel = (int) mTextPaint.getStrokeWidth();

        contentWidth = w - 2 * lineWidthPixel;
        contentHeight = h - 2 * lineWidthPixel;

        int radius = Math.min(w, h) / 2 - lineWidthPixel / 2;
        if (radius < 0) return;

        mTextPaint.setColor(Color.DKGRAY);
        mTextPaint.setStyle(Paint.Style.STROKE);
        RectF outlineRectF = new RectF(
                lineWidthPixel / 2,
                lineWidthPixel / 2,
                lineWidthPixel + contentWidth + lineWidthPixel / 2,
                lineWidthPixel + contentHeight + lineWidthPixel / 2
        );
        canvas.drawRoundRect(outlineRectF, radius, radius, mTextPaint);

        RectF innerRectF = new RectF(
                lineWidthPixel,
                lineWidthPixel,
                lineWidthPixel + contentWidth,
                lineWidthPixel + contentHeight
        );
        mTextPaint.setColor(0xffffffff);
        mTextPaint.setStyle(Paint.Style.FILL);
        canvas.drawRoundRect(innerRectF, radius, radius, mTextPaint);


        mTextPaint.setColor(Color.DKGRAY);
        RectF middleRect = new RectF(
                w / 2 - lineWidthPixel / 2,
                lineWidthPixel * 3,
                w / 2 + lineWidthPixel / 2,
                h - lineWidthPixel * 3
        );
        mTextPaint.setStyle(Paint.Style.FILL);
        canvas.drawRoundRect(middleRect, radius, radius, mTextPaint);

        slideBarRadius = contentHeight / 2;


        RectF circleRectF = new RectF(
                lineWidthPixel + lineWidthPixel + offsetX,
                0,
                slideBarRadius * 2 + offsetX,
                h
        );

        mTextPaint.setColor(Color.GRAY);
        canvas.drawCircle(circleRectF.centerX(), circleRectF.centerY(), slideBarRadius, mTextPaint);

        if(mState==STATUS_LEFT) {
            mTextPaint.setColor(Color.CYAN);
        }else{
            mTextPaint.setColor(Color.RED);
        }
        canvas.drawCircle(circleRectF.centerX(), circleRectF.centerY(), slideBarRadius / 4, mTextPaint);

    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int action = event.getAction();
        int lineWidthPixies = (int) dpTopx(mLineWidth);


        switch (action) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;
            case MotionEvent.ACTION_MOVE:

                float currentX = event.getX();
                float currentY = event.getY();

                float dy = Math.abs(currentY - startY);
                float dx = Math.abs(currentX - startX);

                if (dy <= dx && dx >= mTouchSlop) {
                    isTouchMove = true;
                }

                if (isTouchMove) {
                    offsetX = (int) (currentX - slideBarRadius);
                    startX = currentX;
                    startY = currentY;

                    if (offsetX < (lineWidthPixies)) {
                        offsetX = 0; //最左边
                    } else if (offsetX > contentWidth - slideBarRadius * 2) { //最右边
                        offsetX = contentWidth - slideBarRadius * 2;
                    }

                    postInvalidateOnAnimation();
                }

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:

                getParent().requestDisallowInterceptTouchEvent(false);
                startX = event.getX();
                if (startX <= getWidth() / 2) {
                    if(isTouchMove){
                        offsetX = 0;
                        mState = STATUS_LEFT;
                    }else{
                        startSlideBarAnimation(offsetX,0,STATUS_LEFT);
                    }
                } else {
                    if(isTouchMove){
                        mState = STATUS_RIGHT;
                        offsetX = contentWidth - 2 * slideBarRadius;
                    }else{
                        startSlideBarAnimation(offsetX,contentWidth - 2 * slideBarRadius,STATUS_RIGHT);
                    }
                }
                isTouchMove = false;
                postInvalidateOnAnimation();
                break;
        }


        return super.onTouchEvent(event);
    }

    public void postInvalidateOnAnimation() {
        if (Build.VERSION.SDK_INT >= 16) {
            super.postInvalidateOnAnimation();
        } else {
            postInvalidate();
        }
    }

    public void setState(int state) {
        this.mState = state;
        if (state == STATUS_LEFT) {
            offsetX = 0;
        } else {
            offsetX = contentWidth - 2 * slideBarRadius;
        }
        postInvalidate();
    }

    public void startSlideBarAnimation(int from, int to, final int state) {
        if (mSlideAnimator != null) {
            mSlideAnimator.cancel();
        }
        mSlideAnimator = ValueAnimator.ofInt(from, to).setDuration(300);
        mSlideAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mSlideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                offsetX = (int) animation.getAnimatedValue();
                float fraction = animation.getAnimatedFraction();
                if(fraction>=0.9){
                    mState = state;
                }
                postInvalidate();
            }
        });
        mSlideAnimator.start();
    }
}

三、总结

总体上不是很难,但是需要了解只是有Android的事件传递机制和捕获机制,requestDisallowParentInterceptTouchEvent,另外就是联动机制,滑动方向判断等,还有事件相对View的坐标,要知道的是TouchEvent无法像Canvas那样平移,但是你可以转换。

相关推荐
声声codeGrandMaster12 分钟前
Django框架的前端部分使用Ajax请求一
前端·后端·python·ajax·django
卡尔曼的BD SLAMer23 分钟前
计算机视觉与深度学习 | Python实现EMD-SSA-VMD-LSTM时间序列预测(完整源码和数据)
python·深度学习·算法·cnn·lstm
yu_anan11130 分钟前
Denoising Score Matching with Langevin Dynamics
算法·机器学习·概率论
重生之后端学习2 小时前
02-前端Web开发(JS+Vue+Ajax)
java·开发语言·前端·javascript·vue.js
zimoyin2 小时前
kotlin Android AccessibilityService 无障碍入门
android·开发语言·kotlin
小葡萄20252 小时前
黑马程序员C++2024新版笔记 第三章 数组
笔记·算法·c++20
繁依Fanyi3 小时前
用 CodeBuddy 实现「IdeaSpark 每日灵感卡」:一场 UI 与灵感的极简之旅
开发语言·前端·游戏·ui·编辑器·codebuddy首席试玩官
来自星星的坤5 小时前
【Vue 3 + Vue Router 4】如何正确重置路由实例(resetRouter)——避免“VueRouter is not defined”错误
前端·javascript·vue.js
勇闯逆流河8 小时前
【数据结构】堆
c语言·数据结构·算法
pystraf9 小时前
LG P9844 [ICPC 2021 Nanjing R] Paimon Segment Tree Solution
数据结构·c++·算法·线段树·洛谷