天天看點

自定義View-SwitchButton

自定義View-SwitchButton

        • 一、 分析View
        • 二、最簡單的實作
        • 三、實作`Switch`功能
        • 四、測量View
        • 五、`SwitchButton`狀态的儲存和恢複
        • 六、給`SwitchButton`添加狀态監聽
        • 七、自定義屬性
記得第一次寫自定義View的時候,寫就是switchbutton,現在回想起當時碼的,感覺真的是很low爆了,前段時間業務要求需要用,雖然現在android系統提供了SwitchButton,但是還是想自己寫一個來場回憶殺,哈哈!下面來看一下效果圖
自定義View-SwitchButton

一、 分析View

如圖

自定義View-SwitchButton
  • 由内圓和一個兩邊為半圓(以下都簡稱為外圓)的長方形背景;

    1. 我們定義内圓半徑

    r

    為外圓半徑

    R

    0.9

    倍:
    /**
         * 外圓半徑
         */
        private float outerCircleRadio;
        
        /**
         * 内圓半徑
         */
        private float innerCircleRadio;
               
  1. 為了更加美觀,定義

    W=2.5*H

    R=0.5*H

    ,我們定義

    RectF

    來存儲

    W、H

    /**
      * 背景繪制的區域
      */
     private RectF mBackgroundRectF;
               
  • 狀态分為開關:内圓在左側為關、内圓在右側為開;

    1. 定義标志位

    mSwitchState

    來存儲開關的狀态
    /**
     * switch開關-關閉狀态
     */
    private final int STATE_CLOSE = 0x8;
    
    /**
     * switch開關-打開狀态
     */
    private final int STATE_OPEN = 0x10;
    
    /**
     * switch開關-狀态标志位
     */
    private final int STATE_MASK = 0x18;
    
    /**
     * switch開關-預設狀态
     */
    private int mSwitchState;
    
               
  • 為了差別開關狀态,在每個狀态分别在對應的狀态給對應的内圓和背景不同的顔色來以區分;
  1. 定義

    SwitcButton

    對應狀态内圓的顔色:
    /**
         * 内圓未選中時的顔色
         */
        @ColorInt
        private int mInnerCircleOpenColor;
    
        /**
         * 内圓選中時的顔色
         */
        @ColorInt
        private int mInnerCircleCloseColor;
               
  2. 定義

    SwitcButton

    對應狀态背景的顔色:
    /**
         * 背景區域選中顔色
         */
        @ColorInt
        private int mBackgroundOpenColor;
    
        /**
         * 背景區域未選中顔色
         */
        @ColorInt
        private int mBackgroundCloseColor;
               

二、最簡單的實作

按照上面的分析我們先不管其他因素,先按照最簡單的來實作
  1. 建立類

    SwitchButton

    如下:
    public class SwitchButton extends View {
    
        public SwitchButton(Context context) {
            this(context, null);
        }
    
        public SwitchButton(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
     
    }
               
  2. 定義上面需要的屬性:
    public class SwitchButton extends View {
    
        /**
         * 内圓半徑
         */
        private float innerCircleRadio;
    
        /**
         * 外圓半徑
         */
        private float outerCircleRadio;
    
        /**
         * 内圓未選中時的顔色
         */
        @ColorInt
        private int mInnerCircleOpenColor;
    
        /**
         * 内圓選中時的顔色
         */
        @ColorInt
        private int mInnerCircleCloseColor;
    
    
        /**
         * 背景區域選中顔色
         */
        @ColorInt
        private int mBackgroundOpenColor;
    
        /**
         * 背景區域未選中顔色
         */
        @ColorInt
        private int mBackgroundCloseColor;
    
        /**
         * 背景繪制的區域
         */
        private RectF mBackgroundRectF;
    
    
        /**
         * switch開關-關閉狀态
         */
        private final int STATE_CLOSE = 0x8;
    
        /**
         * switch開關-打開狀态
         */
        private final int STATE_OPEN = 0x10;
    
        /**
         * switch開關-狀态标志位
         */
        private final int STATE_MASK = 0x18;
    
    
        /**
         * switch開關-預設狀态
         */
        private int mSwitchState;
    
    
        public SwitchButton(Context context) {
            this(context, null);
        }
    
        public SwitchButton(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
    
        }
    
        public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
        }
    
    }
    
               
  3. 定義方法

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

    來初始化我們View的一些屬性和工具:
    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            //初始化switch狀态,預設狀态為關閉狀态
            mSwitchState =STATE_MASK & STATE_CLOSE;
            //繪制畫筆
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setAntiAlias(true);
             //背景和内圓變化的顔色
            mBackgroundCloseColor =Color.GRAY;
            mBackgroundOpenColor =Color.WHITE;
            mInnerCircleCloseColor = Color.WHITE;
            mInnerCircleOpenColor = Color.GRAY;
    
            //背景繪制區域
            mBackgroundRectF = new RectF();
        }
    
               
  4. 重寫方法

    protected void onSizeChanged(int w, int h, int oldw, int oldh)

    方法:

    1)定義屬性

    mWidth

    mHeight

    來存儲View的寬高,且在方法

    onSizeChanged

    中進行指派

    mWidth=w、mHeight=h

    (當然你也可以省略這一步驟,後面通過

    getWidth和getHeight

    來擷取View的寬高)
    /**
     * view 布局的寬高
     */
    private int mWidth, mHeight;
               
    2)根據上面分析,考慮到View可能設定padding值,是以我們在計算背景的高度時要減去對應padding值則

    H=mHeight-getPaddingTop()-getPaddingBottom()

    ,而我們的外圓半徑為背景高度的一半且背景的寬是高的2.5倍,是以我們可以計算出來外圓的半徑為

    outerCircleRadio = (w - getPaddingLeft() - getPaddingRight()) / 5f;

    是以内圓的半徑為:

    innerCircleRadio = outerCircleRadio * 0.9f;

    ,背景的繪制區域為:

    mBackgroundRectF.set(getPaddingLeft(), mHeight / 2f - outerCircleRadio, mWidth - getPaddingRight(), mHeight / 2f + outerCircleRadio);

    具體代碼如下:
    @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            //擷取view的寬高
            mWidth = w;
            mHeight = h;
            //計算outerCircle的半徑
            outerCircleRadio = (w - getPaddingLeft() - getPaddingRight()) / 5f;
            innerCircleRadio = outerCircleRadio * 0.9f;
    
            // 計算繪制背景區域, 根據背景的寬高比例為2.5f 來計算
            mBackgroundRectF.set(getPaddingLeft(), mHeight / 2f - outerCircleRadio, mWidth - getPaddingRight(), mHeight / 2f + outerCircleRadio);
        }
    
               
  5. 重寫方法

    protected void onDraw(Canvas canvas)

    進行繪制:

    1)首先繪制預設關閉狀态下背景,首先給畫筆設定未打開時的背景顔色,然後繪制背景

    mBackgroundRectF

    mPaint.setColor(mBackgroundCloseColor);
         canvas.drawRoundRect(mBackgroundRectF, outerCircleRadio, outerCircleRadio, mPaint);
               
    2)繪制預設關閉狀态下内圓,由于在繪制内圓時需要确定圓心的坐标,在此我們定義兩個變量分别存儲内圓圓心的坐标為

    innerCircleOx, innerCircleOy

    ,在方法

    onSizeChanged

    中初始化為

    innerCircleOx = outerCircleRadio+getPaddingLeft(), innerCircleOy = h >> 1;

    ,然後設定畫筆的顔色為關閉時内圓的顔色,然後繪制内圓為:
    mPaint.setColor(innerCircleColor);
       canvas.drawCircle(innerCircleOx, innerCircleOy, innerCircleRadio, mPaint);
               
  6. 我們最初的view已經繪制完成了,但是還無法到達我們

    switch

    的效果,我們先來運作一下看看目前達到的效果,鼓勵一下自己,

    1) 布局檔案如下(不包含padding):

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <vip.zhuhailong.blogapplication.SwitchButton
            android:id="@+id/switchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true" />
    
    </RelativeLayout>
               
    然後運作為:
    自定義View-SwitchButton
    2)我們給View添加padding值為50dp在運作如下:
    自定義View-SwitchButton
