自定義控件(三步搞定switch)

概述

switch控件是我們最常用的控件之一,網(wǎng)上雖有較多源碼,但是通常我們的項(xiàng)目中用到的都是他的變種,初學(xué)者有時(shí)候不知如何修改,希望通過本文的分析,我們可以掌握這個(gè)控件的關(guān)鍵點(diǎn)。

詳解:

我們來做一個(gè)下圖這樣的switch控件。


ezgif.com-9dd15217dd.gif

可以分解為下面三個(gè)過程:

  • 繪制圖像
  • 添加事件
  • 測(cè)量布局
繪制圖像:

此控件的繪制的過程可分解為:

1.繪制邊框線條
2.繪制填充色
3.繪制小圓球
  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawFrameRoundRect(canvas);
        drawRect(canvas);
        drawCircle(canvas);
    }

在繪制之前我們需要準(zhǔn)備好畫筆,我們定義三種畫筆。

    /**
     * 初始化畫筆
     */
    private void initPaint() {

        //填充畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(0);
        mPaint.setStyle(Paint.Style.FILL);

        //邊線畫筆
        fPaint = new Paint();
        fPaint.setAntiAlias(true);
        fPaint.setColor(Color.parseColor("#BDBDBD"));
        fPaint.setAlpha(255);
        fPaint.setStrokeWidth(1);
        fPaint.setStyle(Paint.Style.STROKE);

        //圓圈畫筆
        cPaint = new Paint();
        cPaint.setAntiAlias(true);
        cPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        cPaint.setStrokeWidth(1);
        cPaint.setColor(Color.WHITE);
    }
1.繪制邊框線條

邊線是兩邊圓弧的矩形,所以我們選擇drawRoundRect方法。

    /**
     * 繪制邊框線條
     * @param canvas
     */
    private void drawFrameRoundRect(Canvas canvas) {
        if (rectF == null) {
            rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
        }
        canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, fPaint);
    }
2.繪制填充色
  • 繪制填充色和繪制線條的實(shí)現(xiàn)幾乎一樣,不一樣的也就是畫筆了。
  • 填充色的畫筆setStyle設(shè)置的是Paint.Style.FILL,邊線畫筆設(shè)置的是Paint.Style.STROKE
  • setARGB設(shè)置畫筆的色值,通過alpha值的變化使得漸變過度更平緩。
   /**
     * 繪制填充的色值
     * @param canvas
     */
    private void drawRect(Canvas canvas) {
        mPaint.setARGB(getColorAlpha(), Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor));
        if (rectF == null) {
            rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
        }
        canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, mPaint);
    }
