自定義View之SwitchView

明朗發表於2018-09-19

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

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 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) {
       int  startColor = (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 希望可以幫助到更多的人

相關文章