自定義View仿iOS的UiSwitch控件
本文原創(chuàng)原在,轉(zhuǎn)載請(qǐng)注明出處碰声。歡迎關(guān)注我的 簡(jiǎn)書(shū)播赁。
安利一波我寫(xiě)的開(kāi)發(fā)框架:MyScFrame喜歡的話就給個(gè)Star
前言:
Android的Switch控件相信大家都用過(guò)瞧柔,其實(shí)我覺(jué)得效果還好薪夕,不過(guò)公司要求UI上的統(tǒng)一桃纯,所以讓我仿iOS效果,我就納悶了酷誓,為什么一直要仿iOS,就不能iOS仿Android么态坦?牢騷發(fā)完了盐数,可以開(kāi)工了。
附上效果圖
思路
繪制控件
整個(gè)控件在繪制的時(shí)候分2個(gè)部分:
- 底板娘扩,也就是那個(gè)橢圓形類(lèi)似跑道的部分。
- 按鈕壮锻。
這部分沒(méi)什么需要特別說(shuō)明的琐旁,如果有一點(diǎn)自定義View基礎(chǔ)的朋友應(yīng)該都能輕松搞定。
動(dòng)效處理
動(dòng)效我這里用的也是最基礎(chǔ)的平移動(dòng)效猜绣,同時(shí)加上底板顏色漸變效果
/**
* 關(guān)閉開(kāi)關(guān)
*/
public void toggleOn() {
//手柄槽顏色漸變和手柄滑動(dòng)通過(guò)屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", 0, mOffSpotX);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
invalidate();
}
});
}
/**
* 打開(kāi)開(kāi)關(guān)
*/
public void toggleOff() {
//手柄槽顏色漸變和手柄滑動(dòng)通過(guò)屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", mOffSpotX, 0);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOpenSlotColor, mOffSlotColor);
invalidate();
}
});
}
/**
* 計(jì)算切換時(shí)的手柄槽的顏色
*
* @param fraction 動(dòng)畫(huà)播放進(jìn)度
* @param startColor 起始顏色
* @param endColor 終止顏色
*/
public void calculateColor(float fraction, int startColor, int endColor) {
final int fb = Color.blue(startColor);
final int fr = Color.red(startColor);
final int fg = Color.green(startColor);
final int tb = Color.blue(endColor);
final int tr = Color.red(endColor);
final int tg = Color.green(endColor);
//RGB三通道線性漸變
int sr = (int) (fr + fraction * (tr - fr));
int sg = (int) (fg + fraction * (tg - fg));
int sb = (int) (fb + fraction * (tb - fb));
//范圍限定
sb = clamp(sb, 0, 255);
sr = clamp(sr, 0, 255);
sg = clamp(sg, 0, 255);
mSlotColor = Color.rgb(sr, sg, sb);
}
Touch事件與Onclick事件
這里我是自己做了一個(gè)手勢(shì)工具類(lèi)灰殴,專(zhuān)門(mén)處理Touch中的手勢(shì),沒(méi)任何難度掰邢,只是懶得每個(gè)自定義控件里面都寫(xiě)一套重復(fù)的代碼牺陶,所以就做了個(gè)工具類(lèi),方便今后的開(kāi)發(fā)
這里也分享給大家(不要吐槽我的類(lèi)名辣之,我是百度翻譯的)
/**
* Created by caihan on 2017/2/10.
* 手勢(shì)判斷工具類(lèi),
*/
public class GestureUtils {
private static final String TAG = "GestureUtils";
private float startX = 0f;
private float endX = 0f;
private float startY = 0f;
private float endY = 0f;
private float xDistance = 0f;
private float yDistance = 0f;
public enum Gesture {
PullUp, PullDown, PullLeft, PullRight
}
public GestureUtils() {
}
/**
* 當(dāng)event.getAction() == MotionEvent.ACTION_DOWN 的時(shí)候調(diào)用
* 設(shè)置初始X,Y坐標(biāo)
*
* @param event
*/
public void actionDown(MotionEvent event) {
xDistance = yDistance = 0f;
setStartX(event);
setStartY(event);
}
/**
* 當(dāng)event.getAction() == MotionEvent.ACTION_MOVE 的時(shí)候調(diào)用
* 設(shè)置移動(dòng)的X,Y坐標(biāo)
*
* @param event
*/
public void actionMove(MotionEvent event) {
setEndX(event);
setEndY(event);
}
/**
* 當(dāng)event.getAction() == MotionEvent.ACTION_UP 的時(shí)候調(diào)用
* 設(shè)置截止的X,Y坐標(biāo)
*
* @param event
*/
public void actionUp(MotionEvent event) {
setEndX(event);
setEndY(event);
}
/**
* 手勢(shì)判斷接口
*
* @param gesture
* @return
*/
public boolean getGesture(Gesture gesture) {
switch (gesture) {
case PullUp:
return isRealPullUp();
case PullDown:
return isRealPullDown();
case PullLeft:
return isRealPullLeft();
case PullRight:
return isRealPullRight();
default:
LogUtils.e(TAG, "getGesture error");
return false;
}
}
/**
* 獲取Touch點(diǎn)相對(duì)于屏幕原點(diǎn)的X坐標(biāo)
*
* @param event
* @return
*/
private float gestureRawX(MotionEvent event) {
return event.getRawX();
}
/**
* 獲取Touch點(diǎn)相對(duì)于屏幕原點(diǎn)的Y坐標(biāo)
*
* @param event
* @return
*/
private float gestureRawY(MotionEvent event) {
return event.getRawY();
}
/**
* 獲取X軸偏移量,取絕對(duì)值
*
* @param startX
* @param endX
* @return
*/
private float gestureDistanceX(float startX, float endX) {
setxDistance(Math.abs(endX - startX));
return xDistance;
}
/**
* 獲取Y軸偏移量,取絕對(duì)值
*
* @param startY
* @param endY
* @return
*/
private float gestureDistanceY(float startY, float endY) {
setyDistance(Math.abs(endY - startY));
return yDistance;
}
/**
* endY坐標(biāo)比startY小,相減負(fù)數(shù)表示手勢(shì)上滑
*
* @param startY
* @param endY
* @return
*/
private boolean isPullUp(float startY, float endY) {
return (endY - startY) < 0;
}
/**
* endY坐標(biāo)比startY大,相減正數(shù)表示手勢(shì)下滑
*
* @param startY
* @param endY
* @return
*/
private boolean isPullDown(float startY, float endY) {
return (endY - startY) > 0;
}
/**
* endX坐標(biāo)比startX大,相減正數(shù)表示手勢(shì)右滑
*
* @param startX
* @param endX
* @return
*/
private boolean isPullRight(float startX, float endX) {
return (endX - startX) > 0;
}
/**
* endX坐標(biāo)比startX小,相減負(fù)數(shù)表示手勢(shì)左滑
*
* @param startX
* @param endX
* @return
*/
private boolean isPullLeft(float startX, float endX) {
return (endX - startX) < 0;
}
/**
* 判斷用戶真實(shí)操作是否是上滑
*
* @return
*/
private boolean isRealPullUp() {
if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
//Y軸偏移量大于X軸,表示用戶真實(shí)目的是上下滑動(dòng)
return isPullUp(startY, endY);
}
return false;
}
/**
* 判斷用戶真實(shí)操作是否是下滑
*
* @return
*/
private boolean isRealPullDown() {
if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
//Y軸偏移量大于X軸,表示用戶真實(shí)目的是上下滑動(dòng)
return isPullDown(startY, endY);
}
return false;
}
/**
* 判斷用戶真實(shí)操作是否是左滑
*
* @return
*/
private boolean isRealPullLeft() {
if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
//Y軸偏移量大于X軸,表示用戶真實(shí)目的是上下滑動(dòng)
return isPullLeft(startX, endX);
}
return false;
}
/**
* 判斷用戶真實(shí)操作是否是左滑
*
* @return
*/
private boolean isRealPullRight() {
if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
//Y軸偏移量大于X軸,表示用戶真實(shí)目的是上下滑動(dòng)
return isPullRight(startX, endX);
}
return false;
}
private GestureUtils setStartX(MotionEvent event) {
this.startX = gestureRawX(event);
return this;
}
private GestureUtils setEndX(MotionEvent event) {
this.endX = gestureRawX(event);
return this;
}
private GestureUtils setStartY(MotionEvent event) {
this.startY = gestureRawY(event);
return this;
}
private GestureUtils setEndY(MotionEvent event) {
this.endY = gestureRawY(event);
return this;
}
private GestureUtils setxDistance(float xDistance) {
this.xDistance = xDistance;
return this;
}
private GestureUtils setyDistance(float yDistance) {
this.yDistance = yDistance;
return this;
}
public float getStartX() {
return startX;
}
public float getEndX() {
return endX;
}
public float getStartY() {
return startY;
}
public float getEndY() {
return endY;
}
public float getxDistance() {
return xDistance;
}
public float getyDistance() {
return yDistance;
}
}
大家都知道掰伸,Onclick事件是要在Touch的MotionEvent.ACTION_UP
事件之后才觸發(fā)的,也就是說(shuō)怀估,如果我們dispatchTouchEvent
分發(fā)Touch事件的時(shí)候狮鸭,當(dāng)event.getAction() = MotionEvent.ACTION_UP
時(shí)合搅,返回true歧蕉,那么Onclick就不會(huì)觸發(fā)惯退,這樣的話催跪,我們就能針對(duì)不同的事件做不同的處理懊蒸,我這邊就是這樣設(shè)計(jì)的
private boolean mIsToggleOn = false;//當(dāng)前開(kāi)關(guān)標(biāo)記
private boolean isTouchEvent = false;//是否由滑動(dòng)事件消費(fèi)掉
private boolean isMoveing = false;//是否還在Touch相應(yīng)中
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mGestureUtils.actionDown(event);
isTouchEvent = false;
isMoveing = false;
break;
case MotionEvent.ACTION_MOVE:
mGestureUtils.actionMove(event);
if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
//左滑,關(guān)閉
isTouchEvent = true;
touchToggle(false);
return true;
} else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
//右滑,開(kāi)啟
isTouchEvent = true;
touchToggle(true);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
isMoveing = false;
if (isTouchEvent) {
//不會(huì)觸發(fā)Onclick事件了
return true;
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
/**
* Touch事件觸發(fā)
* mIsToggleOn是當(dāng)前狀態(tài),當(dāng)mIsToggleOn != open時(shí)做出相應(yīng)
*
* @param open 是否打開(kāi)
*/
private void touchToggle(boolean open) {
if (!isMoveing) {
isMoveing = true;
if (mIsToggleOn != open) {
if (mIsToggleOn) {
toggleOff();
} else {
toggleOn();
}
mIsToggleOn = !mIsToggleOn;
if (mOnToggleListener != null) {
mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
}
}
}
}
/**
* Onclick事件觸發(fā)
*/
private void onClickToggle() {
if (mIsToggleOn) {
toggleOff();
} else {
toggleOn();
}
mIsToggleOn = !mIsToggleOn;
if (mOnToggleListener != null) {
mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
}
}
然后監(jiān)聽(tīng)按鈕狀態(tài)
public interface OnToggleListener {
void onSwitchChangeListener(boolean switchState);
}
public void setOnToggleListener(OnToggleListener listener) {
mOnToggleListener = listener;
}
完了完了,就這么簡(jiǎn)單...什么?要完整代碼?好吧
/**
* Created by caihan on 2017/2/10.
* 仿iOS的UiSwitch控件
*/
public class IosSwitch extends View implements View.OnClickListener {
private static final String TAG = "IosSwitch";
private final int BORDER_WIDTH = 2;//邊框?qū)挾?
private int mBasePlaneColor = Color.parseColor("#4ebb7f");//底盤(pán)顏色,布局描邊顏色
private int mOpenSlotColor = Color.parseColor("#4ebb7f");//開(kāi)啟時(shí)手柄滑動(dòng)槽的顏色
private int mOffSlotColor = Color.parseColor("#EEEEEE");//關(guān)閉時(shí)手柄滑動(dòng)槽的顏色
private int mSlotColor;
private RectF mRect = new RectF();
//繪制參數(shù)
private float mBackPlaneRadius;//底板的圓形半徑
private float mSpotRadius;//手柄半徑
private float spotStartX;//手柄的起始X位置,切換時(shí)平移改變它
private float mSpotY;//手柄的起始X位置抡笼,不變
private float mOffSpotX;//關(guān)閉時(shí),手柄的水平位置
private Paint mPaint;//畫(huà)筆
private boolean mIsToggleOn = false;//當(dāng)前開(kāi)關(guān)標(biāo)記
private boolean isTouchEvent = false;//是否由滑動(dòng)事件消費(fèi)掉
private boolean isMoveing = false;//是否還在Touch相應(yīng)中
private OnToggleListener mOnToggleListener;//toggle事件監(jiān)聽(tīng)
private GestureUtils mGestureUtils;//手勢(shì)工具類(lèi)
public interface OnToggleListener {
void onSwitchChangeListener(boolean switchState);
}
public IosSwitch(Context context) {
super(context);
init(context);
}
public IosSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setOnClickListener(this);
setEnabled(true);
mGestureUtils = new GestureUtils();
}
@Override
public void onClick(View v) {
onClickToggle();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mGestureUtils.actionDown(event);
isTouchEvent = false;
isMoveing = false;
break;
case MotionEvent.ACTION_MOVE:
mGestureUtils.actionMove(event);
if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
//左滑,關(guān)閉
isTouchEvent = true;
touchToggle(false);
return true;
} else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
//右滑,開(kāi)啟
isTouchEvent = true;
touchToggle(true);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
isMoveing = false;
if (isTouchEvent) {
//不會(huì)觸發(fā)Onclick事件了
return true;
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSize;
int resultHeight = hSize;
Resources r = Resources.getSystem();
//lp = wrapcontent時(shí) 指定默認(rèn)值
if (wMode == MeasureSpec.AT_MOST) {
resultWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
}
if (hMode == MeasureSpec.AT_MOST) {
resultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
}
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mBackPlaneRadius = Math.min(getWidth(), getHeight()) * 0.5f;
mSpotRadius = mBackPlaneRadius - BORDER_WIDTH;
spotStartX = 0;
mSpotY = 0;
mOffSpotX = getMeasuredWidth() - mBackPlaneRadius * 2;
mSlotColor = mOffSlotColor;
}
@Override
protected void onDraw(Canvas canvas) {
//畫(huà)底板
mRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
//畫(huà)手柄的槽
mRect.set(BORDER_WIDTH,
BORDER_WIDTH,
getMeasuredWidth() - BORDER_WIDTH,
getMeasuredHeight() - BORDER_WIDTH);
mPaint.setColor(mSlotColor);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
//手柄包括包括兩部分,深色底板和白板,這樣做的目的是使圓盤(pán)具有邊框
//手柄的底盤(pán)
mRect.set(spotStartX,
mSpotY,
spotStartX + mBackPlaneRadius * 2,
mSpotY + mBackPlaneRadius * 2);
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
//手柄的圓板
mRect.set(spotStartX + BORDER_WIDTH,
mSpotY + BORDER_WIDTH,
mSpotRadius * 2 + spotStartX + BORDER_WIDTH,
mSpotRadius * 2 + mSpotY + BORDER_WIDTH);
mPaint.setColor(Color.WHITE);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
}
public float getSpotStartX() {
return spotStartX;
}
public void setSpotStartX(float spotStartX) {
this.spotStartX = spotStartX;
}
/**
* 計(jì)算切換時(shí)的手柄槽的顏色
*
* @param fraction 動(dòng)畫(huà)播放進(jìn)度
* @param startColor 起始顏色
* @param endColor 終止顏色
*/
public void calculateColor(float fraction, int startColor, int endColor) {
final int fb = Color.blue(startColor);
final int fr = Color.red(startColor);
final int fg = Color.green(startColor);
final int tb = Color.blue(endColor);
final int tr = Color.red(endColor);
final int tg = Color.green(endColor);
//RGB三通道線性漸變
int sr = (int) (fr + fraction * (tr - fr));
int sg = (int) (fg + fraction * (tg - fg));
int sb = (int) (fb + fraction * (tb - fb));
//范圍限定
sb = clamp(sb, 0, 255);
sr = clamp(sr, 0, 255);
sg = clamp(sg, 0, 255);
mSlotColor = Color.rgb(sr, sg, sb);
}
private int clamp(int value, int low, int high) {
return Math.min(Math.max(value, low), high);
}
/**
* 關(guān)閉開(kāi)關(guān)
*/
public void toggleOn() {
//手柄槽顏色漸變和手柄滑動(dòng)通過(guò)屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", 0, mOffSpotX);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
invalidate();
}
});
}
/**
* 打開(kāi)開(kāi)關(guān)
*/
public void toggleOff() {
//手柄槽顏色漸變和手柄滑動(dòng)通過(guò)屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "spotStartX", mOffSpotX, 0);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOpenSlotColor, mOffSlotColor);
invalidate();
}
});
}
public boolean getSwitchState() {
return mIsToggleOn;
}
/**
* Touch事件觸發(fā)
* mIsToggleOn是當(dāng)前狀態(tài),當(dāng)mIsToggleOn != open時(shí)做出相應(yīng)
*
* @param open 是否打開(kāi)
*/
private void touchToggle(boolean open) {
if (!isMoveing) {
isMoveing = true;
if (mIsToggleOn != open) {
if (mIsToggleOn) {
toggleOff();
} else {
toggleOn();
}
mIsToggleOn = !mIsToggleOn;
if (mOnToggleListener != null) {
mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
}
}
}
}
/**
* Onclick事件觸發(fā)
*/
private void onClickToggle() {
if (mIsToggleOn) {
toggleOff();
} else {
toggleOn();
}
mIsToggleOn = !mIsToggleOn;
if (mOnToggleListener != null) {
mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
}
}
public void setOnToggleListener(OnToggleListener listener) {
mOnToggleListener = listener;
}
/**
* 界面上設(shè)置開(kāi)關(guān)初始狀態(tài)
* @param open
*/
public void setChecked(final boolean open) {
this.postDelayed(new Runnable() {
@Override
public void run() {
touchToggle(open);
}
}, 300);
}
}
感謝
感謝kakacxicm提供的繪制思路與動(dòng)效處理
這邊再分享幾個(gè)其他的版本給大家參考:
SwitchButton
自定義控件(三步搞定switch)
Swift-自定義switch控件
搞定,收工
歡迎大家留言指出我的不足厂捞。