3.繪制小圓球
  • setShadowLayer用來實(shí)現(xiàn)小圓球的陰影效果,使其更有立體感。
  • isPressed用來區(qū)分按下和正常效果,按下之后小圓球變大。
  • drawCircle,drawRoundRect的這些坐標(biāo)讀者可以自己微調(diào),沒有固定的規(guī)則。
  /**
     * 繪制點(diǎn)擊的原點(diǎn)
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        if (isPressed) {
            cPaint.setShadowLayer(12, 0, 12, Color.argb(61, 0x00, 0x00, 0x00));
            canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2 + 6, cPaint);
        } else {
            cPaint.setShadowLayer(6, 0, 6, Color.argb(61, 0x00, 0x00, 0x00));
            canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2,  (getHeight() - getHeight()/3)/2, cPaint);
        }
    }

至此,靜態(tài)的switch完成了。為了讓其動(dòng)起來,我們來響應(yīng)下onTouchEvent事件的實(shí)現(xiàn)。

添加事件:

一般我們響應(yīng)onTouch事件,只需處理這幾個(gè)事件:ACTION_DOWN,ACTION_MOVE, ACTION_CANCEL, ACTION_UP。如果事件不清楚的可以看:View, ViewGroup, Layout

public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            return false;//控件處于disable狀態(tài)的時(shí)候不處理
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = event.getX();
                lastProcess = getProcess();
                isPressed = true;
                break;
            case MotionEvent.ACTION_MOVE:
                setProcess(lastProcess + (event.getX() - lastX)/getWidth());
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isPressed = false;
                if (getProcess() > 0.5) {
                    setProcess(1f);
                } else {
                    setProcess(0f);
                }
                break;
            default:
                break;
        }
        return true;
  • MotionEvent.ACTION_DOWN:獲取按下時(shí)的滑動(dòng)位置
  • MotionEvent.ACTION_MOVE:滑動(dòng)過程中設(shè)置坐標(biāo),并且刷新界面
  • MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP:事件結(jié)束處理
    在滑動(dòng)過程中,最好將坐標(biāo)打印出來看看就比較清楚了。
測(cè)量布局:

添加完響應(yīng)事件,我們switch控件已經(jīng)基本可以用了。用戶如果在xml中不指定控件的長寬,我們的控件就會(huì)鋪滿全屏,直接變形了,所以我們還需要重寫下onMeasure,當(dāng)然你也可把這個(gè)測(cè)量布局放在最開始。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode  = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize  = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            //AT_MOST或者UNSPECIFIED,即用戶沒有指定寬度時(shí),顯示默認(rèn)寬度
            width = dip2px(getContext(), 128);
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            //AT_MOST或者UNSPECIFIED,即用戶沒有指定高度時(shí),顯示默認(rèn)高度
            height = dip2px(getContext(), 48);
        }
        setMeasuredDimension(width, height);
    }

完整代碼:

測(cè)試代碼不貼了,新建一個(gè)工程,將這個(gè)類拷貝到工程,和普通button控件用法一樣就可以使用了。

package com.wayne.android.widget;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Checkable;

public class Switch extends View implements Checkable {

    private static final int FILL_COLOR_1 = Color.parseColor("#EEEEEE");
    private static final int FILL_COLOR_2 = Color.parseColor("#03A9F4");
    private static final int ANIMATION_DURATION = 200;
    private static final int OFFSET = 12;

    private ObjectAnimator processAnimator;
    private boolean isChecked;
    private float process = 0;
    private float lastX = 0;
    private float lastProcess = 0;
    private int fillColor;
    private boolean isPressed;
    private Paint cPaint;
    private Paint fPaint;
    private Paint mPaint;
    private RectF rectF = null;
    public Switch(Context context) {
        super(context);
        init();
    }

    public Switch(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode  = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize  = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            //AT_MOST或者UNSPECIFIED,即用戶沒有指定寬度時(shí),顯示默認(rèn)寬度
            width = dip2px(getContext(), 128);
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            //AT_MOST或者UNSPECIFIED,即用戶沒有指定高度時(shí),顯示默認(rèn)高度
            height = dip2px(getContext(), 48);
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawFrameRoundRect(canvas);
        drawRect(canvas);
        drawCircle(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            return false;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = event.getX();
                lastProcess = getProcess();
                isPressed = true;
                break;
            case MotionEvent.ACTION_MOVE:
                setProcess(lastProcess + (event.getX() - lastX)/getWidth());
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isPressed = false;
                if (getProcess() > 0.5) {
                    setProcess(1f);
                } else {
                    setProcess(0f);
                }
                break;
            default:
                break;
        }
        return true;
    }

    @Override
    public void setChecked(boolean b) {
        if (isChecked != b) {
            isChecked = b;
            animateSwitch(isChecked);
        }
    }

    @Override
    public boolean isChecked() {
        return isChecked;
    }

    @Override
    public void toggle() {
        setChecked(!isChecked);
    }

    /**
     * 初始化
     */
    private void init() {
        isPressed = false;
        process = 0;
        isChecked = false;
        fillColor = FILL_COLOR_1;
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        initAnimation();
        initPaint();
    }

    /**
     * 初始化動(dòng)畫對(duì)象
     */
    private void initAnimation() {
        processAnimator = ObjectAnimator.ofFloat(this, "process", 0, 1);
        processAnimator.setDuration(ANIMATION_DURATION);
        processAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    }

    /**
     * 初始化畫筆
     */
    private void initPaint() {

        //填充畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(0);
        mPaint.setStyle(Paint.Style.FILL);

        //frame畫筆
        fPaint = new Paint();
        fPaint.setAntiAlias(true);
        fPaint.setColor(Color.parseColor("#BDBDBD"));
        fPaint.setAlpha(255);
        fPaint.setStrokeWidth(1);
        fPaint.setStyle(Paint.Style.STROKE);

        //圓圈畫筆
        cPaint = new Paint();
        cPaint.setAntiAlias(true);
        cPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        cPaint.setStrokeWidth(1);
        cPaint.setColor(Color.WHITE);
    }

    public float getProcess() {
        return process;
    }

    /**
     * 設(shè)置滑動(dòng)的進(jìn)度
     * @param process
     */
    public void setProcess(float process) {
        if (process >= 1f) {
            this.process = 1;
            isChecked = true;
        } else if (process <= 0f) {
            this.process = 0;
            isChecked = false;
        } else {
            this.process = process;
        }
        if (this.process > 0.5) {
            fillColor = FILL_COLOR_2;
        } else {
            fillColor = FILL_COLOR_1;
        }
        postInvalidate();
    }

    /**
     * 開關(guān)動(dòng)畫
     * @param checked
     */
    private void animateSwitch(boolean checked) {
        if (processAnimator.isRunning()) {
            processAnimator.cancel();
        }

        if (checked) {
            processAnimator.setFloatValues(process, 1f);
        } else {
            processAnimator.setFloatValues(process, 0f);
        }
        processAnimator.start();
    }

    /**
     * 獲取滑動(dòng)時(shí)的alpha值
     * @return
     */
    private int getColorAlpha() {
        int alpha;
        if (getProcess() >= 0 && getProcess() < 0.5) {
            alpha = (int) (255 * (1 - getProcess()));
        } else {
            alpha = (int) (255 * getProcess());
        }
        int colorAlpha = Color.alpha(fillColor);
        colorAlpha = colorAlpha * alpha / 255;
        return colorAlpha;
    }

    /**
     * 繪制填充的色值
     * @param canvas
     */
    private void drawRect(Canvas canvas) {
        mPaint.setARGB(getColorAlpha(), Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor));
        if (rectF == null) {
            rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
        }
        canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, mPaint);
    }

    /**
     * 繪制邊框線條
     * @param canvas
     */
    private void drawFrameRoundRect(Canvas canvas) {
        if (rectF == null) {
            rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
        }
        canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, fPaint);
    }

    /**
     * 繪制點(diǎn)擊的原點(diǎn)
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        if (isPressed) {
            cPaint.setShadowLayer(12, 0, 12, Color.argb(61, 0x00, 0x00, 0x00));
            canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2 + 6, cPaint);
        } else {
            cPaint.setShadowLayer(6, 0, 6, Color.argb(61, 0x00, 0x00, 0x00));
            canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2,  (getHeight() - getHeight()/3)/2, cPaint);
        }
    }

    /**
     * dip轉(zhuǎn)換的px
     * @param context
     * @param dpValue
     * @return
     */
    private int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) ((dpValue * scale) + 0.5f);
    }
}

