自定義View之SwitchView
工作(我)太(太)忙(懶) 太長時間沒有寫部落格了,再不寫今年一晃就要過去了,順便也總結下今年工作的一些技術點吧。這篇先從一個簡單的自定義控制元件開始吧 先看最終效果圖:

image
這是一個性別選擇的控制元件 本質上是一個Switch類似的控制元件 需要滿足的需求點有:
- 支援左右滑動選中
- 支援左右點選選中
- 支援按鈕漸變色
- 支援選中和未選中狀態字型顏色的變化
由此得出所涉及的自定義View的技術點有:
- View的觸控事件和滑動事件的處理
- 顏色漸變的計算相關api的運用
接下就從最基本的程式碼開始:
//初始化 public class GenderSwitchView extends View { public GenderSwitchView(Context context) { this(context, null); } public GenderSwitchView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GenderSwitchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } }
初始邏輯
private ShapeDrawable backgroundDrawable; private ShapeDrawable genderDrawable; private float mProgress; private int mTouchSlop; private void initView(Context context) { int testSize = SizeUtils.sp2px(16); //這裡是將寬高根據ui 設計圖計算寫死 height = SizeUtils.dp2px(45); width = SizeUtils.dp2px(200); //圓角角度 int radiis = SizeUtils.dp2px(80); //獲取系統識別最小的滑動距離 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); //獲取系統觸發點選事件的時長 mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); selectTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); selectTextPaint.setTextAlign(Paint.Align.CENTER); selectTextPaint.setTextSize(testSize); selectTextPaint.setColor(Color.WHITE); defaultTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); defaultTextPaint.setTextAlign(Paint.Align.CENTER); defaultTextPaint.setTextSize(testSize); defaultTextPaint.setColor(grayText); mProgressAnimator = new ValueAnimator(); float[] outerRadii = {radiis, radiis, radiis, radiis, radiis, radiis, radiis, radiis};//外矩形 左上、右上、右下、左下的圓角半徑 RectF inset = new RectF(0, 0, 0, 0);//內矩形距外矩形,左上角x,y距離, 右下角x,y距離 float[] innerRadii = {0, 0, 0, 0, 0, 0, 0, 0};//內矩形 圓角半徑 RoundRectShape roundRectShape = new RoundRectShape(outerRadii, inset, innerRadii); backgroundDrawable = new ShapeDrawable(roundRectShape); int back_color = ContextCompat.getColor(context, R.color.col_f3f3f3); backgroundDrawable.getPaint().setColor(back_color); backgroundDrawable.setBounds(0, 0, width, height); girlStartColor = ContextCompat.getColor(context, R.color.col_ff719e); girlEndColor = ContextCompat.getColor(context, R.color.col_ffae9b); boyStartColor = ContextCompat.getColor(context, R.color.col_55a8ff); boyEndColor = ContextCompat.getColor(context, R.color.col_8998ff); //漸變色計算類 argbEvaluator = new ArgbEvaluator(); RoundRectShape shape = new RoundRectShape(outerRadii, inset, innerRadii); linearGradient = new LinearGradient(0, 0, boundsWidth, height, girlStartColor, girlEndColor, Shader.TileMode.REPEAT); genderDrawable = new ShapeDrawable(shape); genderDrawable.getPaint().setShader(linearGradient); genderDrawable.getPaint().setStyle(Paint.Style.FILL); boundsWidth = width / 2; bundsX = (int) (mProgress * boundsWidth); bounds = new Rect(bundsX, 0, boundsWidth + bundsX, height); genderDrawable.setBounds(bounds); }

image
其中這段程式碼建立的是最底層圓角矩形Drawable:
float[] outerRadii = {radiis, radiis, radiis, radiis, radiis, radiis, radiis, radiis};//外矩形 左上、右上、右下、左下的圓角半徑 RectF inset = new RectF(0, 0, 0, 0);//內矩形距外矩形,左上角x,y距離, 右下角x,y距離 float[] innerRadii = {0, 0, 0, 0, 0, 0, 0, 0};//內矩形 圓角半徑 RoundRectShape roundRectShape = new RoundRectShape(outerRadii, inset, innerRadii); backgroundDrawable = new ShapeDrawable(roundRectShape); int back_color = ContextCompat.getColor(context, R.color.col_f3f3f3); backgroundDrawab le.getPaint().setColor(back_color); backgroundDrawable.setBounds(0, 0, width, height);

image
建立用於滑動的選擇性別的Drawable,這個Drawable涉及漸變色 用到了 LinearGradient
相關api ofollow,noindex">Android之Shader用法詳細介紹
//女士Drawable 顏色範圍 girlStartColor = ContextCompat.getColor(context, R.color.col_ff719e); girlEndColor = ContextCompat.getColor(context, R.color.col_ffae9b); //男士Drawable 顏色範圍 boyStartColor = ContextCompat.getColor(context, R.color.col_55a8ff); boyEndColor = ContextCompat.getColor(context, R.color.col_8998ff); RoundRectShape shape = new RoundRectShape(outerRadii, inset, innerRadii); //顏色漸變 linearGradient = new LinearGradient(0, 0, boundsWidth, height, girlStartColor, girlEndColor, Shader.TileMode.REPEAT); genderDrawable = new ShapeDrawable(shape); //設定顏色漸變 genderDrawable.getPaint().setShader(linearGradient); genderDrawable.getPaint().setStyle(Paint.Style.FILL); //Drawable 寬高 為背景的一半 boundsWidth = width / 2; bundsX = (int) (mProgress * boundsWidth); bounds = new Rect(bundsX, 0, boundsWidth + bundsX, height); genderDrawable.setBounds(bounds);
然後呼叫 onDraw
進行繪製 看看效果:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //這裡因為是知道具體寬高 在初始化的時候已經計算出來 這裡直接設定進去即可 setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); backgroundDrawable.draw(canvas);//先繪製背景Drawable genderDrawable.draw(canvas);//再繪製上面一層用於可滑動的Drawable }
效果:

image
到這裡最基本的已經做完了 但是目前還不能滑動 所以要開始重寫 onTouchEvent
進行處理 這個也是這個自定義View 的重點 另外在滑動過程中擇性別的Drawable需要漸變顏色:
float mStartX; float mStartY; float mLastX; @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); float deltaX = event.getX() - mStartX; float deltaY = event.getY() - mStartY; switch (action) { case MotionEvent.ACTION_DOWN: mStartX = event.getX(); mStartY = event.getY(); mLastX = mStartX; setPressed(true); break; case MotionEvent.ACTION_MOVE: float x = event.getX(); //計算滑動的比例 boundsWidth為整個寬度的一半 setProcess(getProgress() + (x - mLastX) / boundsWidth); //這裡比較x軸方向的滑動 和y軸方向的滑動 如果y軸大於x軸方向的滑動 事件就不在往下傳遞 if ((Math.abs(deltaX) > mTouchSlop / 2 || Math.abs(deltaY) > mTouchSlop / 2)) { if (Math.abs(deltaY) > Math.abs(deltaX)) { return false; } } mLastX = x; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setPressed(false); //計算從手指觸控到手指抬起時的時間 float time = event.getEventTime() - event.getDownTime(); //如果x軸和y軸滑動距離小於系統所能識別的最小距離 切從手指按下到抬起時間 小於系統預設的點選事件觸發的時間整個行為將被視為觸發點選事件 if (Math.abs(deltaX) < mTouchSlop && Math.abs(deltaY) < mTouchSlop && time < mClickTimeout) { //獲取事件觸發的x軸區域 主要用於區分是左邊還是右邊 float clickX = event.getX(); //如果是在左邊 if (clickX > boundsWidth) { if (mProgress == 1.0f) { return false; } else { animateToState(true); } } else { if (mProgress == 0.0f) { return false; } else { animateToState(false); } } return false; } else { boolean nextStatus = getProgress() > 0.5f; animateToState(nextStatus); } break; } return true; }
通過滑動的距離來計算性別選著Drawable的繪製範圍 :
全域性建立了一個mProgress 用於計算性別選擇Drewable的繪製範圍 和顏色漸變的過程 當mProgress =1時 在右邊 mProgress=0時在左邊
public void setProcess(float progress) { LogUtils.e("setProcess(GenderSwitchView.java:141)進度" + progress); float tp = progress; if (tp > 1) { tp = 1; } else if (tp < 0) { tp = 0; } updatePaintStyle(tp); this.mProgress = tp; bundsX = (int) (mProgress * boundsWidth); bounds.left = bundsX; bounds.right = boundsWidth + bundsX; genderDrawable.setBounds(bounds); invalidate(); }
通過滑動距離來計算顏色的漸變 這裡用到顏色範圍計算的api ArgbEvaluator
:
private void updatePaintStyle(float tp) { intstartColor = (int) (argbEvaluator.evaluate(tp, girlStartColor, boyStartColor)); int endColor = (int) (argbEvaluator.evaluate(tp, girlEndColor, boyEndColor)); LinearGradient linearGradient = new LinearGradient(0, 0, boundsWidth, height, startColor, endColor, Shader.TileMode.REPEAT); //將計算好的 顏色範圍 重新設定到Drawable genderDrawable.getPaint().setShader(linearGradient); }
使用 ValueAnimator
來處理點選事件的動畫效果:
protected void animateToState(boolean checked) { float progress = mProgress; if (mProgressAnimator == null) { return; } if (mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); mProgressAnimator.removeAllUpdateListeners(); } mProgressAnimator.setDuration(mAnimationDuration); if (checked) { //右邊 mProgressAnimator.setFloatValues(progress, 1f); } else { //左邊 mProgressAnimator.setFloatValues(progress, 0.0f); } mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mProgress = (float) animation.getAnimatedValue(); //通過ValueAnimator 進度更新 Drawable 漸變色範圍 updatePaintStyle(mProgress); bundsX = (int) (mProgress * boundsWidth); bounds.left = bundsX; bounds.right = boundsWidth + bundsX; //更新性別選擇Drawable的繪製範圍 genderDrawable.setBounds(bounds); //繪製 postInvalidate(); } }); mProgressAnimator.start(); }
到這裡所有事件相關的工作都做完了 看看效果:

image
剩下就是一些其他細節需求 最外層的文字 和標示圖片等 另外文字的繪製需要計算 BaseLine
也就是繪製基準線:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //計算圖片繪製的 x,y drawBitmapX = SizeUtils.dp2px(22); int textMargin = SizeUtils.dp2px(5); drawBitmapY = (height - girlSign.getHeight()) / 2; String mText = "男士"; Rect bounds = new Rect(); //測量文字的寬度 selectTextPaint.getTextBounds(mText, 0, mText.length(), bounds); //獲取文字的高度 int textHeight = bounds.height(); //計算文字繪製的 x,y drawTextX = drawBitmapX + girlSign.getWidth() + textMargin + bounds.width() / 2; drawTextY = height / 2 + textHeight / 2; }
最後一同繪製 其中文字顏色的變化 和圖示的變化全都集中在更新性別選擇Drawable 顏色漸變函式中 處理 這裡不再貼程式碼了:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); backgroundDrawable.draw(canvas); genderDrawable.draw(canvas); canvas.drawBitmap(girlSign, drawBitmapX, drawBitmapY, bitmapPaint); canvas.drawBitmap(boySign, width / 2 + drawBitmapX, drawBitmapY, bitmapPaint); canvas.drawText("女士", drawTextX, drawTextY, selectTextPaint); canvas.drawText("男士", width / 2 + drawTextX, drawTextY, defaultTextPaint); }
最終效果:

image
總結:在所有的自定義SwitchView 基礎上都少不少觸控事件的處理 所以掌握觸控事件的處理情況下 剩下的各種花樣需求都萬變不離其宗 最後給上完整原始碼地址 SwitchView 希望可以幫助到更多的人