看來我們模型大緻完成了,接下來就是完成

swtich

功能

三、實作

Switch

功能

有兩種方式可以改變

SwtichButton

的狀态:

第一種:通過點選操作來切換更改SwitchButton的操作

第二種:通過滑動内圓來更改

SwtichButton

的狀态

是以我們要在處理ACTION_UP和ACTION_CANCEL事件中對目前一系列的事件進行判斷,判斷是否僅僅是單擊事件還是中間出現了對應的業務滑動,如果僅僅是單擊事件則進行狀态的轉換且更新内顔色位置和背景的顔色,若是滑動事件我們還要進行另一個判斷,判斷我們目前系列事件的down事件落點是否在目前狀态下内圓内,是我們則将更新滑動的進度且補全剩下狀态改變的過渡過程,

  1. 由上分析可知我們需要定義兩個标志位分别來标記目前一系列事件是否是含有滑動事件和目前一系列事件的down事件的落點是否在内圓内(僅在發生滑動時間時起到作用)定義變量如下,且我們要在

    init

    方法中修改

    mSwitchState

    的初始化值為

    //初始化switch狀态 mSwitchState = (STATE_MASK & STATE_CLOSE) | (EVENT_MASK & MOVE_EVENT) | (LOCATION_MASK & OUTER_LOCATION);

    /**
         * switch開關- 事件最開始落點圈外
         */
        private final int OUTER_LOCATION = 0x0;
    
        /**
         * switch開關- 事件最開始落點圈内
         */
        private final int INNER_LOCATION = 0x1;
    
        /**
         * switch開關-事件最開始落點标志位
         */
        private final int LOCATION_MASK = 0x1;
    
        /**
         * switch開關-非移動事件
         */
        private final int OTHER_EVENT = 0x2;
    
        /**
         * switch開關-移動事件
         */
        private final int MOVE_EVENT = 0x4;
    
    
        /**
         * switch開關-是否處理事件标志位
         */
        private final int EVENT_MASK = 0x6;
               
  2. 為了讓我們

    SwitchButton

    狀态切換的更加優雅而不是一瞬間完成那麼生硬, 是以我們要建立屬性動畫

    mValueAnimator

    來美觀且完成

    ACTION_UP和ACTION_CANCEL

    中未完成的過渡過程,因為在涉及到滑動的系列事件中每次

    ACTION_UP和ACTION_CANCEL

    X

    坐标是不确定的,是以我們隻好在每次完成剩餘過渡過程中動态的設定起始和結束值,是以我們在

    init

    方法中添加屬性動畫的最基本的且通用的初始化操作,然後建立方法

    private void startSwitchAnimation(float animatorStartX, float animatorEndX)

    l來動态的執行我們的過渡補全工作,而在配置過程中,由于動畫的起始和結束是不确定的,也就是執行的過程長度是不确定的,是以我們需要進行動态的計算,我們假設從臨界值到另一個臨界值事件為

    1000ms

    ,我們再由實際要執行的動畫長度比上我們兩個臨界值的內插補點絕對值在乘上我們

    1000ms

    ,就可以動态計算出對應的動畫執行世間了,具體代碼如下:
    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    		//省略不在展示其他初始化代碼
    		//恢複動畫
            mValueAnimator = new ValueAnimator();
            mValueAnimator.addUpdateListener(animation -> {
                innerCircleOx = (float) animation.getAnimatedValue();
                invalidate();
            });
            mValueAnimator.setInterpolator(new BounceInterpolator());
    }
    
    private void startSwitchAnimation(float animatorStartX, float animatorEndX) {
        mValueAnimator.setFloatValues(animatorStartX, animatorEndX);
        //動态計算動畫完成時間
        mValueAnimator.setDuration((long) ((Math.abs(animatorEndX - animatorStartX) * 1L / ((mWidth - getPaddingLeft() - getPaddingRight() / 2L))) * mAnimationDuration));
        mValueAnimator.start();
    }
    
               
  3. 為了更直覺的可讀性,我們建立修改标志位的方法和一些讀取對應标志位的方法,并且将其裝換位對應

    布爾值

    1)建立修改标志位方法

    onlySetFlag(int flag, int mask)

    /**
         * 設定目前的Flag
         *
         * @param flag 需要設定的值
         * @param mask 對應标志位
         */
        private void onlySetFlag(int flag, int mask) {
            mSwitchState = (mSwitchState & ~mask) | (flag & mask);
        }
               
    2)建立從mSwitchState取出switch開關的狀态,判斷是否為打開狀态的方法

    public boolean stateIsOpen()

    /**
         * 從mSwitchState取出switch開關的狀态,判斷是否為打開狀态
         *
         * @return 否為打開狀态
         */
        public boolean stateIsOpen() {
            return (mSwitchState & STATE_MASK) == STATE_OPEN;
        }
               
    3)建立從mSwitchState取出event_mask位值,判斷目前是否為業務滑動事件的方法public boolean judgeIsMoveEvent()`:
    /**
         * 從mSwitchState取出event_mask位值,判斷目前是否為業務滑動事件
         *
         * @return 是否為業務滑動事件
         */
        public boolean judgeIsMoveEvent() {
            return (mSwitchState & EVENT_MASK) == MOVE_EVENT;
        }
               
    4)建立從mSwitchState取出系列事件中

    ACTION_DOWN

    事件落點坐标是否在内圓中的方法:
    /**
         * 從mSwitchState取出系列事件最開始事件落點坐标,判斷是否在内圓中
         *
         * @return 是否在内圓中
         */
        public boolean locationIsInner() {
            return (mSwitchState & LOCATION_MASK) == INNER_LOCATION;
        }
               
  4. 在使用非點選事件來改變

    SwitchButton

    狀态時即使用滑動,有可能出現我們的手指滑出背景外去,為了保證我們内圓在背景内,是以我們

    innerCircleOx

    定義一個範圍:

    [innerCircleMaxRightX ,innerCircleMaxLeftX]

    ,如下:
    private float  innerCircleMaxLeftX, innerCircleMaxRightX;
        
    	@Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
    		//省略
    		
    		//初始化innerCircleOx的範圍
            innerCircleMaxLeftX = mBackgroundRectLeft + outerCircleRadio;
            innerCircleMaxRightX = mBackgroundRectRight - outerCircleRadio;
            
         	//省略
        }
    
               
  5. 分析

    ACTION_DOWN

    事件

    1)首先判斷我們是否要消費目前一系列事件,判斷依據是目前空間是否可用

    enable

    、是否可點選

    clickable

    、是否在我們背景内

    mBackgroundRectF.contains(x, y)

    且目前過渡補充動畫不在執行且沒開始

    !mValueAnimator.isRunning() && !mValueAnimator.isStarted()

    即:
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    	float x = event.getX();
        float y = event.getY();	
    	if (event.getAction() == MotionEvent.ACTION_DOWN) {
           //判斷目前是否可用、可點選、在點選範圍内且不再執行恢複動畫
           return isEnabled() && isClickable() && mBackgroundRectF.contains(x, y) && !mValueAnimator.isRunning() && !mValueAnimator.isStarted();
        }
    }
               
    2)判斷目前的落點是否在目前内圓範圍内,并更新标志位,計算方法為:用目前事件的坐标和内圓圓心的坐标進行距離求值,若小于等于外圓半徑則為園内,否則圓外:

    boolean insideInnerCircle = Math.sqrt(Math.pow(Math.abs(motionEventStartX - innerCircleOx), 2) + Math.pow(Math.abs(event.getY() - innerCircleOy), 2)) <= outerCircleRadio;

    ,然後更新對應的标記位

    onlySetFlag(insideInnerCircle ? INNER_LOCATION : OUTER_LOCATION, LOCATION_MASK);

    若後期産生了移動事件則用作為是否處理目前系列事件的依據,是以完整的

    ACTION_DOWN

    事件處理方案為:
    float x = event.getX();
    float y = event.getY();
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
             //判斷系列事件最開始事件落點坐标是否在内圓中
             boolean insideInnerCircle = Math.sqrt(Math.pow(Math.abs(x- innerCircleOx), 2) + Math.pow(Math.abs(event.getY() - innerCircleOy), 2)) <= outerCircleRadio;
             //上面判斷值存儲到mSwitchState中
             onlySetFlag(insideInnerCircle ? INNER_LOCATION : OUTER_LOCATION, LOCATION_MASK);
             //判斷目前是否可用、可點選、在點選範圍内且不再執行恢複動畫
             return isEnabled() && isClickable() && mBackgroundRectF.contains(x, y) && !mValueAnimator.isRunning() && !mValueAnimator.isStarted();
         }
    }
               
  6. 分析

    ACTION_MOVE

    事件

    1)首先判斷目前的

    ACTION_MOVE

    事件對應标記位是否已經更新

    !judgeIsMoveEvent()

    ,若為更新則更新标記位

    onlySetFlag(MOVE_EVENT, EVENT_MASK);

    //取出對應标記值判斷目前是否處于滑動狀态,若不處于滑動狀态,則更新EVENT_MASK(事件标記位)為對應的值
    if (!judgeIsMoveEvent()) {
         onlySetFlag(MOVE_EVENT, EVENT_MASK);
     }
               
    2)按照業務邏輯,由于我們滑動内圓,我們應該更新對應内圓的位置,但是對應的若

    ACTION_DOWN

    落點不在我們當時内圓的範圍内,此時我們的

    ACTION_MOVE

    将不處理此系列事件直接抛棄,若在園内我們将更新移動的位置,且保證内圓位置在上面限定的範圍内即背景範圍内,結合更新

    事件标記位

    代碼如下:
    if (event.getAction() == MotionEvent.ACTION_MOVE) {
                //取出對應标記值判斷目前是否處于滑動狀态,若不處于滑動狀态,則更新EVENT_MASK(事件标記位)為對應的值
                if (!judgeIsMoveEvent()) {
                    onlySetFlag(MOVE_EVENT, EVENT_MASK);
                }
                //判斷目前是否處于業務滑動事件且系列事件最開始事件起始落點在最初狀态的内圓内(實際按照外圓半徑計算)
                //true 更新内圓innerCircleOx值,更新界面
                //false 不處理
                if (judgeIsMoveEvent() && locationIsInner()) {
                    innerCircleOx = x <= innerCircleMaxLeftX ? innerCircleMaxLeftX : x >= innerCircleMaxRightX ? innerCircleMaxRightX : x;
                    invalidate();
    }
               
  7. 事件結束處理

    ACTION_UP

    ACTION_MOVE

    事件
    由于

    SwitchButton

    狀态的改變牽扯到兩種方式,是以我們要分類處理不同的方式,通過方法

    judgeIsMoveEvent()

    取出對應的标志位判斷是否出現滑動事件來判斷是點選模式還是滑動模式
    1)

    judgeIsMoveEvent()=true

    即滑動模式:首先把對應的事件标記位重置(為了避免後面忘記重置事件标記位)

    onlySetFlag(OTHER_EVENT, EVENT_MASK);

    ,接着要判斷

    ACTION_DOWN

    事件落點是否在當時的内圓内,若不在則不作任何處理,若在則更新目前

    innerCircleOx = x <= innerCircleMaxLeftX ? innerCircleMaxLeftX : x >= innerCircleMaxRightX ? innerCircleMaxRightX : x;

    判斷目前事件的

    X

    軸坐标是否在對應移動範圍的臨界值,在根據目前

    innerCircleOx

    是否過移動範圍的一半對應的

    X軸坐标

    來判斷目前

    SwitchButton

    所處的狀态

    boolean stateIsOpen = innerCircleOx > (innerCircleMaxRightX + innerCircleMaxLeftX) / 2f;

    然後更新

    mSwitchState

    對應的标記位

    onlySetFlag(stateIsOpen ? STATE_OPEN : STATE_CLOSE, STATE_MASK);

    ,接着我們配置我們的動畫來補充未完成的滑動過程,由于我們此時事件的

    X

    軸坐标可能恰好在移動範圍的臨界值上,這樣我們就沒必要在配置動畫進行狀态過程補充了這樣也能避免一些不必要的資源浪費,即:
    if (innerCircleOx == innerCircleMaxLeftX || innerCircleOx == innerCircleMaxRightX) {
         return true;
     }
               
    若在臨界範圍之間則調用方法

    startSwitchAnimation

    來補充未完成的進度即:
    animatorStartX = innerCircleOx;
     animatorEndX = stateIsOpen ? innerCircleMaxRightX : innerCircleMaxLeftX;
     startSwitchAnimation(animatorStartX, animatorEndX);
               
    2)

    judgeIsMoveEvent()=false

    即點選模式:這個相對好處理點,直接從對應臨界值到另一個臨界值的過程即:
    //到此為點選事件,直接修改switchState值,且開始過度動畫從老值過渡到最新值
      animatorStartX = innerCircleOx;
      animatorEndX = stateIsOpen() ? innerCircleMaxLeftX : innerCircleMaxRightX;
      onlySetFlag(stateIsOpen() ? STATE_CLOSE : STATE_OPEN, STATE_MASK);
      startSwitchAnimation(animatorStartX, animatorEndX);
               
    運作看一下我們的效果:
    自定義View-SwitchButton
    從效果來看我們大緻實作我們的功能,但是不夠美觀我們之前的狀态顔色也沒有用上,是以我們需讓顔色對号入座
  8. 為了使我們的

    SwitchButton

    更加炫酷,我們使内圓和背景的顔色随内圓運動在兩種狀态顔色中過渡呈現,我們要根據内圓圓心的

    X軸坐标

    在其移動範圍的進度值來動态計算對應的顔色。

    1)首先我們在方法

    onDraw()

    中計算進度值:

    float percent = (innerCircleOx - innerCircleMaxLeftX) / (innerCircleMaxRightX - innerCircleMaxLeftX);

    2)建立類

    ArgbEvaluator mArgbEvaluator;

    用來在方法

    onDraw()

    計算所處進度對應的顔色值:
    //目前background的color
    int backgroundColor = (int) mArgbEvaluator.evaluate(percent, mBackgroundCloseColor, mBackgroundOpenColor);
    //内圓目前的顔色
    int innerCircleColor = (int) mArgbEvaluator.evaluate(percent, mInnerCircleCloseColor, mInnerCircleOpenColor);
               
    3)是以更新

    onDraw()

    方法代碼為:
    @Override
        protected void onDraw(Canvas canvas) {
            //目前内圓圓心所在位置對應的進度
            float percent = (innerCircleOx - innerCircleMaxLeftX) / (innerCircleMaxRightX - innerCircleMaxLeftX);
            //目前background的color
            int backgroundColor = (int) mArgbEvaluator.evaluate(percent, mBackgroundCloseColor, mBackgroundOpenColor);
            //内圓目前的顔色
            int innerCircleColor = (int) mArgbEvaluator.evaluate(percent, mInnerCircleCloseColor, mInnerCircleOpenColor);
    
            mPaint.setColor(backgroundColor);
            canvas.drawRoundRect(mBackgroundRectF, outerCircleRadio, outerCircleRadio, mPaint);
    
            mPaint.setColor(innerCircleColor);
            canvas.drawCircle(innerCircleOx, innerCircleOy, innerCircleRadio, mPaint);
        }
               
    運作效果為:
    自定義View-SwitchButton
    從效果上來我們對顔色的漸變已經達到了我們的效果,nice!

四、測量View

因為自己每次自定義View時,都會非常糾結這部分,因為涉及到測量模式

wrap_content

,需要自己業務的需要和實際考慮來傳回View的測量寬高
  1. 未重寫方法

    onMeasure

    方法出現的問題,舉個例子我們給

    SwitchButton

    寬度固定值

    100dp

    ,高度為自适應

    warp_content

    ,

    padding

    值為

    50dp

    ,為了更好顯示突出我們的視覺邏輯效果,我們給控件一個背景色為

    #32cd32

    布局檔案如下:
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <vip.zhuhailong.blogapplication.SwitchButton
            android:id="@+id/switchButton"
            android:layout_width="100dp"
            android:padding="50dp"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true" />
    
    </RelativeLayout>
               
    運作看一下效果:
    自定義View-SwitchButton

    what ?SwitcButton呢?

    由圖中可知道我們View在實實在在的存在的,但是我們的

    SwitchButton

    卻不見了,這是由于我們的實際繪制區域寬度變為了 ,我們在設定寬度為

    100dp

    ,而

    padding

    值為

    50dp

    ,我們在計算外圓半徑的計算方式為

    (w - getPaddingLeft() - getPaddingRight()) / 5f

    ,也就是

    100dp-50dp-50dp=0dp

    也就是寬度為 了自然也就沒有空白了,當然隻是我們需要自己重寫

    onMeasure

    方法的特殊情況,是以我們修改

    padding

    值為

    10dp

    在重新運作一下為:
    自定義View-SwitchButton
    這樣我們就要顯示出來,但是卻為突出不重寫

    onMeasure

    方法會出現的問題,從圖中的效果來看(不從源碼分析,網上有很多關于源碼的分析當家可以自己去檢視),在未重寫

    onMeasure

    方法時,系統

    warp_content

    測量值和

    match_parent

    測量值一樣的,現在我們修改控件的寬為

    warp_content

    ,高度為固定值為

    100dp

    ,

    padding

    值為

    10dp

    ,布局檔案如下:
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <vip.zhuhailong.blogapplication.SwitchButton
            android:id="@+id/switchButton"
            android:layout_width="wrap_content"
            android:padding="10dp"
            android:background="#32cd32"
            android:layout_height="100dp"
            android:layout_centerInParent="true" />
    
    </RelativeLayout>
               
    運作效果如圖:
    自定義View-SwitchButton

    surprise!

    這樣我們的問題就凸顯出來了,因為在規定

    SwitchButton的背景寬度

    是其

    高度的2.5倍

    ,而我們的

    SwitchButton的寬度

    w - getPaddingLeft() - getPaddingRight()

    ,那麼

    SwitchButton的高度為:(w - getPaddingLeft() - getPaddingRight())*2/5f

    ,而之前我們設定了控件的

    高度

    為固定值

    100dp

    ,此時由于

    SwitchButton的高度

    是幾乎充滿水準防線的螢幕寬度的,也就是說

    SwitchButton的高度

    是有很大的可能大于

    100dp

    的,這也就造成了我們

    View

    控件提供的高度不足以繪制我們的

    SwitchButton背景

    ,也就是現在顯示的殘缺不全的效果,是以我們需要自己根據View的實際可繪制的寬高來配置我們

    SwitchButton

    寬高,進行繪制,是以我們就需要重寫

    onMeasure

    方法。
  2. 重寫

    onMeasure

    方法,關于

    onMeasure

    的一些知識這裡就不在介紹了,需要了解的自行去搜尋

    1)

    MeasureSpec.EXACTLY

    模式即

    match_parent或者是給定确切固定值

    ,這裡比較簡單,如果是這模式我們直接采用系統給我們側臉好的值傳回就行了,後面再根據實際的業務需要來計算我們對應的屬性(這裡沒什麼好說的)

    2)

    MeasureSpec.AT_MOST

    模式即

    warp_content

    ,這裡是我最糾結的部分了,因為在這個模式系統會給我們提供目前

    parent

    能提供給的最大空間,我們可以選擇給自己設定的最小預設值來進行繪制,也可以根據

    parent

    提供的最大空間結合業務邏輯來計算對應的屬性,這裡先采用給予設定的預設值來計算繪制我們的

    SwitchButton

    3)

    MeasureSpec.UNSPECIFIED

    模式,這種方式未指定尺寸,這種模式用的特别的少,這裡也沒什麼好講的,直接舍去,下面就開始就行我們實際編碼

    onMeasure

    方法
  3. 根據我們的業務邏輯重寫

    onMeasure

    方法來測量我們的空間,進而進行

    SwitchButton

    的繪制

    1)設定

    SwitchButton

    繪制區域的最小寬度,定義屬性

    int defaultMinDimension = 50

    也就是最小寬度,根據最前面的邏輯(

    SwitchButton的寬是高的2.5倍

    )來計算

    SwitchButton

    的高度即

    defaultMinDimension/2.5f

    2)在

    init()

    方法中先将最小的預設值賦予我們存儲空間寬高的屬性用于onMeasure方法來進行測量
    mWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension, getResources().getDisplayMetrics());
    mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension / 2.5f, getResources().getDisplayMetrics());
               
    3)測量View的寬度:擷取寬度的測量模式

    int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);

    然後擷取系統在目前測量模式提供的測量參考值

    int measureWith = MeasureSpec.getSize(widthMeasureSpec);

    ,若測量模式為

    MeasureSpec.EXACTLY

    ,則直接采用系統提供的測量參考值,若測量模式不為

    MeasureSpec.EXACTLY

    ,此時我們将

    MeasureSpec.AT_MOST和MeasureSpec.UNSPECIFIED

    并在一起處理,此時我們采用自己定義的

    預設值

    加上

    paddingLeft

    paddingRight

    為:

    measureWith = mWidth + getPaddingLeft() + getPaddingRight();

    ,此時我們不管這樣計算得來的值是否超過

    parent

    給我們提供的空間,因為這是我們的底線!哈哈

    4)測量View的高度:擷取高度的測量模式

    int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);

    ,然後擷取系統在目前測量模式提供的測量參考值

    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);

    ,和測量寬度一樣,若測量模式為:

    MeasureSpec.EXACTLY

    ,則直接采用系統提供的測量參考值,若測量模式不為

    MeasureSpec.EXACTLY

    ,此時我們将

    MeasureSpec.AT_MOST和MeasureSpec.UNSPECIFIED

    并在一起處理,此時我們采用自定義的

    預設值

    加上

    paddingTop

    paddingBottom

    為:

    measureHeight = getPaddingTop() + getPaddingBottom() + mHeight;

    5)設定我們最終View的測量結果

    setMeasuredDimension(measureWith, measureHeight)

    6)

    onMeasure

    代碼為:
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
            int measureWith = MeasureSpec.getSize(widthMeasureSpec);
            int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
            int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    
            if (widthMeasureMode != MeasureSpec.EXACTLY) {
                measureWith = mWidth + getPaddingLeft() + getPaddingRight();
            }
            if (heightMeasureMode != MeasureSpec.EXACTLY) {
                measureHeight = getPaddingTop() + getPaddingBottom() + mHeight;
            }
            setMeasuredDimension(measureWith, measureHeight);
        }
               
  4. 測量完

    View

    後并不是大功告成直接進行我們的

    layout

    onDraw

    工作,

    onMeasure

    并不是

    View

    最終的寬高,它還牽扯到它的父控件

    ViewGroup

    為它布局時實際賦予的寬高也就是我們後面使用

    View

    中方法

    getWidth()和getHeight()

    擷取的值,此值不一定等于我們的測量值,但是測量值它是

    ViewGroup

    測量和布局的一個參考,是以我們還要在得到實際控件的寬高時,對繪制需要的一些屬性就行初始化操作,這裡我們在方法

    onSizeChanged

    中進行,因為控件的寬高發生變化時會回調這個方法,屆時我們就可以再次重新初始化我們的繪制屬性了在這裡我們要注意的時上面示範過得問題的,就是有可能我們的繪制區域超過了我們的View範圍,是以在這裡我們要結合View的實際寬高來計算我們的繪制相關屬性。

    1)确定我們的外圓半徑,我們先按照View的寬度來計算外圓半徑

    float radioByWidth = (w - getPaddingLeft() - getPaddingRight()) / 5f

    ,然後利用外圓半徑計算

    SwitchButton

    繪制區域的高度加上

    paddingTop

    paddingBottom

    ,再将其和于View的高度進行比較,得到

    boolean

    型變量

    isResize = radioByWidth * 2 + getPaddingTop() + getPaddingBottom() > h;

    然後根據這個變量來重新判斷是否重新根據

    View

    的高度來計算外圓半徑,若

    isResize=false

    則直接采用根據

    View寬度

    計算得來的外圓半徑,若

    isResize=true

    則需要根據

    View

    的高度來重新計算外圓半徑即

    outerCircleRadio = isResize ? (h - getPaddingTop() - getPaddingBottom()) / 2f : radioByWidth

    2)計算我們内圓半徑,這個相對要簡單,因為上面已經确定了外圓半徑了我們隻要取外圓半徑的

    0.9

    倍的值即可:

    innerCircleRadio = outerCircleRadio *0.9

    3)确定

    SwitchButton

    繪制區域,配置

    mBackgroundRectF

    ,根據

    isResize

    的值來動态計算,定義變量

    mBackgroundRectLeft和mBackgroundRectRight

    分别用來存儲

    mBackgroundRectF

    left和right

    ,計算比較簡單也比較好了解,這裡不接講述直接貼出:
    //計算背景mBackgroundRectF的left和right
    float mBackgroundRectLeft = isResize ? (mWidth - outerCircleRadio * 5f) / 2 : getPaddingLeft();
    float mBackgroundRectRight = isResize ? (mWidth + outerCircleRadio * 5f) / 2 : mWidth - getPaddingRight();
    // 計算繪制背景區域, 根據背景的寬高比例為2.5f 來計算
    mBackgroundRectF.set(mBackgroundRectLeft, mHeight / 2f - outerCircleRadio, mBackgroundRectRight, mHeight / 2f + outerCircleRadio);
    
               
    4)配置我們的内圓移動範圍,這個也較簡單好了解,是以直接貼出:
    ```
     //初始化innerCircleOx的範圍
     innerCircleMaxLeftX = mBackgroundRectLeft + outerCircleRadio;
     innerCircleMaxRightX = mBackgroundRectRight - outerCircleRadio;
     ```
               
    5)根據目前

    SwitchButton

    的狀态确定目前内圓的位置.
    //初始化内圓圓心坐标即内圓位置
    innerCircleOx = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX;
    innerCircleOy = h >> 1;
               
    6)完成的

    onSizeChanged

    方法:
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //擷取view的寬高
        mWidth = w;
        mHeight = h;
        //計算outerCircle的半徑
        float radioByWidth = (mWidth - getPaddingLeft() - getPaddingRight()) / 5f;
        //根據寬高來判斷radioByWidth是否符合對應的比例
        boolean isResize = radioByWidth * 2 + getPaddingTop() + getPaddingBottom() > mHeight;
        outerCircleRadio = isResize ? (mHeight - getPaddingTop() - getPaddingBottom()) / 2f : radioByWidth;
        //計算背景mBackgroundRectF的left和right
        float mBackgroundRectLeft = isResize ? (mWidth - outerCircleRadio * 5f) / 2 : getPaddingLeft();
        float mBackgroundRectRight = isResize ? (mWidth + outerCircleRadio * 5f) / 2 : mWidth - getPaddingRight();
        // 計算繪制背景區域, 根據背景的寬高比例為2.5f 來計算
        mBackgroundRectF.set(mBackgroundRectLeft, mHeight / 2f - outerCircleRadio, mBackgroundRectRight, mHeight / 2f + outerCircleRadio);
        //計算内圓半徑
        innerCircleRadio = outerCircleRadio * 0.9f;
        //初始化innerCircleOx的範圍
        innerCircleMaxLeftX = mBackgroundRectLeft + outerCircleRadio;
        innerCircleMaxRightX = mBackgroundRectRight - outerCircleRadio;
        //初始化内圓圓心坐标即内圓位置
        innerCircleOx = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX;
        innerCircleOy = h >> 1;

    }
           

再次運作之前測試的配置如下:

自定義View-SwitchButton

完美解決!

五、

SwitchButton

狀态的儲存和恢複

在日常使用app過程中手機有時難免會橫放,緻使螢幕旋轉,而這時我們的View經曆銷毀然後重建,然後再回複之前的狀态,系統提供的TextView等控件隻要我們給其唯一的

id

,系統都能正确恢複其銷毀前的狀态,那我們看看我們繼承View來碼的

SwitchButton

在螢幕發生旋轉時系統會不會為其恢複,當然我們也會給其唯一的

Id

(至于為什麼需要給其唯一的ID系統才能恢複等不是本篇探讨的内容想要深入了解的可以去網上搜尋相關資料或者閱讀源碼),測試效果圖如下,先将

SwitchButton

打開然後再旋轉螢幕,發現

SwitchButton

并沒有保持打開的狀态也就是說系統沒有幫我進行資料狀态的恢複,是以我們要自己手動去儲存且恢複狀态:
自定義View-SwitchButton

要想達到我們想要的效果需要重寫兩個,方法

protected Parcelable onSaveInstanceState()

用于儲存空間的目前屬性狀态,方法

protected void onRestoreInstanceState(Parcelable state)

用于恢複空間的屬性,在重新建立控件類後會調用此房方法,我們參考

TextView

的實作(大家自行去檢視源碼,這裡不再探讨了),建立類

SaveState

繼承

View

中的靜态類

BaseSavedState

,這裡我們需要儲存目前

SwitchButton的開關狀态

即:

static class SaveState extends BaseSavedState {
		//對應SwitchButton的mSwitchState屬性
        private int switchState;	

        public SaveState(Parcel source) {
            super(source);
            switchState = source.readInt();
        }

        public SaveState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(switchState);
        }
        
        public static final Parcelable.Creator<SaveState> CREATOR = new Creator<SaveState>() {
            @Override
            public SaveState createFromParcel(Parcel source) {
                return new SaveState(source);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };

    }
           

1)重寫方法儲存屬性狀态的方法

protected Parcelable onSaveInstanceState()

@Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        SaveState saveState = new SaveState(parcelable);
        saveState.switchState = mSwitchState;
        return saveState;
    }
           

2)重寫方法儲存屬性狀态的方法

protected void onRestoreInstanceState(Parcelable state)

@Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof SaveState) {
            SaveState saveState = (SaveState) state;
            super.onRestoreInstanceState(saveState.getSuperState());
            mSwitchState = saveState.switchState;
        } else {
            super.onRestoreInstanceState(state);
        }

    }
           

至此我們的屬性的儲存及恢複就完成了,以下為測試結果,由此可見我們我們恢複效果達到了:

自定義View-SwitchButton

六、給

SwitchButton

添加狀态監聽

其實這個過程還是比較簡單的,直接開碼
  1. 定義接口

    SwitchListener

    /**
         * 回調狀态
         */
        public interface SwitchListener {
            void switchListener(boolean open);
        }
    
               
  2. SwitchButton

    添加

    SwitchListener

    相關配置操作
    /**
         * switch button 狀态監聽
         */
        private SwitchListener mSwitchListener;
    
        public void setSwitchListener(SwitchListener switchListener) {
            mSwitchListener = switchListener;
        }
               
  3. 在合适的地方法進行回調我們的監聽函數,思考一下,無非就是判斷目前是否發生了

    SwitchButton

    狀态的變化來回調

    1)先說發生狀态改變的情況,這時無非就是

    點選

    滑動

    來更改

    SwitchButton

    的狀态,是以我們隻需要在執行動畫的方法

    startSwitchAnimation(float animatorStartX, float animatorEndX, boolean flagIsChange)

    中進行回調就行了,而在

    滑動

    方式更改狀态的情況下,滑動事件中的

    X

    的值可能為臨界值,這時因為已經是目前要過渡到的效果了是以為了減少一些沒必要資源開銷,我們舍棄了過渡動畫隻要進行監聽回調即可如下:
    @Override
        public boolean onTouchEvent(MotionEvent event) {
            //省略
            if (judgeIsMoveEvent()) {
               		//省略
                    onlySetFlag(stateIsOpen ? STATE_OPEN : STATE_CLOSE, STATE_MASK);
                    if (innerCircleOx == innerCircleMaxLeftX || innerCircleOx == innerCircleMaxRightX) {
                        if (mSwitchListener != null) {
                            mSwitchListener.switchListener(stateIsOpen);
                        }
                        return true;
                    }
                    animatorStartX = innerCircleOx;
                    animatorEndX = stateIsOpen ? innerCircleMaxRightX : innerCircleMaxLeftX;
                    startSwitchAnimation(animatorStartX, animatorEndX);
                }
            } else {
                //到此為點選事件,直接修改switchState值,且開始過度動畫從老值過渡到最新值
                animatorStartX = innerCircleOx;
                animatorEndX = stateIsOpen() ? innerCircleMaxLeftX : innerCircleMaxRightX;
                onlySetFlag(stateIsOpen() ? STATE_CLOSE : STATE_OPEN, STATE_MASK);
                startSwitchAnimation(animatorStartX, animatorEndX);
            }
    
            //省略
        }
    
        /**
         * 設定我們的恢複動畫相關的屬性且開始動畫
         *
         * @param animatorStartX 動畫開始值
         * @param animatorEndX   動畫結束值
         */
        private void startSwitchAnimation(float animatorStartX, float animatorEndX) {
            mValueAnimator.setFloatValues(animatorStartX, animatorEndX);
            //動态計算動畫完成時間
            mValueAnimator.setDuration((long) ((Math.abs(animatorEndX - animatorStartX) * 1L / ((mWidth - getPaddingLeft() - getPaddingRight() / 2L))) * mAnimationDuration));
            mValueAnimator.start();
            //switchState狀态監聽回調
            if (mSwitchListener != null ) {
                mSwitchListener.switchListener(stateIsOpen());
            }
        }
    
               
    2)未發生狀态改變的情況,大家可能疑問了,未發生肯定就不調用啊,這個自然,但是我們的回調有一處是寫在執行動畫的方法裡的,因為目前可能未發生狀态的改變但是内圓的位置卻不在該狀态所在的位置,是以我們還要使用動畫來過渡,是以我們還要在執行動畫的方法裡還要判斷目前是否發生的了變化,是以在方法

    startSwitchAnimation(float animatorStartX, float animatorEndX, boolean flagIsChange)

    中增加

    boolean

    型變量控制是否執行回調,為了更好判斷是否發生了變化,是以我們建立方法

    boolean setFlagAndGetChangeValue(int flag, int mask)

    ,在修改标志位的同時,判斷目前狀态是否發生了變化,是以以上代碼修改如下:
    /**
         * 廢除了ClickListener和LongClickListener
         *
         * @param event 事件
         */
        @Override
        public boolean onTouchEvent(MotionEvent event) {
           		//省略
                //判斷是上個事件是否為業務滑動事件
                if (judgeIsMoveEvent()) {
                    onlySetFlag(OTHER_EVENT, EVENT_MASK);
                    if (locationIsInner()) {
                        innerCircleOx = x <= innerCircleMaxLeftX ? innerCircleMaxLeftX : x >= innerCircleMaxRightX ? innerCircleMaxRightX : x;
                        boolean stateIsOpen = innerCircleOx > (innerCircleMaxRightX + innerCircleMaxLeftX) / 2f;
                        boolean changeValue = setFlagAndGetChangeValue(stateIsOpen ? STATE_OPEN : STATE_CLOSE, STATE_MASK);
                        if (innerCircleOx == innerCircleMaxLeftX || innerCircleOx == innerCircleMaxRightX) {
                            if (mSwitchListener != null && changeValue) {
                                mSwitchListener.switchListener(stateIsOpen);
                            }
                            return true;
                        }
                        animatorStartX = innerCircleOx;
                        animatorEndX = stateIsOpen ? innerCircleMaxRightX : innerCircleMaxLeftX;
                        startSwitchAnimation(animatorStartX, animatorEndX,changeValue);
                    }
                } else {
                    //到此為點選事件,直接修改switchState值,且開始過度動畫從老值過渡到最新值
                    animatorStartX = innerCircleOx;
                    animatorEndX = stateIsOpen() ? innerCircleMaxLeftX : innerCircleMaxRightX;
                    onlySetFlag(stateIsOpen() ? STATE_CLOSE : STATE_OPEN, STATE_MASK);
                    startSwitchAnimation(animatorStartX, animatorEndX,true);
                }
            }
            return true;
        }
    
        /**
         * 設定我們的恢複動畫相關的屬性且開始動畫
         *
         * @param animatorStartX 動畫開始值
         * @param animatorEndX   動畫結束值
         */
        private void startSwitchAnimation(float animatorStartX, float animatorEndX, boolean flagIsChange) {
            mValueAnimator.setFloatValues(animatorStartX, animatorEndX);
            //動态計算動畫完成時間
            mValueAnimator.setDuration((long) ((Math.abs(animatorEndX - animatorStartX) * 1L / ((mWidth - getPaddingLeft() - getPaddingRight() / 2L))) * mAnimationDuration));
            mValueAnimator.start();
            //switchState狀态監聽回調
            if (mSwitchListener != null && flagIsChange) {
                mSwitchListener.switchListener(stateIsOpen());
            }
    
        }
               
    3)當然我們還需要能夠在代碼中控制改變

    SwitchButton

    的狀态,是以我們要添加方法

    public void setSwitchState(boolean state)

    來進行直接修改

    SwitchButton

    的狀态,當然,在這裡我們還需要判斷代碼設定前後是否發生了狀态的改變,來合理進行

    SwitchListener

    的回調和狀态過渡,代碼如下:
    /**
         * 設定我們Switch開關的狀态
         *
         * @param state 要設定的switchState的值
         */
        public void setSwitchState(boolean state) {
            boolean isUsefulSetting = setFlagAndGetChangeValue(state ? STATE_OPEN : STATE_CLOSE, STATE_MASK);
            if (isUsefulSetting) {
                float animatorStartX, animatorEndX;
                animatorStartX = innerCircleOx;
                animatorEndX = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX;
                startSwitchAnimation(animatorStartX, animatorEndX,true);
            }
        }
    
        /**
         * 設定目前的Flag且傳回目前是否發生了改變,
         *
         * @param flag 需要設定的值
         * @param mask 對應标志位
         * @return 是否發生了改變
         */
        private boolean setFlagAndGetChangeValue(int flag, int mask) {
            int oldState = mSwitchState;
            mSwitchState = (mSwitchState & ~mask) | (flag & mask);
            return (oldState ^ mSwitchState) > 0;
        }
    
               

    BlogActivity

    中先測試

    SwitchListener

    ,給

    SwitchButton

    設定監聽事件如下:
    public class MainActivity extends AppCompatActivity {
    
        private SwitchButton mSwitchButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mSwitchButton = ((SwitchButton) findViewById(R.id.switchButton));
    
            mSwitchButton.setSwitchListener(open ->
                    Toast.makeText(MainActivity.this, "目前狀态為" + (open ? "開" : "關"), Toast.LENGTH_SHORT).show()
            );
        }
    }
    
               
    為了更好顯示效果,我們設定

    SwitchButton

    的寬高為

    match_parent

    ,測試結果為:
    自定義View-SwitchButton
    從測試結果來看我們的

    SwitchListener

    是沒有問題的,現在我們來測試一下直接在代碼中修改

    SwitchButton

    的狀态,建立兩個

    button

    分别在其點選事件中修改

    SwitchButton

    的狀态,布局檔案和代碼如下:
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
    
        <vip.zhuhailong.blogapplication.SwitchButton
            android:id="@+id/switchButton"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:background="#32cd32"
            android:padding="10dp" />
    
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_marginTop="20dp"
            android:background="@color/colorAccent"
            android:orientation="horizontal">
    
    
            <Button
                android:id="@+id/closeBtn"
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:layout_weight="1"
                android:background="@drawable/btn_background"
                android:text="關閉按鈕" />
    
            <Button
                android:id="@+id/openBtn"
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:layout_weight="1"
                android:background="@drawable/btn_background"
                android:text="打開按鈕" />
    
        </LinearLayout>
    
    	public class MainActivity extends AppCompatActivity {
    
        private SwitchButton mSwitchButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mSwitchButton = ((SwitchButton) findViewById(R.id.switchButton));
    
            mSwitchButton.setSwitchListener(open ->
                    Toast.makeText(MainActivity.this, "目前狀态為" + (open ? "開" : "關"), Toast.LENGTH_SHORT).show()
            );
    
            findViewById(R.id.openBtn).setOnClickListener(v -> mSwitchButton.setSwitchState(true));
            findViewById(R.id.closeBtn).setOnClickListener(v -> mSwitchButton.setSwitchState(false));
        }
    }
    
    
               
    運作測試結果如下:
    自定義View-SwitchButton

七、自定義屬性

其實到達這一步驟,基本上我們的

SwitchButton

就完成了,但是為了使我們的控件更加好看,能夠靈活配合業務UI設計,我們可以将那些固定的顔色、外圓半徑和内圓半徑比變為可以在

xml

中配置的自定義屬性,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>


    <declare-styleable name="SwitchButton">
        <!--配置内圓半徑和外圓半徑的比率,動态調整内圓大小-->
        <attr name="circleRadioScale" format="float|reference" />
        <!--内圓未選中(打開)的顔色-->
        <attr name="innerCircleOpenColor" format="color|reference" />
        <attr name="innerCircleCloseColor" format="color|reference" />
        <!--背景區域選中(打開)顔色-->
        <attr name="backgroundOpenColor" format="color|reference" />
        <!--背景區域未選中(關閉)顔色-->
        <attr name="backgroundCloseColor" format="color|reference" />
        <!--關閉到打開動畫需要執行的總時長-->
        <attr name="animationDuration" format="integer" />
    </declare-styleable>

</resources>
           

是以我們需要修改方法

init()

在此擷取對應的自定義屬性,這裡不再講解一下貼出整個代碼邏輯:

public class SwitchButton extends View {

    private final String TAG = this.getClass().getSimpleName();


    /**
     * 繪制畫筆
     */
    private Paint mPaint;

    /**
     * 内圓半徑
     */
    private float innerCircleRadio;

    /**
     * 内圓圓心坐标
     */
    private float innerCircleOx, innerCircleOy, innerCircleMaxLeftX, innerCircleMaxRightX;

    /**
     * 内圓未選中時的顔色
     */
    @ColorInt
    private int mInnerCircleOpenColor;

    /**
     * 内圓選中時的顔色
     */
    @ColorInt
    private int mInnerCircleCloseColor;


    /**
     * 外圓半徑
     */
    private float outerCircleRadio;

    /**
     * 背景區域選中顔色
     */
    @ColorInt
    private int mBackgroundOpenColor;

    /**
     * 背景區域未選中顔色
     */
    @ColorInt
    private int mBackgroundCloseColor;

    /**
     * view 布局的寬高
     */
    private int mWidth, mHeight;

    /**
     * 背景繪制的區域
     */
    private RectF mBackgroundRectF;

    /**
     * 内圓半徑和外圓半徑比
     */
    private float circleRadioScale;

    /**
     * 預設switch按鈕最小的background寬度
     */
    @Dimension
    private int defaultMinDimension = 50;

    /**
     * 恢複動畫
     */
    private ValueAnimator mValueAnimator;

    /**
     * 動畫執行時長
     */
    private long mAnimationDuration;


    /**
     * switch開關- 事件最開始落點圈外
     */
    private final int OUTER_LOCATION = 0x0;

    /**
     * switch開關- 事件最開始落點圈内
     */
    private final int INNER_LOCATION = 0x1;

    /**
     * switch開關-事件最開始落點标志位
     */
    private final int LOCATION_MASK = 0x1;

    /**
     * switch開關-非移動事件
     */
    private final int OTHER_EVENT = 0x2;

    /**
     * switch開關-移動事件
     */
    private final int MOVE_EVENT = 0x4;


    /**
     * switch開關-是否處理事件标志位
     */
    private final int EVENT_MASK = 0x6;

    /**
     * switch開關-關閉狀态
     */
    private final int STATE_CLOSE = 0x8;

    /**
     * switch開關-打開狀态
     */
    private final int STATE_OPEN = 0x10;

    /**
     * switch開關-狀态标志位
     */
    private final int STATE_MASK = 0x18;


    /**
     * switch開關-預設狀态
     */
    private int mSwitchState;

    /**
     * 用于計算過度顔色
     */
    private ArgbEvaluator mArgbEvaluator;

    /**
     * switch button 狀态監聽
     */
    private SwitchListener mSwitchListener;

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

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

    public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
        Log.e(TAG, "SwitchButton ");
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        //設定為可點選,否則将無法接收到對應的一些列事件也就無法消費事件
        setClickable(true);

        //先将最小的預設值賦予我們存儲空間寬高的屬性用于onMeasure方法來進行測量
        mWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension, getResources().getDisplayMetrics());
        mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension / 2.5f, getResources().getDisplayMetrics());

        //擷取自定義屬性
        TypedArray typedAttributes = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);
        //預設的内圓半徑和外圓半徑比 預設值為0.9f
        circleRadioScale = typedAttributes.getFloat(R.styleable.SwitchButton_circleRadioScale, 0.9f);
        //背景和内圓變化的顔色
        mBackgroundCloseColor = typedAttributes.getColor(R.styleable.SwitchButton_backgroundCloseColor, Color.GRAY);
        mBackgroundOpenColor = typedAttributes.getColor(R.styleable.SwitchButton_backgroundOpenColor, Color.WHITE);
        mInnerCircleCloseColor = typedAttributes.getColor(R.styleable.SwitchButton_innerCircleCloseColor, Color.WHITE);
        mInnerCircleOpenColor = typedAttributes.getColor(R.styleable.SwitchButton_innerCircleOpenColor, Color.GRAY);

        //設定動畫執行的時長
        mAnimationDuration = typedAttributes.getInt(R.styleable.SwitchButton_animationDuration, 1000);
        typedAttributes.recycle();

        //計算色彩值
        mArgbEvaluator = new ArgbEvaluator();

        //初始化switch狀态
        mSwitchState = (STATE_MASK & STATE_CLOSE) | (EVENT_MASK & MOVE_EVENT) | (LOCATION_MASK & OUTER_LOCATION);
        //繪制畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);

        //背景繪制區域
        mBackgroundRectF = new RectF();

        //初始指派innerCircleOx和innerCircleOy
        innerCircleOx = innerCircleOy = innerCircleMaxLeftX = innerCircleMaxRightX - 1;

        //恢複動畫
        mValueAnimator = new ValueAnimator();
        mValueAnimator.addUpdateListener(animation -> {
            innerCircleOx = (float) animation.getAnimatedValue();
            invalidate();
        });
        mValueAnimator.setInterpolator(new BounceInterpolator());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //擷取view的寬高
        mWidth = w;
        mHeight = h;
        //計算outerCircle的半徑
        float radioByWidth = (mWidth - getPaddingLeft() - getPaddingRight()) / 5f;
        //根據寬高來判斷radioByWidth是否符合對應的比例
        boolean isResize = radioByWidth * 2 + getPaddingTop() + getPaddingBottom() > mHeight;
        outerCircleRadio = isResize ? (mHeight - getPaddingTop() - getPaddingBottom()) / 2f : radioByWidth;
        //計算背景mBackgroundRectF的left和right
        float mBackgroundRectLeft = isResize ? (mWidth - outerCircleRadio * 5f) / 2 : getPaddingLeft();
        float mBackgroundRectRight = isResize ? (mWidth + outerCircleRadio * 5f) / 2 : mWidth - getPaddingRight();
        // 計算繪制背景區域, 根據背景的寬高比例為2.5f 來計算
        mBackgroundRectF.set(mBackgroundRectLeft, mHeight / 2f - outerCircleRadio, mBackgroundRectRight, mHeight / 2f + outerCircleRadio);
        //計算内圓半徑
        innerCircleRadio = outerCircleRadio * circleRadioScale;
        //初始化innerCircleOx的範圍
        innerCircleMaxLeftX = mBackgroundRectLeft + outerCircleRadio;
        innerCircleMaxRightX = mBackgroundRectRight - outerCircleRadio;
        //初始化内圓圓心坐标即内圓位置
        innerCircleOx = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX;
        innerCircleOy = h >> 1;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        //目前内圓圓心所在位置對應的進度
        float percent = (innerCircleOx - innerCircleMaxLeftX) / (innerCircleMaxRightX - innerCircleMaxLeftX);
        //目前background的color
        int backgroundColor = (int) mArgbEvaluator.evaluate(percent, mBackgroundCloseColor, mBackgroundOpenColor);
        //内圓目前的顔色
        int innerCircleColor = (int) mArgbEvaluator.evaluate(percent, mInnerCircleCloseColor, mInnerCircleOpenColor);

        mPaint.setColor(backgroundColor);
        canvas.drawRoundRect(mBackgroundRectF, outerCircleRadio, outerCircleRadio, mPaint);

        mPaint.setColor(innerCircleColor);
        canvas.drawCircle(innerCircleOx, innerCircleOy, innerCircleRadio, mPaint);
    }

    /**
     * 廢除了ClickListener和LongClickListener
     *
     * @param event 事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //記錄系列事件最開始事件起始的坐标X
            //判斷系列事件最開始事件落點坐标是否在内圓中
            boolean insideInnerCircle = Math.sqrt(Math.pow(Math.abs(x - innerCircleOx), 2) + Math.pow(Math.abs(event.getY() - innerCircleOy), 2)) <= outerCircleRadio;
            //上面判斷值存儲到mSwitchState中
            onlySetFlag(insideInnerCircle ? INNER_LOCATION : OUTER_LOCATION, LOCATION_MASK);
            //判斷目前是否可用、可點選、在點選範圍内且不再執行恢複動畫
            return isEnabled() && isClickable() && mBackgroundRectF.contains(x, y) && !mValueAnimator.isRunning() && !mValueAnimator.isStarted();
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //取出對應标記值判斷目前是否處于滑動狀态,若不處于滑動狀态,則更新EVENT_MASK(事件标記位)為對應的值
            if (!judgeIsMoveEvent()) {
                onlySetFlag(MOVE_EVENT, EVENT_MASK);
            }
            //判斷目前是否處于業務滑動事件且系列事件最開始事件起始落點在最初狀态的内圓内(實際按照外圓半徑計算)
            //true 更新内圓innerCircleOx值,更新界面
            //false 不處理
            if (judgeIsMoveEvent() && locationIsInner()) {
                innerCircleOx = x <= innerCircleMaxLeftX ? innerCircleMaxLeftX : x >= innerCircleMaxRightX ? innerCircleMaxRightX : x;
                invalidate();
            }
        } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
            float animatorStartX, animatorEndX;
            //判斷是上個事件是否為業務滑動事件
            if (judgeIsMoveEvent()) {
                //判斷系列事件最開始事件落點坐标是否在内圓外
                //是-則恢複EVENT_MASK位為OTHER_EVENT,return中斷後續操作
                //否-則進行switchButton狀态修改,且開始過度動畫過渡到修改的最新值
                onlySetFlag(OTHER_EVENT, EVENT_MASK);
                if (locationIsInner()) {
                    innerCircleOx = x <= innerCircleMaxLeftX ? innerCircleMaxLeftX : x >= innerCircleMaxRightX ? innerCircleMaxRightX : x;
                    boolean stateIsOpen = innerCircleOx > (innerCircleMaxRightX + innerCircleMaxLeftX) / 2f;
                    boolean flagIsChange = setFlagAndGetChangeValue(stateIsOpen ? STATE_OPEN : STATE_CLOSE, STATE_MASK);
                    if (innerCircleOx == innerCircleMaxLeftX || innerCircleOx == innerCircleMaxRightX) {
                        if (mSwitchListener != null&&flagIsChange) {
                            mSwitchListener.switchListener(stateIsOpen);
                        }
                        return true;
                    }
                    animatorStartX = innerCircleOx;
                    animatorEndX = stateIsOpen ? innerCircleMaxRightX : innerCircleMaxLeftX;
                    startSwitchAnimation(animatorStartX, animatorEndX,flagIsChange);
                }
            } else {
                Log.e(TAG, "onTouchEvent click");
                //到此為點選事件,直接修改switchState值,且開始過度動畫從老值過渡到最新值
                animatorStartX = innerCircleOx;
                animatorEndX = stateIsOpen() ? innerCircleMaxLeftX : innerCircleMaxRightX;
                onlySetFlag(stateIsOpen() ? STATE_CLOSE : STATE_OPEN, STATE_MASK);
                startSwitchAnimation(animatorStartX, animatorEndX,true);
            }
        }
        return true;
    }

    /**
     * 設定我們的恢複動畫相關的屬性且開始動畫
     *
     * @param animatorStartX 動畫開始值
     * @param animatorEndX   動畫結束值
     */
    private void startSwitchAnimation(float animatorStartX, float animatorEndX,boolean flagIsChange) {
        mValueAnimator.setFloatValues(animatorStartX, animatorEndX);
        //動态計算動畫完成時間
        mValueAnimator.setDuration((long) ((Math.abs(animatorEndX - animatorStartX) * 1L / ((mWidth - getPaddingLeft() - getPaddingRight() / 2L))) * mAnimationDuration));
        mValueAnimator.start();
        //switchState狀态監聽回調
        if (mSwitchListener != null&&flagIsChange) {
            mSwitchListener.switchListener(stateIsOpen());
        }

    }


    /**
     * 設定我們Switch開關的狀态
     *
     * @param state 要設定的switchState的值
     */
    public void setSwitchState(boolean state) {
        boolean isUsefulSetting = setFlagAndGetChangeValue(state ? STATE_OPEN : STATE_CLOSE, STATE_MASK);
        if (isUsefulSetting) {
            float animatorStartX, animatorEndX;
            animatorStartX = innerCircleOx;
            animatorEndX = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX;
            startSwitchAnimation(animatorStartX, animatorEndX,true);
        }
    }

    /**
     * 設定目前的Flag且傳回目前是否發生了改變,
     *
     * @param flag 需要設定的值
     * @param mask 對應标志位
     * @return 是否發生了改變
     */
    private boolean setFlagAndGetChangeValue(int flag, int mask) {
        int oldState = mSwitchState;
        mSwitchState = (mSwitchState & ~mask) | (flag & mask);
        return (oldState ^ mSwitchState) > 0;
    }

    /**
     * 設定目前的Flag
     *
     * @param flag 需要設定的值
     * @param mask 對應标志位
     */
    private void onlySetFlag(int flag, int mask) {
        mSwitchState = (mSwitchState & ~mask) | (flag & mask);
    }

    /**
     * 從mSwitchState取出switch開關的狀态,判斷是否為打開狀态
     *
     * @return 否為打開狀态
     */
    public boolean stateIsOpen() {
        return (mSwitchState & STATE_MASK) == STATE_OPEN;
    }

    /**
     * 從mSwitchState取出event_mask位值,判斷目前是否為業務滑動事件
     *
     * @return 是否為業務滑動事件
     */
    public boolean judgeIsMoveEvent() {
        return (mSwitchState & EVENT_MASK) == MOVE_EVENT;
    }

    /**
     * 從mSwitchState取出系列事件ACTION_DOWN事件落點坐标,判斷是否在内圓中
     *
     * @return 是否在内圓中
     */
    public boolean locationIsInner() {
        return (mSwitchState & LOCATION_MASK) == INNER_LOCATION;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureWith = MeasureSpec.getSize(widthMeasureSpec);
        int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMeasureMode != MeasureSpec.EXACTLY) {
            measureWith = mWidth + getPaddingLeft() + getPaddingRight();
        }
        if (heightMeasureMode != MeasureSpec.EXACTLY) {
            measureHeight = getPaddingTop() + getPaddingBottom() + mHeight;
        }
        setMeasuredDimension(measureWith, measureHeight);

    }

    /**
     * 回調狀态
     */
    public interface SwitchListener {
        void switchListener(boolean open);
    }

    public void setSwitchListener(SwitchListener switchListener) {
        mSwitchListener = switchListener;
    }

    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        SaveState saveState = new SaveState(parcelable);
        saveState.switchState = mSwitchState;
        return saveState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof SaveState) {
            SaveState saveState = (SaveState) state;
            super.onRestoreInstanceState(saveState.getSuperState());
            mSwitchState = saveState.switchState;
        } else {
            super.onRestoreInstanceState(state);
        }

    }

    static class SaveState extends BaseSavedState {

        //對應SwitchButton的mSwitchState屬性
        private int switchState;

        public SaveState(Parcel source) {
            super(source);
            switchState = source.readInt();
        }

        public SaveState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(switchState);
        }

        public static final Parcelable.Creator<SaveState> CREATOR = new Creator<SaveState>() {
            @Override
            public SaveState createFromParcel(Parcel source) {
                return new SaveState(source);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };

    }

}
           
本來還有些其他的東西要分享的,但是這篇寫的和代碼貼的實在太多了,不得不在此結束了,如果本篇文章有不對或者描述不恰當的地方希望大家指出,希望大家給個贊哦!