自定義seekbar詳解

自定義view之seekbar

本文簡介:在github上找了很多seekbar,有些庫具有至關複雜的功能,因此我想本身寫一個簡單易用的seekbar。本文主要講述爲何要自定義view,自定義view的大致步驟,編寫重難點。git

一、爲何要自定義view

因爲工做上的須要,咱們每每須要實現某種特殊的佈局或者界面效果,這時候官方沒有提供相應的控件支持,須要咱們繼承view或者其它view類擴展。通常初學者入門能夠先嚐試組合view,即先本身利用多個官方控件拼裝成須要的效果,而後內置邏輯(參考本人的數量加減view)。也就是把abc等多個view組合在一塊兒使用,比include方式多了內置邏輯的好處。(具體範例參考本人其它博客)github

接下來本文講述的是如何自定義一個seekbar。先看效果圖,以下。canvas

這裏寫圖片描述

二、分析要繪製的自定義view

1)根據最終效果圖或者需求方提供的功能說明等,去分析界面效果包含哪些動做,好比手勢(點擊,觸摸移動),要顯示的圖形形狀、文本(矩形,原型,弧形,隨圖形一塊兒繪製的文本等等,都要仔細分析),拆解view圖形爲小的模塊。bash

2)好比本文的seekbar,明顯分爲3個部分,一個是後面刻度的進度條,一個是當前的進度條。還有一個圓形按鈕。而後手指點擊刻度條,會根據點擊位置當前進度跳轉至此,而且圓形按鈕也是如此。有一個特殊的需求是能夠圓角也能夠無圓角,而且圓形按鈕無關緊要。因此須要2個標記boolean去區分。須要注意的一點是,按照習慣通常圓形按鈕的圓心的x所在座標應該是在白色的當前進度的最右邊x座標。ide

3)根據圖片,咱們能夠得出,3個模塊的繪製都是本身有自身的大小控制,而爲了適配左右padding,因此的繪製進度條時,要預留padding。 而上下padding,我不許備處理,直接讓seekbar繪製在縱向的中間便可。即縱座標y中心點都是height/2,而且限制3個模塊的最大高度爲view的高度,避免繪製出界。函數

三、自定義view主要方法介紹

主要方法有onmeasure、ondraw、ontouchevent、構造函數。自定義view通常圍繞這幾個方法進行處理,構造函數裏獲取自定義屬性的值,初始化paint等對象,初始化一些view參數。ondraw進行繪製圖形,這個主要有drawarc等方法,這個很少講,自行搜索相關方法總覽。ontouchevent就是處理點擊座標,而後觸發一些繪製操做或響應某個方法動做。對於viewgroup的話還有onlayout等方法。佈局

四、開始繪製

先準備本view須要的自定義屬性,3個模塊的高度大小、是否圓角、顏色等。tickBar是刻度條,circlebutton是圓形按鈕,progress就是當前進度,代碼以下。測試

<!--自定義 seekbar-->
    <declare-styleable name="NumTipSeekBar">
        <attr name="tickBarHeight" format="dimension"/>
        <attr name="tickBarColor" format="color"/>
        <attr name="circleButtonColor" format="color"/>
        <attr name="circleButtonTextColor" format="color"/>
        <attr name="circleButtonTextSize" format="dimension"/>
        <attr name="circleButtonRadius" format="dimension"/>
        <attr name="progressHeight" format="dimension"/>
        <attr name="progressColor" format="color"/>
        <attr name="selectProgress" format="integer"/>
        <attr name="startProgress" format="integer"/>
        <attr name="maxProgress" format="integer"/>
        <attr name="isShowButtonText" format="boolean"/>
        <attr name="isShowButton" format="boolean"/>
        <attr name="isRound" format="boolean"/>
    </declare-styleable>
複製代碼

接下來就是獲取自定義屬性,而後初始化view參數了。TypedArray對象必定要記得attr.recycle();關閉,通常textsize是getDimension,而高度大小什麼的是獲取getDimensionPixelOffset,view自己測試出來的也是px值,可是settextsize的方法須要傳入dp或者sp值。我在initview方法裏初始化所須要的paint對象,避免ondraw反覆繪製裏new對象耗費沒必要要的內存。可能初學者不清楚RectF是什麼東西,你百度一下會死啊。。。代碼以下。動畫

