天天看點

Android_自定義可定制步長的雙向SeekBar

還記得幾年前寫過一個雙向seekbar嗎,不足的是不支援步長擴充,老的雙向seekbar連結

這幾天正好做需求,要擴充一個支援步長,一次隻能滑動50個,松開,即刻回彈到距離它最近的機關坐标上,WFK.那麼我們要開車了.

需求理一下

  • 雙向拖動
  • 定義步長
  • 回彈确定最終值
  • 文字描述不能因為太近而遮蓋
  • …..還有一堆擴充屬性不說了

老規矩,效果圖如下

Android_自定義可定制步長的雙向SeekBar

直接看做出來的成品

Android_自定義可定制步長的雙向SeekBar

接下裡就手把手,我們來實作一下這個支援定制步長的拖拽seekbar

GIF豈不是更爽

Android_自定義可定制步長的雙向SeekBar

實作步驟

  • 分析自定義屬性擴充
  • 定義并且擷取屬性
  • 計算要繪制的區域的坐标以及範圍
  • 暴露接口傳回資料

1.自定義屬性

<declare-styleable name="MyElongScaleSeekBar">
        <attr name="scale_progress_normal_color" format="color" />
        <attr name="scale_progress_section_color" format="color" />
        <attr name="scale_left_ball_bg_color" format="color" />
        <attr name="scale_left_ball_stroke_color" format="color" />
        <attr name="scale_left_ball_stroke_with" format="dimension" />
        <attr name="scale_right_ball_bg_color" format="color" />
        <attr name="scale_right_ball_stroke_color" format="color" />
        <attr name="scale_right_ball_stroke_with" format="dimension" />
        <attr name="scale_ball_radio" format="dimension" />
        <attr name="scale_ball_shadow_radio" format="dimension" />
        <attr name="scale_ball_shadow_color" format="color" />
        <attr name="scale_seek_height" format="dimension" />
        <attr name="scale_left_text_color" format="dimension" />
        <attr name="scale_left_text_size" format="dimension" />
        <attr name="scale_right_text_color" format="dimension" />
        <attr name="scale_right_text_size" format="dimension" />
        <attr name="scale_text_margin_ball" format="dimension" />
        <attr name="scale_ball_stick_height" format="dimension" />
        <attr name="scale_ball_stick_width" format="dimension" />
        <attr name="scale_ball_stick_margin" format="dimension" />
        <attr name="scale_ball_stick_color" format="color" />
        <attr name="scale_progress_unit" format="integer" />
        <attr name="scale_symbol_front" format="string" />

    </declare-styleable>
           

這裡就不多解釋了,思路就是和你用Android原生textview一樣,盡可能的考慮到每個屬性的擴充,比如這次我們seekbar的步數,小球的顔色,陰影的顔色,等等.

2.擷取自定義屬性

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyElongScaleSeekBar, , R.style.default_scale_seekbar_style);
        int indexCount = typedArray.getIndexCount();
        for (int i = ; i < indexCount; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.MyElongScaleSeekBar_scale_progress_normal_color:
                    scaleProgressNormalColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                    ....省略.....
           

自定義屬性的擷取,其實就是view在購置的時候,我們用context拿到TypedArray,去循環周遊我們聲明的擴充屬性,循環到一個屬性,根據你定義的類型,去getvalue即可擷取到

3.确定要繪制的圖案分幾部分組成

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 背景進度條
        drawScaleSeekNormal(canvas);
        // 目前選中的進度
        drawScaleSeekSection(canvas);
        // 左邊的球
        drawLeftBall(canvas);
        // 右邊的球
        drawRightBall(canvas);
        // 左邊球的文字
        drawLeftText(canvas);
        // 右邊球的文字
        drawRightText(canvas);

        // 說了這麼多,其實就是畫圓圈和畫方塊的小把戲
    }
           

4.我們定義了要畫哪些東西,接下裡就是調用canvas的api

// 畫方塊
canvas.drawRect(...)
// 畫圓圈
canvas.drawCircle(...)
// 本效果就用到了這兩個api
           

5.繪制之前,我們要重寫view的測量.這個要根據你的效果圖自己設定,我們view的寬度肯定是充滿父布局的,是以直接外面給match就行主要的是高度,因為效果圖分兩部分,上半部分是文字區域,下半部分是拖拽區域是以view的高度要在測量模式中改成EXACTLY模式,因為高度你自己從定義的屬性裡面能擷取到.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureHeight;
        if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
        //确定view的高度(文字高度+距離+拖拽高度)
            measureHeight = Math.max(scaleLeftTextSize, scaleRightTextSize) +  + scaleTextMarginBall + scaleBallRadio * ;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
           

6.确定繪制的圖形的原始位置,展現靜态圖

思路很簡單,把UI給你的效果圖,在一張紙上畫一下,

你用鉛筆畫的過程,就是你轉換代碼确定位置的過程,

