```
```
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" />
```
```
```
`