Android 自定義 View -- 雙向范圍選擇器,新手踏坑

風(fēng)起

最近的項(xiàng)目需要用到一個(gè)雙向范圍選擇器,遂自己操刀并做下記錄

介紹

范圍選擇器要實(shí)現(xiàn)的功能就是進(jìn)行范圍選擇,并提供接口向調(diào)用者暴露所選最小最大值,由于項(xiàng)目只是需要一個(gè)普通的范圍選擇器,所以并沒有其他的花哨的動(dòng)畫特效 duang ~(為自己的技窮找一個(gè)借口)

實(shí)現(xiàn)

  1. 確定范圍選擇器需要哪些自定義屬性,并在 res/values 目錄下新建一個(gè)資源文件 attrs.xml (隨意) 來聲明我們這些屬性
    <resources>
    <declare-styleable name="LcRangeBar">
    <attr name="minMark" format="integer" />
    <attr name="maxMark" format="integer" />
    <attr name="markBallRadius" format="dimension" />
    <attr name="markBallColor" format="color" />
    <attr name="unMarkLineSize" format="dimension" />
    <attr name="markLineSize" format="dimension" />
    <attr name="unMarkLineColor" format="color" />
    <attr name="markLineColor" format="color" />
    </declare-styleable>
    </resources>

  2. 接下來接是創(chuàng)建范圍選擇器,LcRangeView 繼承自 View ,并實(shí)現(xiàn) LcRangeView 的三個(gè)構(gòu)造方法
    public LcRangeBar(Context context) {
    super(context);
    initAttrs(null);
    }
    public LcRangeBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    initAttrs(attrs);
    }
    public LcRangeBar(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initAttrs(attrs);
    }

  3. 之前我們定義選擇器所需要的屬性,那么現(xiàn)在我就要在 View 中拿到這些屬性的賦值并處理,當(dāng)然為了避免調(diào)用者沒有給這之中的哪個(gè)屬性賦值而產(chǎn)生繪圖顯示異常,我們也默認(rèn)得給這些屬性默認(rèn)值
    private void initAttrs(AttributeSet attrs) {
    if (attrs != null) {
    TypedArray ta = getContext().obtainStyledAttributes(attrs,
    R.styleable.LcRangeBar, 0, 0);
    minMark = ta.getInt(R.styleable.LcRangeBar_minMark,
    DEFAULT_MIN_MARK);
    maxMark = ta.getInt(R.styleable.LcRangeBar_maxMark,
    DEFAULT_MAX_MARK);
    markBallColor = ta.getColor(R.styleable.LcRangeBar_markBallColor,
    DEFAULT_MARK_BALL_COLOR);
    markLineColor = ta.getColor(R.styleable.LcRangeBar_markLineColor,
    DEFAULT_MARK_LINE_COLOR);
    unMarkLineColor = ta.getColor(
    R.styleable.LcRangeBar_unMarkLineColor,
    DEFAULT_UNMARK_LINE_COLOR);
    markBallRadius = (int) ta.getDimension(
    R.styleable.LcRangeBar_markBallRadius,
    dp2px(DEFAULT_MARK_BALL_RADIUS));
    markLineSize = (int) ta.getDimension(
    R.styleable.LcRangeBar_markLineSize,
    dp2px(DEFAULT_MARK_LINE_SIZE));
    unMarkLineSize = (int) ta.getDimension(
    R.styleable.LcRangeBar_unMarkLineSize,
    dp2px(DEFAULT_UNMARK_LINE_SIZE));
    ta.recycle();
    }
    markRange = maxMark - minMark;
    }

  4. 拿到了繪圖所需要的數(shù)據(jù),接下來就是測(cè)量選擇器的大小,重寫 onMeasure() 方法。首先試想一下,自適應(yīng)情況控件的寬高應(yīng)該是多大,寬的話我們就填充完屏幕,高呢,選擇球的高度,外部大小決定好了就該考慮一下內(nèi)部的測(cè)量,標(biāo)刻線應(yīng)該為控件正中間位置,即兩個(gè)球心的連接線,寬則為控件左右邊各空出一個(gè)球的半徑位置以保證球在最左或最右顯示不完整。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int expectedWidth = dp2px(200);
    int expectedHeight = dp2px(30);
    int finalWidth = expectedWidth;
    int finalHeight = expectedHeight;

         if (widthMode == MeasureSpec.EXACTLY) {
             finalWidth = widthSize; 
         } else if (widthMode == MeasureSpec.AT_MOST) {
             finalWidth = expectedWidth;
         }
         if (heightMode == MeasureSpec.EXACTLY) {
             finalHeight = heightSize;
         } else if (heightMode == MeasureSpec.AT_MOST) {
             finalHeight = markBallRadius;
         }
         
         mLineLength = (finalWidth - markBallRadius * 2);
         mMidY = finalHeight / 2;
         Log.d("測(cè)試", "看看y"+mMidY);
         mLineStartX = markBallRadius;
         mLineEndX = mLineLength + markBallRadius;
         mMinPosition = mLineStartX;
         mMaxPosition = mLineEndX;
     }
    
  5. 測(cè)量好了就該繪圖了,重寫 onDraw() 方法,我們要明確的畫圖的順序,標(biāo)準(zhǔn)刻度線 -> 選擇刻度線 -> 選擇球,想好了怎么畫就該準(zhǔn)備筆 (paint) 和 (canvas) ,繪制所需的參數(shù)在前面已經(jīng)定義過了,形狀一出立馬感覺成功了一半,

     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         drawUnMarkLine(canvas);
         drawMarkLine(canvas);
         drawMarkBalls(canvas);  
     }
    
     private void drawMarkBalls(Canvas canvas) {
         mPaint.setColor(markBallColor);
         canvas.drawCircle(mMinPosition, mMidY, markBallRadius, mPaint);
         canvas.drawCircle(mMaxPosition, mMidY, markBallRadius, mPaint);
     }
    
     private void drawMarkLine(Canvas canvas) {
         mPaint.setColor(markLineColor);
         mPaint.setStrokeWidth(markLineSize);
         canvas.drawLine(mMinPosition, mMidY, mMaxPosition, mMidY, mPaint);
     }
    
     private void drawUnMarkLine(Canvas canvas) {
         mPaint.setColor(unMarkLineColor);
         mPaint.setStrokeWidth(unMarkLineSize);
         canvas.drawLine(mLineStartX, mMidY, mLineEndX, mMidY, mPaint);
     }
    
  6. 圖形已經(jīng)出現(xiàn),我們目前要操作的是兩個(gè)球,那么我們就得判斷球是否被觸摸到,我這里觸摸的范圍是剛好裝下球的正方形,你也適當(dāng)?shù)迷龃笥|控面積(如果你的球需要繪制很小的話)

     private boolean isTouchingMaxBall(MotionEvent event) {
         return event.getX() > mMaxPosition - markBallRadius
                 && event.getX() < mMaxPosition + markBallRadius
                 && event.getY() > mMidY - markBallRadius
                 && event.getY() < mMidY + markBallRadius;
     }
    
     private boolean isTouchingMinBall(MotionEvent event) {
         return event.getX() > mMinPosition - markBallRadius
                 && event.getX() < mMinPosition + markBallRadius
                 && event.getY() > mMidY - markBallRadius
                 && event.getY() < mMidY + markBallRadius;
     }
    
  7. 寫好了判斷,接下來就是實(shí)現(xiàn)拖動(dòng)效果了,當(dāng)手指按下時(shí),就判斷是否觸摸到了球,觸摸了那個(gè)球,記錄下狀態(tài);當(dāng)手指抬起時(shí),都將觸摸狀態(tài)置為 false ;當(dāng)手指滑動(dòng)時(shí),根據(jù)觸摸狀態(tài)執(zhí)行相應(yīng)的的滑動(dòng)

     @Override
     public boolean onTouchEvent(MotionEvent event) {
         switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
             if (isTouchingMinBall(event)) {
                 isOnMinBall = true;
             } else if (isTouchingMaxBall(event)) {
                 isOnMaxBall = true;
             }
             break;
         case MotionEvent.ACTION_MOVE:
             if (isOnMinBall) {
                 jumpToMin(event);
             }
             if (isOnMaxBall) {
                 jumpToMax(event);
             }
             break;
    
         case MotionEvent.ACTION_UP:
             if (isOnMinBall) {
                 isOnMinBall = false;
             }
             if (isOnMaxBall) {
                 isOnMaxBall = false;
             }
             break;
         }
    
         return true;
     }
    
  8. 繼續(xù)來處理滑動(dòng)邏輯,我們要先知道球的滑動(dòng)范圍,
    minBall 的滑動(dòng)范圍為 標(biāo)準(zhǔn)線的起點(diǎn) -- maxBall 的球心位置,
    maxBall 的滑動(dòng)位置為 maxBall 的球心位置 -- 標(biāo)準(zhǔn)線的終點(diǎn)。
    (如果需要讓兩個(gè)球不重疊,可以邊界增加一個(gè)球的寬度)
    確定球的新位置后,調(diào)用 invalidate() 進(jìn)行重繪
    當(dāng)確定為正在移動(dòng)球的時(shí)候,即使脫離本控件的的范圍一樣可以更新視圖

     private void moveToMinPosition(MotionEvent event) {
         if (event.getX() < mMaxPosition && event.getX() >= mLineStartX) {
             mMinPosition = (int) event.getX();
             invalidate();
             /** 配合 10 一起看,這個(gè)必須判斷是否為空,如果調(diào)用者不監(jiān)聽會(huì)導(dǎo)致空指針異常
             if (mRangeChangeListener != null) {
                 mRangeChangeListener.onMinChange(Math
                         .round((float) (mMinPosition - mLineStartX)
                                 / mLineLength * markRange));
             }
             **/
         }
     }
    
     private void moveToMaxPosition(MotionEvent event) {
         if (event.getX() > mMinPosition && event.getX() <= mLineEndX) {
             mMaxPosition = (int) event.getX();
             invalidate();
             /** 配合 10 一起看
             if (mRangeChangeListener != null) {
                 mRangeChangeListener.onMaxChange(Math
                         .round((float) (mMaxPosition - mLineStartX)
                                 / mLineLength * markRange));
             }
             **/
         }
     }
    
  9. 現(xiàn)在界面的雛形已經(jīng)出現(xiàn)了,接下來我們要根據(jù)滑動(dòng)來實(shí)時(shí)更新我們的范圍值,一開始我們就拿到了總范圍值,然后根據(jù)滑動(dòng)比例獲取范圍值

    計(jì)算公式

    • min 值:(minBall 位置 - 標(biāo)準(zhǔn)線起點(diǎn))/ 標(biāo)準(zhǔn)線長度 * 總范圍值

    • max 值:(maxBall 位置 - 標(biāo)準(zhǔn)線起點(diǎn))/ 標(biāo)準(zhǔn)線長度 * 總范圍值

  10. 范圍值我們拿到了,最后一步結(jié)束范圍值提供給調(diào)用者,這個(gè)部分大家都很熟悉了,直接貼

    public interface RangeChangeListener {
        void onMinChange(int minValue);
        void onMaxChange(int maxValue);
    }
    
    public void setRangeChangeListener(RangeChangeListener rangeChangeListener) {
        mRangeChangeListener = rangeChangeListener;
    }
    

總結(jié)

到此為止一個(gè)簡單的范圍選擇器就完成了,由于最近還在趕其他項(xiàng)目,所以目前先這么簡陋的吧,如果有其他需要還可以更加完善,如多點(diǎn)操作,在標(biāo)準(zhǔn)線上點(diǎn)擊實(shí)現(xiàn)球的位置跳轉(zhuǎn),變化動(dòng)畫等。沒什么技術(shù)含量,純粹寫寫文記錄開發(fā)經(jīng)歷而已。(有空補(bǔ)上源碼圖片)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容