你的紙就是Android的坐标系,畫圓圈要确定圓心位置,畫方塊要确定方塊的四個頂點.那麼接下來你隻要确定了圓心位置,和方塊位置定點,那麼靜态圖就出來了.

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //球的半徑包含描邊
        radioWithStroke = scaleBallRadio + scaleLeftBallStrokeWith * F;
        xCoordinateUnit = radioWithStroke * ;
        valueEntities = calculateAllXCoordinate(scaleProgressUnit, w - radioWithStroke * , maxValue);

        if (valueEntities != null && valueEntities.size() > ) {
            currentLeft = valueEntities.get();
            currentRight = valueEntities.get(valueEntities.size() - );

            leftDesc = creatCurrentDataDesc(currentLeft);
            rightDesc = creatCurrentDataDesc(currentRight);

            if (this.seekBarDragListener != null) {
                this.seekBarDragListener.seekMoveValue(currentLeft.value, currentRight.value);
            }
        }

        if (currentLeft != null && currentRight != null) {
            // 背景進度條的區域
            scaleSeekNormalRectF = new RectF();
            scaleSeekNormalRectF.left = ;
            float top = h - (scaleBallRadio + scaleLeftBallStrokeWith * F) - scaleSeekHeight * F - scaleBallShadowRadio;
            scaleSeekNormalRectF.top = top;
            scaleSeekNormalRectF.bottom = top + scaleSeekHeight;
            scaleSeekNormalRectF.right = w;


            // 左邊球的圓心坐标
            leftBallPoint = new SeekPoint();
            leftBallPoint.x = currentLeft.xCoordinat + radioWithStroke;
            leftBallPoint.y = h - radioWithStroke - scaleBallShadowRadio;

            // 左邊球中間的猴三棍
            calculateLeftSticks();

            // 右邊球的坐标
            rightBallPoint = new SeekPoint();
            rightBallPoint.x = currentRight.xCoordinat + radioWithStroke;
            rightBallPoint.y = h - radioWithStroke - scaleBallShadowRadio;

            // 右邊球中間的猴三棍
            calculateRightSticks();

            // 選中背景條的間距部分
            scaleSeekSectionRectF = new RectF();
            scaleSeekSectionRectF.left = leftBallPoint.x - radioWithStroke;
            scaleSeekSectionRectF.right = rightBallPoint.x - radioWithStroke;
            scaleSeekSectionRectF.top = scaleSeekNormalRectF.top;
            scaleSeekSectionRectF.bottom = scaleSeekNormalRectF.bottom;

            // 圓球的路徑和區域
            scaleLeftBallPath = new Path();
            scaleLeftBallPath.addCircle(leftBallPoint.x, leftBallPoint.y, radioWithStroke, Path.Direction.CW);

            scaleRightBallPath = new Path();
            scaleRightBallPath.addCircle(rightBallPoint.x, rightBallPoint.y, radioWithStroke, Path.Direction.CW);

            scaleLeftBallRegion = updateRegionByPath(scaleLeftBallPath);
            scaleRightBallRegion = updateRegionByPath(scaleRightBallPath);
        }
    }
           

7.最後一步就是拖拽,将靜态圖改變成動态重新整理的靜态圖

因為你每次的拖拽都執行的move,那麼成千上萬的move組成的靜态圖在一zhen一zhen的過的時候,是不是所謂的連續動畫,類似于小時候的動畫書…

  • 動起來,根據ontouch的位置x坐标實時計算圓心的坐标和矩形的四個頂點
  • 确定拖拽的是哪個圓圈?
// 圓球的路徑和區域
            scaleLeftBallPath = new Path();
            scaleLeftBallPath.addCircle(leftBallPoint.x, leftBallPoint.y, radioWithStroke, Path.Direction.CW);

 Region region = new Region();
        if (path != null) {
            RectF tempRectF = new RectF();
            path.computeBounds(tempRectF, true);
            region.setPath(path, new Region((int) tempRectF.left, (int) tempRectF.top, (int) tempRectF.right, (int) tempRectF.bottom));
        }
        return region;
           

我們把圓圈的坐标放到path裡面,根據path生成一個他的region,那麼我們在觸摸的時候,能拿到目前觸摸的坐标x,y,那麼我們在ACTION_DOWN裡面就可以判斷你觸摸的點是左邊的球還是右邊的球,那麼拖動那個就對那個進行坐标幀重新整理即可.

case MotionEvent.ACTION_DOWN:
                boolean touchLeftBall = scaleLeftBallRegion.contains((int) event.getX(), (int) event.getY()) && !scaleRightBallRegion.contains((int) event.getX(), (int) event.getY());
                boolean touchRightBall = scaleRightBallRegion.contains((int) event.getX(), (int) event.getY()) && !scaleLeftBallRegion.contains((int) event.getX(), (int) event.getY());
           

8.最後一步,彈性回到目前坐标對應的步長.

這個就是一個經常面試的時候一個小問題,給你一個任意數字,在一個數組裡面找到與他最接近的數字并傳回.

對應到我們onTouch裡面就是,你觸摸的任意一點,要從你生成的步長資料集合中找到與之對應的最接近的值即可

public UnitValueEntity binarySearchKey(List<UnitValueEntity> data, int targetNum) {
        if (data != null && data.size() > ) {
            int left = , right = ;
            for (right = data.size() - ; left != right; ) {
                int midIndex = (right + left) / ;
                int mid = (right - left);
                int midValue = (int) data.get(midIndex).xCoordinat;
                if (targetNum == midValue) {
                    return data.get(midIndex);
                }
                if (targetNum > midValue) {
                    left = midIndex;
                } else {
                    right = midIndex;
                }

                if (mid <= ) {
                    break;
                }
            }
            UnitValueEntity rightnum = data.get(right);
            UnitValueEntity leftnum = data.get(left);
            return Math.abs((rightnum.xCoordinat - leftnum.xCoordinat) / ) > Math.abs(rightnum.xCoordinat - targetNum) ? rightnum : leftnum;
        }
        return null;
    }
           

// 我們這邊就用二分查找,來找到這個值,并且傳回.

9最後提醒一點,在滑動的時候,注意對兩個球的臨界值進行判斷,比如很直覺的兩個球不能滑動到螢幕外面,是以對邊界進行判定即可.

說一千到一萬,不如源碼給你直接看,請各位看官賞臉,點個star,萬分感謝,https://github.com/GuoFeilong/ATDragViewDemo