SeekBar

27 阅读4分钟

超级无敌SeekBar专门为笨B打造

```
package com.example.myapplicationwithg;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import androidx.annotation.Nullable;

public class CustomSeekBar extends View {

    private static final float SCALE_FACTOR = 1.5f;
    private static final long RELEASE_DELAY_MS = 200L;
    public static final int ORIENTATION_HORIZONTAL = 0;
    public static final int ORIENTATION_VERTICAL = 1;

    private int trackColor = Color.LTGRAY;
    private Drawable progressDrawable;
    private int progressColorPressed = Color.BLUE;
    private Drawable edgeDrawable;
    private int edgeColorPressed = Color.BLACK;
    private int thumbColor = Color.WHITE;
    private Drawable thumbDrawable = null;

    private float userSetTrackHeight = -1f;
    private float baseTrackHeight = 0f;
    private float pressedTrackHeight = 0f;
    private float edgeWidth = 0f;
    private float paddingAroundThumb = 0f;
    private float trackProgressGap = 0f;

    private int orientation = ORIENTATION_HORIZONTAL;
    private int progress = 5;
    private int maxProgress = 20;

    private boolean isPressedState = false;
    private float currentTrackHeight = 0f;
    private int currentThumbAlpha = 0;
    private float currentThumbRadius = 0f;
    private ValueAnimator animator = null;

    private final RectF trackRect = new RectF();
    private final RectF progressRect = new RectF();
    private final Rect thumbBounds = new Rect();
    private final Path clipPath = new Path();
    private float thumbCenterX = 0f;
    private float thumbCenterY = 0f;

    private final Paint commonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final Runnable releaseRunnable = () -> setPressedState(false);
    private float touchExtension;

    public interface OnProgressChangeListener {
        void onProgressChanged(int progress);

        void onStopTrackingTouch(int progress);
    }

    private OnProgressChangeListener onProgressChangeListener = null;

    public void setOnProgressChangeListener(OnProgressChangeListener listener) {
        this.onProgressChangeListener = listener;
    }

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

    public CustomSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        float density = getResources().getDisplayMetrics().density;
        touchExtension = 20f * density;

        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSeekBar);
            try {
                trackColor = a.getColor(R.styleable.CustomSeekBar_seekbar_trackColor, Color.LTGRAY);
                progressDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_progressDrawable);
                progressColorPressed = a.getColor(R.styleable.CustomSeekBar_seekbar_progressColorPressed, Color.BLUE);
                edgeDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_edgeDrawable);
                edgeColorPressed = a.getColor(R.styleable.CustomSeekBar_seekbar_edgeColorPressed, Color.BLACK);
                thumbColor = a.getColor(R.styleable.CustomSeekBar_seekbar_thumbColor, Color.WHITE);
                thumbDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_thumbDrawable);
                userSetTrackHeight = a.getDimension(R.styleable.CustomSeekBar_seekbar_trackHeight, -1f);
                edgeWidth = a.getDimension(R.styleable.CustomSeekBar_seekbar_edgeWidth, 2f * density);
                paddingAroundThumb = a.getDimension(R.styleable.CustomSeekBar_seekbar_thumbPadding, 2f * density);
                trackProgressGap = a.getDimension(R.styleable.CustomSeekBar_seekbar_progressPadding, 5f * density);
                maxProgress = a.getInt(R.styleable.CustomSeekBar_seekbar_max, 100);
                progress = a.getInt(R.styleable.CustomSeekBar_seekbar_progress, 50);
                orientation = a.getInt(R.styleable.CustomSeekBar_seekbar_orientation, ORIENTATION_HORIZONTAL);
            } finally {
                a.recycle();
            }
        }
        setClickable(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w <= 0 || h <= 0) return;

        float availableSize = (orientation == ORIENTATION_HORIZONTAL) ?
                (h - getPaddingTop() - getPaddingBottom()) :
                (w - getPaddingLeft() - getPaddingRight());

        if (userSetTrackHeight >= 0) {
            baseTrackHeight = userSetTrackHeight;
            pressedTrackHeight = Math.min(baseTrackHeight * SCALE_FACTOR, availableSize);
        } else {
            pressedTrackHeight = availableSize;
            baseTrackHeight = availableSize / SCALE_FACTOR;
        }

        currentTrackHeight = isPressedState ? pressedTrackHeight : baseTrackHeight;
        updateThumbRadius();
        updateGeometry();
    }

    private void updateThumbRadius() {
        float pThickness = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
        currentThumbRadius = Math.max(0f, pThickness / 2f - paddingAroundThumb);
    }

    private float getFixedSafeMargin() {
        float maxPThickness = Math.max(0f, pressedTrackHeight - 2 * trackProgressGap - edgeWidth);
        float maxRadius = Math.max(0f, maxPThickness / 2f - paddingAroundThumb);
        return trackProgressGap + edgeWidth / 2f + paddingAroundThumb + maxRadius;
    }

    private void updateGeometry() {
        float ratio = (float) progress / maxProgress;
        float safeMargin = getFixedSafeMargin();
        float innerEdge = trackProgressGap + edgeWidth / 2f;

        if (orientation == ORIENTATION_HORIZONTAL) {
            float trackW = getWidth() - getPaddingLeft() - getPaddingRight();
            float tTop = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom() - currentTrackHeight) / 2f;
            trackRect.set(getPaddingLeft(), tTop, getPaddingLeft() + trackW, tTop + currentTrackHeight);

            float minX = trackRect.left + safeMargin, maxX = trackRect.right - safeMargin;
            thumbCenterX = minX + (maxX - minX) * ratio;
            thumbCenterY = trackRect.centerY();

            float pHeight = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
            float pTop = trackRect.top + (currentTrackHeight - pHeight) / 2f;
            float wrap = isPressedState ? (currentThumbRadius + paddingAroundThumb) : 0f;
            float right = (progress == maxProgress) ? trackRect.right - innerEdge : clamp(thumbCenterX + wrap, trackRect.left + innerEdge, trackRect.right - innerEdge);
            progressRect.set(trackRect.left + innerEdge, pTop, right, pTop + pHeight);

        } else {
            float trackH = getHeight() - getPaddingTop() - getPaddingBottom();
            float tLeft = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight() - currentTrackHeight) / 2f;
            trackRect.set(tLeft, getPaddingTop(), tLeft + currentTrackHeight, getPaddingTop() + trackH);

            float minY = trackRect.top + safeMargin, maxY = trackRect.bottom - safeMargin;
            thumbCenterY = minY + (maxY - minY) * ratio;
            thumbCenterX = trackRect.centerX();

            float pWidth = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
            float pLeft = trackRect.left + (currentTrackHeight - pWidth) / 2f;
            float wrap = isPressedState ? (currentThumbRadius + paddingAroundThumb) : 0f;
            float bottom = (progress == maxProgress) ? trackRect.bottom - innerEdge : clamp(thumbCenterY + wrap, trackRect.top + innerEdge, trackRect.bottom - innerEdge);
            progressRect.set(pLeft, trackRect.top + innerEdge, pLeft + pWidth, bottom);

        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        float corner = currentTrackHeight / 2f;

        // 1. 轨道背景
        commonPaint.setStyle(Paint.Style.FILL);
        commonPaint.setColor(trackColor);
        commonPaint.setAlpha(255);
        canvas.drawRoundRect(trackRect, corner, corner, commonPaint);

        // 2. 进度条绘制
        if (!progressRect.isEmpty() && (progress > 0 || isPressedState)) {
            float pCorner = (orientation == ORIENTATION_HORIZONTAL) ? progressRect.height() / 2f : progressRect.width() / 2f;

            if (isPressedState) {
                // 按压模式修复:分两次绘制,先填色再描边
                commonPaint.setStyle(Paint.Style.FILL);
                commonPaint.setColor(progressColorPressed);
                commonPaint.setAlpha(255);
                canvas.drawRoundRect(progressRect, pCorner, pCorner, commonPaint);

                if (edgeWidth > 0) {
                    commonPaint.setStyle(Paint.Style.STROKE);
                    commonPaint.setStrokeWidth(edgeWidth);
                    commonPaint.setColor(edgeColorPressed);
                    canvas.drawRoundRect(progressRect, pCorner, pCorner, commonPaint);
                }
                // 还原状态
                commonPaint.setStyle(Paint.Style.FILL);
            } else if (progressDrawable != null) {
                // 默认渐变模式
                canvas.save();
                clipPath.reset();
                clipPath.addRoundRect(progressRect, pCorner, pCorner, Path.Direction.CW);
                canvas.clipPath(clipPath);

                progressDrawable.setBounds((int) progressRect.left, (int) progressRect.top, (int) progressRect.right, (int) progressRect.bottom);
                progressDrawable.draw(canvas);

                if (edgeDrawable != null && edgeWidth > 0) {
                    edgeDrawable.setBounds((int) progressRect.left, (int) progressRect.top, (int) progressRect.right, (int) progressRect.bottom);
                    edgeDrawable.draw(canvas);
                }
                canvas.restore();
            }
        }

        // 3. 滑块绘制
        if (currentThumbAlpha > 0 && currentThumbRadius > 0) {
            commonPaint.setStyle(Paint.Style.FILL);
            if (thumbDrawable != null) {
                thumbBounds.set((int) (thumbCenterX - currentThumbRadius), (int) (thumbCenterY - currentThumbRadius),
                        (int) (thumbCenterX + currentThumbRadius), (int) (thumbCenterY + currentThumbRadius));
                thumbDrawable.setBounds(thumbBounds);
                thumbDrawable.setAlpha(currentThumbAlpha);
                thumbDrawable.draw(canvas);
            } else {
                commonPaint.setColor(thumbColor);
                commonPaint.setAlpha(currentThumbAlpha);
                canvas.drawCircle(thumbCenterX, thumbCenterY, currentThumbRadius, commonPaint);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (isTouchInArea(event.getX(), event.getY())) {
                    removeCallbacks(releaseRunnable);
                    setPressedState(true);
                    updateProgressFromTouch(event.getX(), event.getY());
                    if (getParent() != null) getParent().requestDisallowInterceptTouchEvent(true);
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isPressedState) updateProgressFromTouch(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (isPressedState) {
                    if (onProgressChangeListener != null)
                        onProgressChangeListener.onStopTrackingTouch(progress);
                    postDelayed(releaseRunnable, RELEASE_DELAY_MS);
                    if (getParent() != null) getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    private void updateProgressFromTouch(float tx, float ty) {
        float safeMargin = getFixedSafeMargin();
        float minPos = (orientation == ORIENTATION_HORIZONTAL) ? trackRect.left + safeMargin : trackRect.top + safeMargin;
        float maxPos = (orientation == ORIENTATION_HORIZONTAL) ? trackRect.right - safeMargin : trackRect.bottom - safeMargin;
        float current = (orientation == ORIENTATION_HORIZONTAL) ? tx : ty;

        float ratio = (clamp(current, minPos, maxPos) - minPos) / Math.max(1f, maxPos - minPos);

        int newProgress = Math.round(ratio * maxProgress);
        if (newProgress != progress) {
            progress = clamp(newProgress, 0, maxProgress);
            updateGeometry();
            invalidate();
            performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
            if (onProgressChangeListener != null)
                onProgressChangeListener.onProgressChanged(progress);
        }
    }

    private void setPressedState(boolean pressed) {
        if (this.isPressedState == pressed) return;
        this.isPressedState = pressed;
        if (animator != null) animator.cancel();

        final float startH = currentTrackHeight, targetH = pressed ? pressedTrackHeight : baseTrackHeight;
        final int startA = currentThumbAlpha, targetA = pressed ? 255 : 0;

        animator = ValueAnimator.ofFloat(0f, 1f).setDuration(pressed ? 200 : 150);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            float f = animation.getAnimatedFraction();
            currentTrackHeight = startH + (targetH - startH) * f;
            currentThumbAlpha = isPressedState ? (int) (startA + (targetA - startA) * f) : 0;
            updateThumbRadius();
            updateGeometry();
            invalidate();
        });
        animator.start();
    }

    private boolean isTouchInArea(float x, float y) {
        return x >= (trackRect.left - touchExtension) && x <= (trackRect.right + touchExtension) &&
                y >= (trackRect.top - touchExtension) && y <= (trackRect.bottom + touchExtension);
    }

    public void setProgress(int v) {
        this.progress = clamp(v, 0, maxProgress);
        updateGeometry();
        invalidate();
    }

    private float clamp(float val, float min, float max) {
        return Math.max(min, Math.min(max, val));
    }

    private int clamp(int val, int min, int max) {
        return Math.max(min, Math.min(max, val));
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (animator != null) animator.cancel();
        removeCallbacks(releaseRunnable);
    }
}
```
```
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomSeekBar">
        <attr name="seekbar_trackColor" format="color" /><!-- 轨道背景颜色 -->
        <attr name="seekbar_progressDrawable" format="reference|color" /><!-- 进度条默认状态图片 -->
        <attr name="seekbar_progressColorPressed" format="color" /><!-- 进度条压下状态图片 -->
        <attr name="seekbar_edgeDrawable" format="reference|color" /><!-- 进度条默认状态描边图片 -->
        <attr name="seekbar_edgeColorPressed" format="color" /><!-- 进度条压下状态描边颜色 -->
        <attr name="seekbar_thumbColor" format="color" />
        <attr name="seekbar_thumbDrawable" format="reference" />
        <attr name="seekbar_trackHeight" format="dimension" /><!-- 轨道默认高度 -->
        <attr name="seekbar_edgeWidth" format="dimension" />
        <attr name="seekbar_thumbPadding" format="dimension" /><!-- 滑块padding -->
        <attr name="seekbar_progressPadding" format="dimension" /><!-- 进度条padding -->
        <attr name="seekbar_max" format="integer" />
        <attr name="seekbar_progress" format="integer" />
        <attr name="seekbar_orientation" format="enum">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
        </attr>
    </declare-styleable>
</resources>
```
```
<com.example.myapplicationwithg.CustomSeekBar
    android:id="@+id/customSeekBar99"
    android:layout_width="300dp"
    android:layout_height="64dp"
    android:layout_marginVertical="16dp"
    android:layout_marginStart="10dp"
    android:layout_marginBottom="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:seekbar_edgeColorPressed="#4d9df4"
    app:seekbar_edgeDrawable="@drawable/gradient_tri_stage"
    app:seekbar_edgeWidth="2dp"
    app:seekbar_max="100"
    app:seekbar_orientation="horizontal"
    app:seekbar_progress="50"
    app:seekbar_progressColorPressed="#3A77F0"
    app:seekbar_progressDrawable="@drawable/gradient_custom"
    app:seekbar_progressPadding="4dp"
    app:seekbar_thumbColor="#FFFFFF"
    app:seekbar_thumbPadding="4dp"
    app:seekbar_trackColor="#d0d3da"
    app:seekbar_trackHeight="36dp" />
```



`