Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> Android View 自定義 RangeSeekBar 范圍選擇器

Android View 自定義 RangeSeekBar 范圍選擇器

編輯:Android資訊

前段時間群裡兄弟項目中有類似這樣的需求

我看到兄弟受苦受難,於心不忍。又因事不關己,打算高高掛起。正在愛恨糾結之時,日神對我說:沒事多造點輪子,你的人生會有很多收獲。這波雞湯讓我深受觸動,於是決定拯救兄弟於水生火熱之中。

重寫onMeasure 決策自身大小

顯而易見當可以拖拽的范圍極限為零時,也就是RangeSeeBar正常顯示能夠接受的極限,粗略一看:Width > 2 * Height

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
    if (heightSize * 2 > widthSize) {  
        setMeasuredDimension(widthSize, widthSize / 2);  
    } else {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    }  
}

繪制拖動條背景 凡事先從簡單開始

public class RangeSeekBar extends View {  
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);  

    private int lineTop, lineBottom, lineLeft, lineRight;  
    private int lineCorners;  
    private int lineWidth;  
    private RectF line = new RectF();  

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

    public RangeSeekBar(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
        if (heightSize * 2 > widthSize) {  
            setMeasuredDimension(widthSize, (int) (widthSize / 2));  
        } else {  
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        }  
    }  

    @Override  
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
        super.onSizeChanged(w, h, oldw, oldh);  
        int seekBarRadius = h / 2;  
        /** 
         * 屬性 left right top bottom 描述了SeekBar按鈕的位置 
         * 藍後根據它們預先設置確定出 RectF line 背景的三維 
         * lineCorners 圓滑的邊緣似乎會比直角更好看 
         */  
        lineLeft = seekBarRadius;  
        lineRight = w - seekBarRadius;  
        lineTop = seekBarRadius - seekBarRadius / 4;  
        lineBottom = seekBarRadius + seekBarRadius / 4;  
        lineWidth = lineRight - lineLeft;  
        line.set(lineLeft, lineTop, lineRight, lineBottom);  
        lineCorners = (int) ((lineBottom - lineTop) * 0.45f);  
    }  

    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        paint.setStyle(Paint.Style.FILL);  
        paint.setColor(0xFFD7D7D7);  
        canvas.drawRoundRect(line, lineCorners, lineCorners, paint);  
    }  
}

很明顯這裡設計seekBarRadius作為SeekBar按鈕的半徑,值為RangeSeekBar自身高度一半。那麼為了使默認狀態的SeekBar按鈕圓心能壓在背景條的起點和終點

背景條的起點和終點當然就分別相對於自身寬度往內部偏移一個半徑咯。

拖動舞台已經備好,SeekBar按鈕半徑也已定好。順水推舟,下一步就繪制SeekBar把。

SeekBar按鈕 擁有對象是極好的

粗略一想:按鈕有顏色、有大小、有變色、被繪制,碰撞檢測、邊界檢測、被拖拽等,最關鍵的是有多個。因此SeekBar按鈕可以說是一個復雜的集合體,是時候來發對象了。

private class SeekBar {  
    int widthSize;  
    int left, right, top, bottom;  
    Bitmap bmp;  

    /** 
     * 當RangeSeekBar尺寸發生變化時,SeekBar按鈕尺寸隨之變化  
     * 
     * @param centerX    SeekBar按鈕的X中心在RangeSeekBar中的相對位置 
     * @param centerY    SeekBar按鈕的Y中心在RangeSeekBar中的相對位置 
     * @param heightSize RangeSeekBar期望SeekBar所擁有的高度 
     */  
    void onSizeChanged(int centerX, int centerY, int heightSize) {  
        /** 
         * 屬性 left right top bottom 描述了SeekBar按鈕的位置<br> 
         * widthSize = heightSize * 0.8f 可見按鈕實際區域是個矩形而非正方形 
         * 圓圈按鈕為什麼要占有矩形區域?因為按鈕陰影效果。不要陰影不行嗎?我就不 
         * 那麼 onMeasure 那邊說好的2倍寬度?我就不 
         */  
        widthSize = (int) (heightSize * 0.8f);  
        left = centerX - widthSize / 2;  
        right = centerX + widthSize / 2;  
        top = centerY - heightSize / 2;  
        bottom = centerY + heightSize / 2;  

        bmp = Bitmap.createBitmap(widthSize, heightSize, Bitmap.Config.ARGB_8888);  
        int bmpCenterX = bmp.getWidth() / 2;  
        int bmpCenterY = bmp.getHeight() / 2;  
        int bmpRadius = (int) (widthSize * 0.5f);  
        Canvas defaultCanvas = new Canvas(bmp);  
        Paint defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        // 繪制Shadow  
        defaultPaint.setStyle(Paint.Style.FILL);  
        int barShadowRadius = (int) (bmpRadius * 0.95f);  
        defaultCanvas.save();  
        defaultCanvas.translate(0, bmpRadius * 0.25f);  
        RadialGradient shadowGradient = new RadialGradient(bmpCenterX, bmpCenterY, barShadowRadius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);  
        defaultPaint.setShader(shadowGradient);  
        defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, barShadowRadius, defaultPaint);  
        defaultPaint.setShader(null);  
        defaultCanvas.restore();  
        // 繪制Body  
        defaultPaint.setStyle(Paint.Style.FILL);  
        defaultPaint.setColor(0xFFFFFFFF);  
        defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);  
        // 繪制Border  
        defaultPaint.setStyle(Paint.Style.STROKE);  
        defaultPaint.setColor(0xFFD7D7D7);  
        defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);  
    }  

    void draw(Canvas canvas) {  
        canvas.drawBitmap(bmp, left, top, null);  
    }  
}
public class RangeSeekBar extends View {  