結(jié)語:

為了盡量簡單,代碼量少些,本文沒有按照前面文章中寫的那樣,如:定義drawable將繪制移入其中,并且定義屬性文件。有空的話大家可以改造下,好了,就到這里,周末愉快。
上篇:自定義控件(progresses)(360手機(jī)助手下載進(jìn)度示例)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、Android開發(fā)初體驗(yàn) 二、Android與MVC設(shè)計(jì)模式模型對(duì)象存儲(chǔ)著應(yīng)用的數(shù)據(jù)和業(yè)務(wù)邏輯。模型類通常用來...
    為夢(mèng)想戰(zhàn)斗閱讀 1,068評(píng)論 0 3
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,353評(píng)論 0 17
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,108評(píng)論 25 709
  • 6、View的繪制 (1)當(dāng)測(cè)量好一個(gè)View之后,我們就可以簡單的重寫 onDraw()方法,并在 Canvas...
    b5e7a6386c84閱讀 1,980評(píng)論 0 3
  • 風(fēng)為她 鋪了一個(gè)晝夜的路 而她,只是悄然的來了 壓過臘梅的寒香 掩蓋凋零的肅殺 在光與暗交錯(cuò)的夜 她雀躍著,狂歡著...
    遙途閱讀 202評(píng)論 0 0

友情鏈接更多精彩內(nèi)容