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

風起

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

介紹

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

實現

  1. 確定范圍選擇器需要哪些自定義屬性,并在 res/values 目錄下新建一個資源文件 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. 接下來接是創建范圍選擇器,LcRangeView 繼承自 View ,并實現 LcRangeView 的三個構造方法
    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. 之前我們定義選擇器所需要的屬性,那么現在我就要在 View 中拿到這些屬性的賦值并處理,當然為了避免調用者沒有給這之中的哪個屬性賦值而產生繪圖顯示異常,我們也默認得給這些屬性默認值
    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. 拿到了繪圖所需要的數據,接下來就是測量選擇器的大小,重寫 onMeasure() 方法。首先試想一下,自適應情況控件的寬高應該是多大,寬的話我們就填充完屏幕,高呢,選擇球的高度,外部大小決定好了就該考慮一下內部的測量,標刻線應該為控件正中間位置,即兩個球心的連接線,寬則為控件左右邊各空出一個球的半徑位置以保證球在最左或最右顯示不完整。
    @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("測試", "看看y"+mMidY);
         mLineStartX = markBallRadius;
         mLineEndX = mLineLength + markBallRadius;
         mMinPosition = mLineStartX;
         mMaxPosition = mLineEndX;
     }
    
  5. 測量好了就該繪圖了,重寫 onDraw() 方法,我們要明確的畫圖的順序,標準刻度線 -> 選擇刻度線 -> 選擇球,想好了怎么畫就該準備筆 (paint) 和 (canvas) ,繪制所需的參數在前面已經定義過了,形狀一出立馬感覺成功了一半,

     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. 圖形已經出現,我們目前要操作的是兩個球,那么我們就得判斷球是否被觸摸到,我這里觸摸的范圍是剛好裝下球的正方形,你也適當得增大觸控面積(如果你的球需要繪制很小的話)

     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. 寫好了判斷,接下來就是實現拖動效果了,當手指按下時,就判斷是否觸摸到了球,觸摸了那個球,記錄下狀態;當手指抬起時,都將觸摸狀態置為 false ;當手指滑動時,根據觸摸狀態執行相應的的滑動

     @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. 繼續來處理滑動邏輯,我們要先知道球的滑動范圍,
    minBall 的滑動范圍為 標準線的起點 -- maxBall 的球心位置,
    maxBall 的滑動位置為 maxBall 的球心位置 -- 標準線的終點。
    (如果需要讓兩個球不重疊,可以邊界增加一個球的寬度)
    確定球的新位置后,調用 invalidate() 進行重繪
    當確定為正在移動球的時候,即使脫離本控件的的范圍一樣可以更新視圖

     private void moveToMinPosition(MotionEvent event) {
         if (event.getX() < mMaxPosition && event.getX() >= mLineStartX) {
             mMinPosition = (int) event.getX();
             invalidate();
             /** 配合 10 一起看,這個必須判斷是否為空,如果調用者不監聽會導致空指針異常
             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. 現在界面的雛形已經出現了,接下來我們要根據滑動來實時更新我們的范圍值,一開始我們就拿到了總范圍值,然后根據滑動比例獲取范圍值

    計算公式

    • min 值:(minBall 位置 - 標準線起點)/ 標準線長度 * 總范圍值

    • max 值:(maxBall 位置 - 標準線起點)/ 標準線長度 * 總范圍值

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

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

總結

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容