    private SeekBar seekBar = new SeekBar();  

    private class SeekBar {  
        ...  
    }  

    @Override  
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
        super.onSizeChanged(w, h, oldw, oldh);  
        int seekBarRadius = h / 2;  
        ...  
        // 在RangeSeekBar確定尺寸時確定SeekBar按鈕尺寸  
        seekBar.onSizeChanged(seekBarRadius, seekBarRadius, h);  
    }  

    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        ...  
        // 在RangeSeekBar被繪制時繪制SeekBar按鈕  
        seekBar.draw(canvas);  
    }  
}

距離成功又進了一步

onTouchEvent 觸摸監聽 讓SeekBar按鈕動起來

@Override  
public boolean onTouchEvent(MotionEvent event) {  
    switch (event.getAction()) {  
        case MotionEvent.ACTION_DOWN:  
            boolean touchResult = false;  
            // 進行檢測,手指手指是否落在當前SeekBar上。即聲明SeekBar時使用left、top、right、bottom屬性所描述區域的內部     
            if (seekbar.collide(event)) {  
                touchResult = true;  
            }  
            return touchResult;  
        case MotionEvent.ACTION_MOVE:  
            float percent;  
            float x = event.getX();  
            if (x <= lineLeft) {  
                percent = 0;  
            } else if (x >= lineRight){  
                percent = 1;  
            } else {  
                percent = (x - lineLeft) * 1f / (lineWidth);  
            }  
            // SeekBar按鈕根據當前手指在拖動條上的滑動而滑動  
            seekbar.slide(percent);  
            invalidate();  
            break;  
    }  
    return super.onTouchEvent(event);  
}
private class SeekBar {  
    int lineWidth; // 拖動條寬度 可在onSizeChanged時刻獲得  
    float currPercent;  
    int left, right, top, bottom;  

    boolean collide(MotionEvent event) {  
        float x = event.getX();  
        float y = event.getY();  
        int offset = (int) (lineWidth * currPercent);  
        return x > left + offset && x < right + offset && y > top && y < bottom;  
    }  

    void slide(float percent) {  
        if (percent < 0) percent = 0;  
        else if (percent > 1) percent = 1;  
        currPercent = percent;  
    }  

    void draw(Canvas canvas) {  
        int offset = (int) (lineWidth * currPercent);  
        canvas.save();  
        canvas.translate(offset, 0);  
        canvas.drawBitmap(bmp, left, top, null);  
        canvas.restore();  
    }  
}

更好的視覺體驗

到目前位置,SeekBar被按壓時顯得死氣沉沉,接下來為其添加強烈的視覺反饋。

那麼之前通過onSizeChanged預設按鈕的偷懶手段就GG了,因為SeekBar的UI效果需要隨觸摸狀態的變化而變化。

首先在onTouchEvent中拿到這個變化

@Override  
public boolean onTouchEvent(MotionEvent event) {  
    switch (event.getAction()) {  
        case MotionEvent.ACTION_MOVE:  
            seekBar.material = seekBar.material >= 1 ? 1 : seekBar.material + 0.1f;  
            ...  
            invalidate();  
            break;  
        case MotionEvent.ACTION_CANCEL:  
        case MotionEvent.ACTION_UP:  
            seekBar.materialRestore();  
            break;  
    }  
    return super.onTouchEvent(event);  
}

之後在SeekBar按鈕中響應這個變化

private class SeekBar {  
    float material = 0;  
    ValueAnimator anim;  
    final TypeEvaluator<Integer> te = new TypeEvaluator<Integer>() {  
        @Override  
        public Integer evaluate(float fraction, Integer startValue, Integer endValue) {  
            int alpha = (int) (Color.alpha(startValue) + fraction * (Color.alpha(endValue) - Color.alpha(startValue)));  
            int red = (int) (Color.red(startValue) + fraction * (Color.red(endValue) - Color.red(startValue)));  
            int green = (int) (Color.green(startValue) + fraction * (Color.green(endValue) - Color.green(startValue)));  
            int blue = (int) (Color.blue(startValue) + fraction * (Color.blue(endValue) - Color.blue(startValue)));  
            return Color.argb(alpha, red, green, blue);  
        }  
    };  

    void draw(Canvas canvas) {  
        int offset = (int) (lineWidth * currPercent);  
        canvas.save();  
        canvas.translate(left, 0);  
        canvas.translate(offset, 0);  
        drawDefault(canvas);  
        canvas.restore();  
    }  

