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

一、 分析View
如圖
-
由内圓和一個兩邊為半圓(以下都簡稱為外圓)的長方形背景;
1. 我們定義内圓半徑
為外圓半徑r
的R
倍:0.9
/** * 外圓半徑 */ private float outerCircleRadio; /** * 内圓半徑 */ private float innerCircleRadio;
- 為了更加美觀,定義
且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;
- 為了差別開關狀态,在每個狀态分别在對應的狀态給對應的内圓和背景不同的顔色來以區分;
- 定義
對應狀态内圓的顔色:SwitcButton
/** * 内圓未選中時的顔色 */ @ColorInt private int mInnerCircleOpenColor; /** * 内圓選中時的顔色 */ @ColorInt private int mInnerCircleCloseColor;
- 定義
對應狀态背景的顔色:SwitcButton
/** * 背景區域選中顔色 */ @ColorInt private int mBackgroundOpenColor; /** * 背景區域未選中顔色 */ @ColorInt private int mBackgroundCloseColor;
二、最簡單的實作
按照上面的分析我們先不管其他因素,先按照最簡單的來實作
- 建立類
如下: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); } }
- 定義上面需要的屬性:
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); } }
- 定義方法
來初始化我們View的一些屬性和工具:private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
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(); }
- 重寫方法
protected void onSizeChanged(int w, int h, int oldw, int oldh)
方法:
1)定義屬性
和mWidth
來存儲View的寬高,且在方法mHeight
中進行指派onSizeChanged
(當然你也可以省略這一步驟,後面通過mWidth=w、mHeight=h
來擷取View的寬高)getWidth和getHeight
2)根據上面分析,考慮到View可能設定padding值,是以我們在計算背景的高度時要減去對應padding值則/** * view 布局的寬高 */ private int mWidth, mHeight;
,而我們的外圓半徑為背景高度的一半且背景的寬是高的2.5倍,是以我們可以計算出來外圓的半徑為H=mHeight-getPaddingTop()-getPaddingBottom()
是以内圓的半徑為: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); }
- 重寫方法
protected void onDraw(Canvas canvas)
進行繪制:
1)首先繪制預設關閉狀态下背景,首先給畫筆設定未打開時的背景顔色,然後繪制背景
mBackgroundRectF
2)繪制預設關閉狀态下内圓,由于在繪制内圓時需要确定圓心的坐标,在此我們定義兩個變量分别存儲内圓圓心的坐标為mPaint.setColor(mBackgroundCloseColor); canvas.drawRoundRect(mBackgroundRectF, outerCircleRadio, outerCircleRadio, mPaint);
,在方法innerCircleOx, innerCircleOy
中初始化為onSizeChanged
,然後設定畫筆的顔色為關閉時内圓的顔色,然後繪制内圓為:innerCircleOx = outerCircleRadio+getPaddingLeft(), innerCircleOy = h >> 1;
mPaint.setColor(innerCircleColor); canvas.drawCircle(innerCircleOx, innerCircleOy, innerCircleRadio, mPaint);
- 我們最初的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>
2)我們給View添加padding值為50dp在運作如下:自定義View-SwitchButton 自定義View-SwitchButton
看來我們模型大緻完成了,接下來就是完成 swtich
功能
三、實作 Switch
功能
Switch
有兩種方式可以改變
SwtichButton
的狀态:
第一種:通過點選操作來切換更改SwitchButton的操作
第二種:通過滑動内圓來更改
SwtichButton
的狀态
是以我們要在處理ACTION_UP和ACTION_CANCEL事件中對目前一系列的事件進行判斷,判斷是否僅僅是單擊事件還是中間出現了對應的業務滑動,如果僅僅是單擊事件則進行狀态的轉換且更新内顔色位置和背景的顔色,若是滑動事件我們還要進行另一個判斷,判斷我們目前系列事件的down事件落點是否在目前狀态下内圓内,是我們則将更新滑動的進度且補全剩下狀态改變的過渡過程,
- 由上分析可知我們需要定義兩個标志位分别來标記目前一系列事件是否是含有滑動事件和目前一系列事件的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;
- 為了讓我們
狀态切換的更加優雅而不是一瞬間完成那麼生硬, 是以我們要建立屬性動畫SwitchButton
來美觀且完成mValueAnimator
中未完成的過渡過程,因為在涉及到滑動的系列事件中每次ACTION_UP和ACTION_CANCEL
的ACTION_UP和ACTION_CANCEL
坐标是不确定的,是以我們隻好在每次完成剩餘過渡過程中動态的設定起始和結束值,是以我們在X
方法中添加屬性動畫的最基本的且通用的初始化操作,然後建立方法init
l來動态的執行我們的過渡補全工作,而在配置過程中,由于動畫的起始和結束是不确定的,也就是執行的過程長度是不确定的,是以我們需要進行動态的計算,我們假設從臨界值到另一個臨界值事件為private void startSwitchAnimation(float animatorStartX, float animatorEndX)
,我們再由實際要執行的動畫長度比上我們兩個臨界值的內插補點絕對值在乘上我們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(); }
- 為了更直覺的可讀性,我們建立修改标志位的方法和一些讀取對應标志位的方法,并且将其裝換位對應
布爾值
:
1)建立修改标志位方法
:onlySetFlag(int flag, int mask)
2)建立從mSwitchState取出switch開關的狀态,判斷是否為打開狀态的方法/** * 設定目前的Flag * * @param flag 需要設定的值 * @param mask 對應标志位 */ private void onlySetFlag(int flag, int mask) { mSwitchState = (mSwitchState & ~mask) | (flag & mask); }
:public boolean stateIsOpen()
3)建立從mSwitchState取出event_mask位值,判斷目前是否為業務滑動事件的方法public boolean judgeIsMoveEvent()`:/** * 從mSwitchState取出switch開關的狀态,判斷是否為打開狀态 * * @return 否為打開狀态 */ public boolean stateIsOpen() { return (mSwitchState & STATE_MASK) == STATE_OPEN; }
4)建立從mSwitchState取出系列事件中/** * 從mSwitchState取出event_mask位值,判斷目前是否為業務滑動事件 * * @return 是否為業務滑動事件 */ public boolean judgeIsMoveEvent() { return (mSwitchState & EVENT_MASK) == MOVE_EVENT; }
事件落點坐标是否在内圓中的方法:ACTION_DOWN
/** * 從mSwitchState取出系列事件最開始事件落點坐标,判斷是否在内圓中 * * @return 是否在内圓中 */ public boolean locationIsInner() { return (mSwitchState & LOCATION_MASK) == INNER_LOCATION; }
- 在使用非點選事件來改變
狀态時即使用滑動,有可能出現我們的手指滑出背景外去,為了保證我們内圓在背景内,是以我們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; //省略 }
- 分析
ACTION_DOWN
事件
1)首先判斷我們是否要消費目前一系列事件,判斷依據是目前空間是否可用
、是否可點選enable
、是否在我們背景内clickable
且目前過渡補充動畫不在執行且沒開始mBackgroundRectF.contains(x, y)
即:!mValueAnimator.isRunning() && !mValueAnimator.isStarted()
2)判斷目前的落點是否在目前内圓範圍内,并更新标志位,計算方法為:用目前事件的坐标和内圓圓心的坐标進行距離求值,若小于等于外圓半徑則為園内,否則圓外:@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(); } }
,然後更新對應的标記位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(); } }
- 分析
ACTION_MOVE
事件
1)首先判斷目前的
事件對應标記位是否已經更新ACTION_MOVE
,若為更新則更新标記位!judgeIsMoveEvent()
onlySetFlag(MOVE_EVENT, EVENT_MASK);
2)按照業務邏輯,由于我們滑動内圓,我們應該更新對應内圓的位置,但是對應的若//取出對應标記值判斷目前是否處于滑動狀态,若不處于滑動狀态,則更新EVENT_MASK(事件标記位)為對應的值 if (!judgeIsMoveEvent()) { onlySetFlag(MOVE_EVENT, EVENT_MASK); }
落點不在我們當時内圓的範圍内,此時我們的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(); }
- 事件結束處理
和ACTION_UP
事件ACTION_MOVE
由于
1)
狀态的改變牽扯到兩種方式,是以我們要分類處理不同的方式,通過方法SwitchButton
取出對應的标志位判斷是否出現滑動事件來判斷是點選模式還是滑動模式judgeIsMoveEvent()
即滑動模式:首先把對應的事件标記位重置(為了避免後面忘記重置事件标記位)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
2)animatorStartX = innerCircleOx; animatorEndX = stateIsOpen ? innerCircleMaxRightX : innerCircleMaxLeftX; startSwitchAnimation(animatorStartX, animatorEndX);
即點選模式:這個相對好處理點,直接從對應臨界值到另一個臨界值的過程即:judgeIsMoveEvent()=false
運作看一下我們的效果://到此為點選事件,直接修改switchState值,且開始過度動畫從老值過渡到最新值 animatorStartX = innerCircleOx; animatorEndX = stateIsOpen() ? innerCircleMaxLeftX : innerCircleMaxRightX; onlySetFlag(stateIsOpen() ? STATE_CLOSE : STATE_OPEN, STATE_MASK); startSwitchAnimation(animatorStartX, animatorEndX);
從效果來看我們大緻實作我們的功能,但是不夠美觀我們之前的狀态顔色也沒有用上,是以我們需讓顔色對号入座自定義View-SwitchButton - 為了使我們的
更加炫酷,我們使内圓和背景的顔色随内圓運動在兩種狀态顔色中過渡呈現,我們要根據内圓圓心的SwitchButton
X軸坐标
在其移動範圍的進度值來動态計算對應的顔色。
1)首先我們在方法
中計算進度值:onDraw()
2)建立類float percent = (innerCircleOx - innerCircleMaxLeftX) / (innerCircleMaxRightX - innerCircleMaxLeftX);
用來在方法ArgbEvaluator mArgbEvaluator;
計算所處進度對應的顔色值:onDraw()
3)是以更新//目前background的color int backgroundColor = (int) mArgbEvaluator.evaluate(percent, mBackgroundCloseColor, mBackgroundOpenColor); //内圓目前的顔色 int innerCircleColor = (int) mArgbEvaluator.evaluate(percent, mInnerCircleCloseColor, mInnerCircleOpenColor);
方法代碼為: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); }
從效果上來我們對顔色的漸變已經達到了我們的效果,nice!自定義View-SwitchButton
四、測量View
因為自己每次自定義View時,都會非常糾結這部分,因為涉及到測量模式 wrap_content
,需要自己業務的需要和實際考慮來傳回View的測量寬高
- 未重寫方法
方法出現的問題,舉個例子我們給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
由圖中可知道我們View在實實在在的存在的,但是我們的what ?SwitcButton呢?
卻不見了,這是由于我們的實際繪制區域寬度變為了 ,我們在設定寬度為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
,也就是現在顯示的殘缺不全的效果,是以我們需要自己根據View的實際可繪制的寬高來配置我們SwitchButton背景
寬高,進行繪制,是以我們就需要重寫SwitchButton
方法。onMeasure
- 重寫
方法,關于onMeasure
onMeasure
的一些知識這裡就不在介紹了,需要了解的自行去搜尋
1)
模式即MeasureSpec.EXACTLY
match_parent或者是給定确切固定值
,這裡比較簡單,如果是這模式我們直接采用系統給我們側臉好的值傳回就行了,後面再根據實際的業務需要來計算我們對應的屬性(這裡沒什麼好說的)
2)
模式即MeasureSpec.AT_MOST
,這裡是我最糾結的部分了,因為在這個模式系統會給我們提供目前warp_content
能提供給的最大空間,我們可以選擇給自己設定的最小預設值來進行繪制,也可以根據parent
提供的最大空間結合業務邏輯來計算對應的屬性,這裡先采用給予設定的預設值來計算繪制我們的parent
3)SwitchButton
模式,這種方式未指定尺寸,這種模式用的特别的少,這裡也沒什麼好講的,直接舍去,下面就開始就行我們實際編碼MeasureSpec.UNSPECIFIED
方法onMeasure
- 根據我們的業務邏輯重寫
方法來測量我們的空間,進而進行onMeasure
SwitchButton
的繪制
1)設定
繪制區域的最小寬度,定義屬性SwitchButton
也就是最小寬度,根據最前面的邏輯(int defaultMinDimension = 50
)來計算SwitchButton的寬是高的2.5倍
的高度即SwitchButton
2)在defaultMinDimension/2.5f
方法中先将最小的預設值賦予我們存儲空間寬高的屬性用于onMeasure方法來進行測量init()
3)測量View的寬度:擷取寬度的測量模式mWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension, getResources().getDisplayMetrics()); mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, defaultMinDimension / 2.5f, getResources().getDisplayMetrics());
然後擷取系統在目前測量模式提供的測量參考值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
5)設定我們最終View的測量結果measureHeight = getPaddingTop() + getPaddingBottom() + mHeight;
6)setMeasuredDimension(measureWith, measureHeight)
代碼為: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); }
- 測量完
後并不是大功告成直接進行我們的View
和layout
工作,onDraw
并不是onMeasure
最終的寬高,它還牽扯到它的父控件View
為它布局時實際賦予的寬高也就是我們後面使用ViewGroup
中方法View
擷取的值,此值不一定等于我們的測量值,但是測量值它是getWidth()和getHeight()
測量和布局的一個參考,是以我們還要在得到實際控件的寬高時,對繪制需要的一些屬性就行初始化操作,這裡我們在方法ViewGroup
onSizeChanged
中進行,因為控件的寬高發生變化時會回調這個方法,屆時我們就可以再次重新初始化我們的繪制屬性了在這裡我們要注意的時上面示範過得問題的,就是有可能我們的繪制區域超過了我們的View範圍,是以在這裡我們要結合View的實際寬高來計算我們的繪制相關屬性。
1)确定我們的外圓半徑,我們先按照View的寬度來計算外圓半徑
,然後利用外圓半徑計算float radioByWidth = (w - getPaddingLeft() - getPaddingRight()) / 5f
繪制區域的高度加上SwitchButton
和paddingTop
,再将其和于View的高度進行比較,得到paddingBottom
型變量boolean
然後根據這個變量來重新判斷是否重新根據isResize = radioByWidth * 2 + getPaddingTop() + getPaddingBottom() > h;
的高度來計算外圓半徑,若View
則直接采用根據isResize=false
計算得來的外圓半徑,若View寬度
則需要根據isResize=true
的高度來重新計算外圓半徑即View
2)計算我們内圓半徑,這個相對要簡單,因為上面已經确定了外圓半徑了我們隻要取外圓半徑的outerCircleRadio = isResize ? (h - getPaddingTop() - getPaddingBottom()) / 2f : radioByWidth
倍的值即可:0.9
3)确定innerCircleRadio = outerCircleRadio *0.9
繪制區域,配置SwitchButton
,根據mBackgroundRectF
的值來動态計算,定義變量isResize
分别用來存儲mBackgroundRectLeft和mBackgroundRectRight
的mBackgroundRectF
,計算比較簡單也比較好了解,這裡不接講述直接貼出:left和right
4)配置我們的内圓移動範圍,這個也較簡單好了解,是以直接貼出://計算背景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);
5)根據目前``` //初始化innerCircleOx的範圍 innerCircleMaxLeftX = mBackgroundRectLeft + outerCircleRadio; innerCircleMaxRightX = mBackgroundRectRight - outerCircleRadio; ```
的狀态确定目前内圓的位置.SwitchButton
6)完成的//初始化内圓圓心坐标即内圓位置 innerCircleOx = stateIsOpen() ? innerCircleMaxRightX : innerCircleMaxLeftX; innerCircleOy = h >> 1;
方法: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;
}
再次運作之前測試的配置如下:
完美解決!
五、 SwitchButton
狀态的儲存和恢複
SwitchButton
在日常使用app過程中手機有時難免會橫放,緻使螢幕旋轉,而這時我們的View經曆銷毀然後重建,然後再回複之前的狀态,系統提供的TextView等控件隻要我們給其唯一的,系統都能正确恢複其銷毀前的狀态,那我們看看我們繼承View來碼的
id
在螢幕發生旋轉時系統會不會為其恢複,當然我們也會給其唯一的
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);
}
}
至此我們的屬性的儲存及恢複就完成了,以下為測試結果,由此可見我們我們恢複效果達到了:
六、給 SwitchButton
添加狀态監聽
SwitchButton
其實這個過程還是比較簡單的,直接開碼
- 定義接口
SwitchListener
/** * 回調狀态 */ public interface SwitchListener { void switchListener(boolean open); }
- 給
添加SwitchButton
相關配置操作SwitchListener
/** * switch button 狀态監聽 */ private SwitchListener mSwitchListener; public void setSwitchListener(SwitchListener switchListener) { mSwitchListener = switchListener; }
- 在合适的地方法進行回調我們的監聽函數,思考一下,無非就是判斷目前是否發生了
SwitchButton
狀态的變化來回調
1)先說發生狀态改變的情況,這時無非就是
和點選
來更改滑動
的狀态,是以我們隻需要在執行動畫的方法SwitchButton
中進行回調就行了,而在startSwitchAnimation(float animatorStartX, float animatorEndX, boolean flagIsChange)
方式更改狀态的情況下,滑動事件中的滑動
的值可能為臨界值,這時因為已經是目前要過渡到的效果了是以為了減少一些沒必要資源開銷,我們舍棄了過渡動畫隻要進行監聽回調即可如下:X
2)未發生狀态改變的情況,大家可能疑問了,未發生肯定就不調用啊,這個自然,但是我們的回調有一處是寫在執行動畫的方法裡的,因為目前可能未發生狀态的改變但是内圓的位置卻不在該狀态所在的位置,是以我們還要使用動畫來過渡,是以我們還要在執行動畫的方法裡還要判斷目前是否發生的了變化,是以在方法@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()); } }
中增加startSwitchAnimation(float animatorStartX, float animatorEndX, boolean flagIsChange)
型變量控制是否執行回調,為了更好判斷是否發生了變化,是以我們建立方法boolean
,在修改标志位的同時,判斷目前狀态是否發生了變化,是以以上代碼修改如下:boolean setFlagAndGetChangeValue(int flag, int mask)
3)當然我們還需要能夠在代碼中控制改變/** * 廢除了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()); } }
的狀态,是以我們要添加方法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
七、自定義屬性
其實到達這一步驟,基本上我們的就完成了,但是為了使我們的控件更加好看,能夠靈活配合業務UI設計,我們可以将那些固定的顔色、外圓半徑和内圓半徑比變為可以在
SwitchButton
中配置的自定義屬性,如下:
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];
}
};
}
}
本來還有些其他的東西要分享的,但是這篇寫的和代碼貼的實在太多了,不得不在此結束了,如果本篇文章有不對或者描述不恰當的地方希望大家指出,希望大家給個贊哦!