自定義View之SwitchView

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

image

這是一個性別選擇的控件 本質上是一個Switch類似的控件 需要滿足的需求點有:

  • 支持左右滑動選中
  • 支持左右點擊選中
  • 支持按鈕漸變色
  • 支持選中和未選中狀態(tài)字體顏色的變化

由此得出所涉及的自定義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);
        //這里是將寬高根據(jù)ui 設計圖計算寫死
        height = SizeUtils.dp2px(45);
        width = SizeUtils.dp2px(200);
        //圓角角度
        int radiis = SizeUtils.dp2px(80);
        //獲取系統(tǒng)識別最小的滑動距離
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        //獲取系統(tǒng)觸發(fā)點擊事件的時長
        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

其中這段代碼創(chuàng)建的是最底層圓角矩形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

創(chuàng)建用于滑動的選擇性別的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軸滑動距離小于系統(tǒng)所能識別的最小距離 切從手指按下到抬起時間 小于系統(tǒng)默認的點擊事件觸發(fā)的時間  整個行為將被視為觸發(fā)點擊事件
                if (Math.abs(deltaX) < mTouchSlop && Math.abs(deltaY) < mTouchSlop && time < mClickTimeout) {
                    //獲取事件觸發(fā)的x軸區(qū)域 主要用于區(qū)分是左邊還是右邊
                    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的繪制范圍 :

全局創(chuàng)建了一個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

剩下就是一些其他細節(jié)需求 最外層的文字 和標示圖片等 另外文字的繪制需要計算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 顏色漸變函數(shù)中 處理 這里不再貼代碼了:

   @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 希望可以幫助到更多的人

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末疯潭,一起剝皮案震驚了整個濱河市赊堪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌竖哩,老刑警劉巖哭廉,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異相叁,居然都是意外死亡遵绰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門钝荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來街立,“玉大人,你說我怎么就攤上這事埠通∈昀耄” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵端辱,是天一觀的道長梁剔。 經常有香客問我虽画,道長,這世上最難降的妖魔是什么荣病? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任码撰,我火速辦了婚禮,結果婚禮上个盆,老公的妹妹穿的比我還像新娘脖岛。我一直安慰自己,他們只是感情好颊亮,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布柴梆。 她就那樣靜靜地躺著,像睡著了一般终惑。 火紅的嫁衣襯著肌膚如雪绍在。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天雹有,我揣著相機與錄音偿渡,去河邊找鬼。 笑死霸奕,一個胖子當著我的面吹牛溜宽,可吹牛的內容都是我干的。 我是一名探鬼主播铅祸,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼坑质,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了临梗?” 一聲冷哼從身側響起涡扼,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盟庞,沒想到半個月后吃沪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡什猖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年票彪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片不狮。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡降铸,死狀恐怖,靈堂內的尸體忽然破棺而出摇零,到底是詐尸還是另有隱情推掸,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站谅畅,受9級特大地震影響登渣,放射性物質發(fā)生泄漏。R本人自食惡果不足惜毡泻,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一胜茧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仇味,春花似錦呻顽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至带到,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英染,已是汗流浹背揽惹。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留四康,地道東北人搪搏。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像闪金,于是被迫代替她去往敵國和親疯溺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容

  • 用兩張圖告訴你哎垦,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料囱嫩? 從這篇文章中你...
    hw1212閱讀 12,744評論 2 59
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,988評論 3 119
  • 作為現(xiàn)代開放式的公寓漏设,經衬校可以看到其簡約的一面,其實在這簡約的背后郑口,犧牲了很多戶型面積鸳碧,比如,原本兩房犬性,為了簡約做...
    我叫黑名單閱讀 296評論 0 0
  • 2018年7月25日 學習流水賬 早上瞻离,鬧鐘響了都沒聽見。六點半乒裆,同宿舍的艷艷把我喚醒套利。起床穿衣、洗漱打扮、餐廳吃...
    在水一方198158閱讀 271評論 1 1
  • 一日裙、什么是RunLoop 基本作用: 保持程序的持續(xù)運行吹艇; 處理App中的各種事件(比如觸摸事件、定時器事件昂拂、Se...
    magic_pill閱讀 913評論 0 0