public NumTipSeekBar(Context context) {
        this(context, null);
    }

    public NumTipSeekBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
 public NumTipSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }


    /**
     * 初始化view的屬性
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {

        TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.NumTipSeekBar);
        mTickBarHeight = attr.getDimensionPixelOffset(R.styleable
                .NumTipSeekBar_tickBarHeight, getDpValue(8));
        mTickBarColor = attr.getColor(R.styleable.NumTipSeekBar_tickBarColor, getResources()
                .getColor(R.color.orange_f6));
        mCircleButtonColor = attr.getColor(R.styleable.NumTipSeekBar_circleButtonColor,
                getResources().getColor(R.color.white));
        mCircleButtonTextColor = attr.getColor(R.styleable.NumTipSeekBar_circleButtonTextColor,
                getResources().getColor(R.color.purple_82));
        mCircleButtonTextSize = attr.getDimension(R.styleable
                .NumTipSeekBar_circleButtonTextSize, getDpValue(16));
        mCircleButtonRadius = attr.getDimensionPixelOffset(R.styleable
                .NumTipSeekBar_circleButtonRadius, getDpValue(16));
        mProgressHeight = attr.getDimensionPixelOffset(R.styleable
                .NumTipSeekBar_progressHeight, getDpValue(20));
        mProgressColor = attr.getColor(R.styleable.NumTipSeekBar_progressColor,
                getResources().getColor(R.color.white));
        mSelectProgress = attr.getInt(R.styleable.NumTipSeekBar_selectProgress, 0);
        mStartProgress = attr.getInt(R.styleable.NumTipSeekBar_startProgress, 0);
        mMaxProgress = attr.getInt(R.styleable.NumTipSeekBar_maxProgress, 10);
        mIsShowButtonText = attr.getBoolean(R.styleable.NumTipSeekBar_isShowButtonText, true);
        mIsShowButton = attr.getBoolean(R.styleable.NumTipSeekBar_isShowButton, true);
        mIsRound = attr.getBoolean(R.styleable.NumTipSeekBar_isRound, true);
        initView();

        attr.recycle();


    }
     private void initView() {
        mProgressPaint = new Paint();
        mProgressPaint.setColor(mProgressColor);
        mProgressPaint.setStyle(Paint.Style.FILL);
        mProgressPaint.setAntiAlias(true);

        mCircleButtonPaint = new Paint();
        mCircleButtonPaint.setColor(mCircleButtonColor);
        mCircleButtonPaint.setStyle(Paint.Style.FILL);
        mCircleButtonPaint.setAntiAlias(true);

        mCircleButtonTextPaint = new Paint();
        mCircleButtonTextPaint.setTextAlign(Paint.Align.CENTER);
        mCircleButtonTextPaint.setColor(mCircleButtonTextColor);
        mCircleButtonTextPaint.setStyle(Paint.Style.FILL);
        mCircleButtonTextPaint.setTextSize(mCircleButtonTextSize);
        mCircleButtonTextPaint.setAntiAlias(true);

        mTickBarPaint = new Paint();
        mTickBarPaint.setColor(mTickBarColor);
        mTickBarPaint.setStyle(Paint.Style.FILL);
        mTickBarPaint.setAntiAlias(true);

        mTickBarRecf = new RectF();//矩形,一會根據這個繪製刻度條在這個矩形內
        mProgressRecf = new RectF();
        mCircleRecf = new RectF();
    }
複製代碼

因爲本view沒有太大必要編寫onmeasure方法去適配wrapcontent。因此接下來就是ondraw裏進行繪製了。首先咱們先繪製刻度條,首先獲取當前view的高寬,刻度條設置的高寬,而後計算y座標中心,計算出剛纔RectF矩形範圍。要設置上下左右的座標起點,左就是getPaddingLeft()做爲起點,即默認自定義view支持paddingleft的設置。top的起點就是(mViewHeight - mTickBarHeight) / 2,即含義是繪製在view縱座標y的中心點,而後tickbar高度今後點分爲上下2半。同理求出橫向的終點的x座標以及底部座標等ui

@Overrid
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        initValues(width, height);
        // do........
    }

  private void initValues(int width, int height) {
        mViewWidth = width - getPaddingRight() - getPaddingLeft();
        mViewHeight = height;
        if (mTickBarHeight > mViewHeight) {
            //若是刻度條的高度大於view自己的高度的1/2,則顯示不完整,因此處理下。
            mTickBarHeight = mViewHeight;
        }
        mTickBarRecf.set(getPaddingLeft(), (mViewHeight - mTickBarHeight) / 2,
                mViewWidth + getPaddingLeft(), mTickBarHeight / 2 +
                        mViewHeight / 2);


      
複製代碼

同理處理進度條部分的繪製,這個比剛纔多了一層邏輯,起點依舊,可是終點x(矩形的right座標)須要根據當前進度計算。mSelectProgress 是當前進度值,mMaxProgress 是最大值,mStartProgress是默認起點表明多少刻度值,好比1-10的seekbar效果(起點是1,終點是10)。求出比值而後乘以view自己的實際繪製範圍的寬度(上面代碼有計算),加上paddingleft,得出矩形的終點x。

mCirclePotionX = (float) (mSelectProgress - mStartProgress) /
                (mMaxProgress - mStartProgress) * mViewWidth + getPaddingLeft();
  if (mProgressHeight > mViewHeight) {
            //若是刻度條的高度大於view自己的高度的1/2,則顯示不完整,因此處理下。
            mProgressHeight = mViewHeight;
        }

        mProgressRecf.set(getPaddingLeft(), (mViewHeight - mProgressHeight) / 2,
                mCirclePotionX, mProgressHeight / 2 + mViewHeight / 2);
複製代碼

同理求出圓形按鈕的座標範圍

if (mCircleButtonRadius > mViewHeight / 2) {
            //若是圓形按鈕的半徑大於view自己的高度的1/2,則顯示不完整,因此處理下。
            mCircleButtonRadius = mViewHeight / 2;
        }
        mCircleRecf.set(mCirclePotionX - mCircleButtonRadius, mViewHeight / 2 -
                        mCircleButtonRadius / 2,
                mCirclePotionX + mCircleButtonRadius, mViewHeight / 2 +
                        mCircleButtonRadius / 2);
複製代碼

開始繪製,mIsRound控制圓角。重點說明的是 Paint.FontMetricsInt處理文本的居中顯示。 代碼以下。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        initValues(width, height);
       
        if (mIsRound) {
            canvas.drawRoundRect(mTickBarRecf, mProgressHeight / 2, mProgressHeight / 2,
                    mTickBarPaint);
            canvas.drawRoundRect(mProgressRecf, mProgressHeight / 2, mProgressHeight / 2,
                    mProgressPaint);
        } else {
            canvas.drawRect(mTickBarRecf, mTickBarPaint);
            canvas.drawRect(mProgressRecf, mProgressPaint);
        }
//        canvas.drawArc(mCircleRecf, 0, 360, true, mCircleButtonPaint);
        if (mIsShowButton) {
            canvas.drawCircle(mCirclePotionX, mViewHeight / 2, mCircleButtonRadius,
                    mCircleButtonPaint);
        }
        if (mIsShowButtonText) {
            Paint.FontMetricsInt fontMetrics = mCircleButtonTextPaint.getFontMetricsInt();
            int baseline = (int) ((mCircleRecf.bottom + mCircleRecf.top - fontMetrics.bottom -
                    fontMetrics
                            .top) / 2);
            // 下面這行是實現水平居中,drawText對應改成傳入targetRect.centerX()
            canvas.drawText(String.valueOf(mSelectProgress), mCircleRecf.centerX
                            (), baseline,
                    mCircleButtonTextPaint);

        }
    }
複製代碼

五、處理觸摸邏輯

這裏主要是依賴onTouchEvent判斷手勢,當event知足某個觸摸條件就進行獲取當前座標計算進度。本view是ACTION_MOVE、ACTION_DOWN時觸發。isEnabled判斷是否設置setEnabled屬性,若是設置則屏蔽觸摸繪製,這是個人特殊需求。judgePosition()主要是根據x座標進行計算進度。BigDecimal 是處理四捨五入,大概發生進度變化時從新繪製自身view。return true;是爲了消費觸摸事件。(觸摸事件分發機制,請移步大牛的博客)

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            //若是設置不可用,則禁用觸摸設置進度
            return false;
        }
        float x = event.getX();
        float y = event.getY();
//        Log.i(TAG, "onTouchEvent: x:" + x);
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                judgePosition(x);
                return true;
            case MotionEvent.ACTION_DOWN:
                judgePosition(x);
                return true;
            case MotionEvent.ACTION_UP:
                if (mOnProgressChangeListener != null) {
                    Log.i(TAG, "onTouchEvent: 觸摸結束,通知監聽器-mSelectProgress:"+mSelectProgress);
                    mOnProgressChangeListener.onChange(mSelectProgress);
                }
                return true;
            default:
                break;
        }

        return super.onTouchEvent(event);
    }

    private void judgePosition(float x) {
        float end = getPaddingLeft() + mViewWidth;
        float start = getPaddingLeft();
        int progress = mSelectProgress;
//        Log.i(TAG, "judgePosition: x-start:" + (x - start));
//        Log.i(TAG, "judgePosition: start:" + start + " end:" + end + " mMaxProgress:" +
//                mMaxProgress);
        if (x >= start) {
            double result = (x - start) / mViewWidth * (float) mMaxProgress;
            BigDecimal bigDecimal = new BigDecimal(result).setScale(0, BigDecimal.ROUND_HALF_UP);
//            Log.i(TAG, "judgePosition: progress:" + bigDecimal.intValue() + " result:" + result
//                    + " (x - start) / end :" + (x - start) / end);
            progress = bigDecimal.intValue();
            if (progress > mMaxProgress) {
//                Log.i(TAG, "judgePosition:x > end 超出座標範圍:");
                progress = mMaxProgress;
            }
        } else if (x < start) {
//            Log.i(TAG, "judgePosition: x < start 超出座標範圍:");
            progress = 0;
        }
         if (progress != mSelectProgress) {
            //發生變化才通知view從新繪製
            setSelectProgress(progress, false);
        }

    }
複製代碼

下面是一些主要的set方法,用來更新view。

/**
     * 設置當前選中的值
     *
     * @param selectProgress 進度
     */
    public void setSelectProgress(int selectProgress) {
        this.setSelectProgress(selectProgress, true);
    }

    /**
     * 設置當前選中的值
     *
     * @param selectProgress   進度
     * @param isNotifyListener 是否通知progresschangelistener
     */
    public void setSelectProgress(int selectProgress, boolean isNotifyListener) {
        getSelectProgressValue(selectProgress);
        Log.i(TAG, "mSelectProgress: " + mSelectProgress + " mMaxProgress: " +
                mMaxProgress);
        if (mOnProgressChangeListener != null && isNotifyListener) {
            mOnProgressChangeListener.onChange(mSelectProgress);
        }
        invalidate();
    }


    /**
     * 計算當前選中的進度條的值
     *
     * @param selectProgress 進度
     */
    private void getSelectProgressValue(int selectProgress) {
        mSelectProgress = selectProgress;
        if (mSelectProgress > mMaxProgress) {
            mSelectProgress = mMaxProgress;
        } else if (mSelectProgress <= mStartProgress) {
            mSelectProgress = mStartProgress;
        }
    }


複製代碼

自此本seekbar基本講述完畢,觀看下面源碼,能夠了解詳細的內容,每一個字段都有註釋,初學者能夠進行源碼查看。 源碼地址:github.com/389273716/h…

下一篇預告: 刻度盤view,支持外部倒計時控制,支持觸摸移動,點擊,帶動畫,支持配置界面元素,適配屏幕。

這裏寫圖片描述
相關文章
相關標籤/搜索