SeekBar

9 阅读3分钟
```
```
package com.example.myapplicationwithg

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.withStyledAttributes
import kotlin.math.max

class CustomSeekBar3 @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    companion object {
        private const val SCALE_FACTOR = 1.5f // 缩放倍率
    }

    // --- 核心尺寸参数 ---
    private var userSetTrackHeight = -1f
    private var baseTrackHeight = 0f
    private var edgeWidth = 0f
    private var paddingAroundThumb = 0f
    private var trackProgressGap = 0f

    // --- 颜色 ---
    private var trackColor = Color.LTGRAY
    private var progressColor = Color.WHITE
    private var progressColorPressed = Color.BLUE
    private var thumbColor = Color.WHITE
    private var edgeColor = Color.BLACK

    // --- 状态与数据 ---
    private var isPressedState = false
    private var progress = 50
    private var maxProgress = 100

    // --- 绘制对象 (复用以优化性能) ---
    private val trackRect = RectF()
    private val progressRect = RectF()
    private val commonPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
    private val edgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE }

    // --- 动画与动态值 ---
    private var trackHeightAnimator: ValueAnimator? = null
    private var currentTrackHeight = 0f
    private var currentThumbAlpha = 0
    private var currentThumbRadius = 0f
    private var pressedTrackHeight = 0f

    // --- 缓存计算值 ---
    private var thumbCenterX = 0f
    private var usableTrackLeft = 0f
    private var usableTrackRight = 0f

    private val touchExtension = 20f * resources.displayMetrics.density

    var onProgressChangeListener: ((Int) -> Unit)? = null

    init {
        context.withStyledAttributes(attrs, R.styleable.CustomSeekBar2) {
            trackColor = getColor(R.styleable.CustomSeekBar2_csb_trackColor, Color.LTGRAY)
            progressColor = getColor(R.styleable.CustomSeekBar2_csb_progressColor, Color.WHITE)
            progressColorPressed =
                getColor(R.styleable.CustomSeekBar2_csb_progressColorPressed, Color.BLUE)
            thumbColor = getColor(R.styleable.CustomSeekBar2_csb_thumbColor, Color.WHITE)
            edgeColor = getColor(R.styleable.CustomSeekBar2_csb_edgeColor, Color.BLACK)

            if (hasValue(R.styleable.CustomSeekBar2_csb_trackHeight)) {
                userSetTrackHeight = getDimension(R.styleable.CustomSeekBar2_csb_trackHeight, 0f)
            }

            edgeWidth = getDimension(
                R.styleable.CustomSeekBar2_csb_edgeWidth,
                2f * resources.displayMetrics.density
            )
            paddingAroundThumb = getDimension(
                R.styleable.CustomSeekBar2_csb_paddingAroundThumb,
                2f * resources.displayMetrics.density
            )
            trackProgressGap = getDimension(
                R.styleable.CustomSeekBar2_csb_trackProgressGap,
                5f * resources.displayMetrics.density
            )

            maxProgress = getInt(R.styleable.CustomSeekBar2_csb_max, 100)
            progress = getInt(R.styleable.CustomSeekBar2_csb_progress, 50)
        }
        edgePaint.color = edgeColor
        isClickable = true
        isFocusable = true
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (w > 0 && h > 0) {
            calculateSizes(w, h)
            updateGeometry()
        }
    }

    private fun calculateSizes(width: Int, height: Int) {
        val availableHeight = (height - paddingTop - paddingBottom).toFloat()

        if (userSetTrackHeight >= 0) {
            baseTrackHeight = userSetTrackHeight
            pressedTrackHeight = (baseTrackHeight * SCALE_FACTOR).coerceAtMost(availableHeight)
        } else {
            // 如果用户通过 layout_height 设置,按下填满,平时为 1/SCALE_FACTOR
            pressedTrackHeight = availableHeight
            baseTrackHeight = availableHeight / SCALE_FACTOR
        }

        currentTrackHeight = if (isPressedState) pressedTrackHeight else baseTrackHeight
        updateThumbRadius()
    }

    /**
     * 核心修复:根据当前进度条的高度动态计算滑块半径
     * 确保滑块与进度条边缘的间距始终等于 paddingAroundThumb
     */
    private fun updateThumbRadius() {
        val currentProgressHeight =
            (currentTrackHeight - 2 * trackProgressGap - edgeWidth).coerceAtLeast(1f)
        // 半径 = (高度 / 2) - 间距
        currentThumbRadius = (currentProgressHeight / 2f - paddingAroundThumb).coerceAtLeast(2f)
    }

    private fun updateGeometry() {
        val w = width.toFloat()
        val h = height.toFloat()
        if (w <= 0 || h <= 0) return

        // 1. 计算轨道矩形 (居中)
        val availableWidth = w - paddingStart - paddingEnd - currentThumbRadius * 2
        val trackTop = paddingTop + (h - paddingTop - paddingBottom - currentTrackHeight) / 2f

        trackRect.set(
            paddingStart + currentThumbRadius,
            trackTop,
            paddingStart + currentThumbRadius + availableWidth,
            trackTop + currentTrackHeight
        )

        // 2. 计算可用滑动区域
        usableTrackLeft = trackRect.left + trackProgressGap + edgeWidth / 2f
        usableTrackRight = trackRect.right - trackProgressGap - edgeWidth / 2f
        val usableTrackWidth = usableTrackRight - usableTrackLeft

        // 3. 计算滑块中心位置
        thumbCenterX = if (usableTrackWidth > 0) {
            usableTrackLeft + usableTrackWidth * progress / maxProgress
        } else usableTrackLeft

        // 限制中心,确保不超出进度条圆角范围
        val minThumbCenter = usableTrackLeft + paddingAroundThumb + currentThumbRadius
        val maxThumbCenter = usableTrackRight - paddingAroundThumb - currentThumbRadius

        if (minThumbCenter <= maxThumbCenter) {
            thumbCenterX = thumbCenterX.coerceIn(minThumbCenter, maxThumbCenter)
        }

        // 4. 计算进度条矩形
        val progressHeight =
            (currentTrackHeight - 2 * trackProgressGap - edgeWidth).coerceAtLeast(1f)
        val progressTop = trackRect.top + (currentTrackHeight - progressHeight) / 2f

        // 按下时进度条右侧包裹住滑块
        val visualProgressRight = if (isPressedState && currentThumbAlpha > 0) {
            thumbCenterX + currentThumbRadius + paddingAroundThumb
        } else {
            if (usableTrackWidth > 0) usableTrackLeft + usableTrackWidth * progress / maxProgress
            else usableTrackLeft
        }.coerceIn(usableTrackLeft, usableTrackRight)

        progressRect.set(
            usableTrackLeft,
            progressTop,
            visualProgressRight,
            progressTop + progressHeight
        )
    }

    override fun onDraw(canvas: Canvas) {
        // 1. 轨道
        commonPaint.color = trackColor
        commonPaint.alpha = 255
        val trackCorner = currentTrackHeight / 2f
        canvas.drawRoundRect(trackRect, trackCorner, trackCorner, commonPaint)

        // 2. 进度条
        if (progressRect.width() > 0) {
            commonPaint.color = if (isPressedState) progressColorPressed else progressColor
            val progressCorner = progressRect.height() / 2f
            canvas.drawRoundRect(progressRect, progressCorner, progressCorner, commonPaint)

            edgePaint.strokeWidth = edgeWidth
            canvas.drawRoundRect(progressRect, progressCorner, progressCorner, edgePaint)
        }

        // 3. 滑块
        if (currentThumbAlpha > 0) {
            commonPaint.color = thumbColor
            commonPaint.alpha = currentThumbAlpha

            // 按下时滑块紧贴进度条右缘,否则位于计算出的中心
            val drawX = if (isPressedState) {
                progressRect.right - currentThumbRadius - paddingAroundThumb
            } else thumbCenterX

            canvas.drawCircle(drawX, progressRect.centerY(), currentThumbRadius, commonPaint)
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                parent?.requestDisallowInterceptTouchEvent(true)
                if (isTouchInArea(x, y)) {
                    setPressedState(true)
                    updateProgressFromTouch(x)
                    return true
                }
            }

            MotionEvent.ACTION_MOVE -> {
                if (isPressedState) {
                    updateProgressFromTouch(x)
                    return true
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (isPressedState) {
                    setPressedState(false)
                    parent?.requestDisallowInterceptTouchEvent(false)
                    return true
                }
            }
        }
        return super.onTouchEvent(event)
    }

    private fun isTouchInArea(x: Float, y: Float): Boolean {
        return x >= trackRect.left - touchExtension &&
                x <= trackRect.right + touchExtension &&
                y >= 0 && y <= height
    }

    private fun updateProgressFromTouch(touchX: Float) {
        val minCenter = usableTrackLeft + paddingAroundThumb + currentThumbRadius
        val maxCenter = usableTrackRight - paddingAroundThumb - currentThumbRadius
        val effectiveWidth = maxCenter - minCenter

        val newProgress = if (effectiveWidth > 0) {
            val clampedX = touchX.coerceIn(minCenter, maxCenter)
            ((clampedX - minCenter) / effectiveWidth * maxProgress).toInt()
        } else {
            if (touchX <= usableTrackLeft) 0 else maxProgress
        }

        val clampedProgress = newProgress.coerceIn(0, maxProgress)
        if (clampedProgress != progress) {
            progress = clampedProgress
            updateGeometry()
            invalidate()
            performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
            onProgressChangeListener?.invoke(progress)
        }
    }

    private fun setPressedState(pressed: Boolean) {
        if (isPressedState == pressed) return
        isPressedState = pressed
        animateTrackHeight(pressed)
        currentThumbAlpha = if (pressed) 255 else 0
        updateThumbRadius()
        updateGeometry()
        invalidate()
    }

    private fun animateTrackHeight(toPressed: Boolean) {
        trackHeightAnimator?.cancel()
        val target = if (toPressed) pressedTrackHeight else baseTrackHeight
        trackHeightAnimator = ValueAnimator.ofFloat(currentTrackHeight, target).apply {
            duration = 200
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener {
                currentTrackHeight = it.animatedValue as Float
                updateThumbRadius() // 高度变化时实时重新计算滑块半径
                updateGeometry()
                invalidate()
            }
            start()
        }
    }

    fun setProgress(value: Int) {
        progress = value.coerceIn(0, maxProgress)
        updateGeometry()
        invalidate()
    }

    fun setMax(value: Int) {
        maxProgress = max(value, 1)
        if (progress > maxProgress) progress = maxProgress
        updateGeometry()
        invalidate()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        trackHeightAnimator?.cancel()
    }
}
```
```
```

```
<declare-styleable name="CustomSeekBar2">
    <attr name="csb_trackHeight" format="dimension" />
    <attr name="csb_trackColor" format="color" />
    <attr name="csb_progressColor" format="color" />
    <attr name="csb_progressColorPressed" format="color" />
    <attr name="csb_thumbRadius" format="dimension" />
    <attr name="csb_thumbColor" format="color" />
    <attr name="csb_edgeWidth" format="dimension" />
    <attr name="csb_edgeColor" format="color" />
    <attr name="csb_paddingAroundThumb" format="dimension" />
    <attr name="csb_cornerRadius" format="dimension" />
    <attr name="csb_trackProgressGap" format="dimension" />
    <attr name="csb_max" format="integer" />
    <attr name="csb_progress" format="integer" />
</declare-styleable>
```


```
```
```
<com.example.myapplicationwithg.CustomSeekBar3
    android:id="@+id/customSeekBar3"
    android:layout_width="300dp"
    android:layout_height="30dp"
    android:layout_marginVertical="16dp"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="50dp"
    android:background="#00FF00"
    app:csb_edgeColor="#000000"
    app:csb_max="100"
    app:csb_progress="50"
    app:csb_paddingAroundThumb="4dp"
    app:csb_progressColor="#FFFFFF"
    app:csb_progressColorPressed="#2196F3"
    app:csb_thumbColor="#FFFFFF"
    app:csb_trackColor="#CCCCCC"
    app:csb_trackHeight="20dp"
    app:csb_trackProgressGap="5dp"
    app:layout_constraintLeft_toLeftOf="parent" />
```
```
```



`