    private void drawDefault(Canvas canvas) {  
        int centerX = widthSize / 2;  
        int centerY = heightSize / 2;  
        int radius = (int) (widthSize * 0.5f);  
        // draw shadow  
        defaultPaint.setStyle(Paint.Style.FILL);  
        canvas.save();  
        canvas.translate(0, radius * 0.25f);  
        canvas.scale(1 + (0.1f * material), 1 + (0.1f * material), centerX, centerY);  
        defaultPaint.setShader(shadowGradient);  
        canvas.drawCircle(centerX, centerY, radius, defaultPaint);  
        defaultPaint.setShader(null);  
        canvas.restore();  
        // draw body  
        defaultPaint.setStyle(Paint.Style.FILL);  
        defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFFE7E7E7));  
        canvas.drawCircle(centerX, centerY, radius, defaultPaint);  
        // draw border  
        defaultPaint.setStyle(Paint.Style.STROKE);  
        defaultPaint.setColor(0xFFD7D7D7);  
        canvas.drawCircle(centerX, centerY, radius, defaultPaint);  
    }  

    private void materialRestore() {  
        if (anim != null) anim.cancel();  
        anim = ValueAnimator.ofFloat(material, 0);  
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
            @Override  
            public void onAnimationUpdate(ValueAnimator animation) {  
                material = (float) animation.getAnimatedValue();  
                invalidate();  
            }  
        });  
        anim.addListener(new AnimatorListenerAdapter() {  
            @Override  
            public void onAnimationEnd(Animator animation) {  
                material = 0;  
                invalidate();  
            }  
        });  
        anim.start();  
    }  
}

draw方法中的直接繪制bmp的邏輯被替換為drawDefault

那麼drawDefault的內部邏輯基本和預制bmp一樣,唯二的區別在於對陰影Shadow做了個scale處理,對按鈕Body顏色做了個漸變處理
materialRestore即當用戶手指抬起後開個線程將狀態漸變為初始態

Range

Range的意思就是范圍,但是就算知道這些似乎並沒有什麼卵用 _(:3 」∠)_
so為了了解其中規律,本寶寶使勁摸索。最終發現

如果分開來看它們都擁有自己的固定滑動區間,右邊的SeekBar按鈕就是左邊SeekBar按鈕向右平移了個SeekBar按鈕寬度而已。

public class RangeSeekBar extends View {  
    private SeekBar leftSB = new SeekBar();  
    private SeekBar rightSB = new SeekBar();  
    /** 
     * 用來記錄當前用戶觸摸的到底是哪個SB 
     */  
    private SeekBar currTouch;  

    @Override  
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
        super.onSizeChanged(w, h, oldw, oldh);  
        ...  
        // rightSB就如同分析的一樣,緊緊貼在leftSB的右邊而已  
        rightSB.left += leftSB.widthSize;  
        rightSB.right += leftSB.widthSize;  
    }  

    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
        ...  
        leftSB.draw(canvas);  
        rightSB.draw(canvas);  
    }  

    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                boolean touchResult = false;  
                /** 
                 * 為什麼不先檢測leftSB而先檢測rightSB?為什麼? (●'◡'●) 
                 */  
                if (rightSB.collide(event)) {  
                    currTouch = rightSB;  
                    touchResult = true;  
                } else if (leftSB.collide(event)) {  
                    currTouch = leftSB;  
                    touchResult = true;  
                }  
                return touchResult;  
            case MotionEvent.ACTION_MOVE:  
                float percent;  
                float x = event.getX();  

                if (currTouch == leftSB) {  
                    if (x < lineLeft) {  
                        percent = 0;  
                    } else {  
                        percent = (x - lineLeft) * 1f / (lineWidth - rightSB.widthSize);  
                    }  

                    if (percent > rightSB.currPercent) {  
                        percent = rightSB.currPercent;  
                    }  
                    leftSB.slide(percent);  
                } else if (currTouch == rightSB) {  
                    if (x > lineRight) {  
                        percent = 1;  
                    } else {  
                        percent = (x - lineLeft - leftSB.widthSize) * 1f / (lineWidth - leftSB.widthSize);  
                    }  
                    if (percent < leftSB.currPercent) {  
                        percent = leftSB.currPercent;  
                    }  
                    rightSB.slide(percent);  
                }  

                invalidate();  
                break;  
        }  
        return super.onTouchEvent(event);  
    }  
}

通過觸摸改變一些屬性的值,通過這些屬性的值繪制出對應的UI效果,套路一切都是套路

那麼繼SwitchButton後,又算是重新溫習了一次該套路

那麼本寶寶的RangeSeekBar還能做到什麼?

支持負數

支持預留(保留)范圍

什麼是預留(保留)范圍?比如那個,你懂得。只可意會,不可言傳。(≖ ‿ ≖)✧

比如現在2個按鈕直接就保留了一個距離,當然也可以保留n個

支持刻度模式

當然支持刻度的同時也支持預留范圍

支持自定義UI按鈕樣式背景顏色

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved