ITmob-Ly
发布于 2025-08-07 / 4 阅读
0

Jetpack Compose 为音频播放的滑动进度条(Slider)实现正在缓冲的脉动/脉冲效果

在实现音频播放的进度进度条时,当用户滑动进度条选择播放进度时,如果音频还未加载成功,需要在播放进度上实现一个正在缓冲的脉动效果来表达音频正在加载/缓冲,我们的项目使用的是 Jetpack Compose,该如何实现呢?

先来看下最终要实现的效果:

track - the track to be displayed on the slider, it is placed underneath the thumb. The lambda receives a SliderState which is used to obtain the current active track.

上面是 Slider 源码中 track 参数的注释:Slider 上显示的轨迹,它位于滑块下方。

所以要实现我们想要的效果,可用为 track 可组合项添加一个绘制脉动动画的 Modifier 即可。

不多述了,下面是实现带有脉动效果的 Slider/滑动条,在需要缓冲时控制脉动动画的显示即可:

package cn.itmob.composable

// ----------- START - PulsatingSlider -----------

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PulsatingSlider() {
    val sliderState = remember {
        SliderState(
            value = 0.5F,
            valueRange = 0.0F..1.0F,
        )
    }

    val interactionSource = remember { MutableInteractionSource() }

    Slider(
        modifier = Modifier.padding(horizontal = 16.dp).height(8.dp),
        state = sliderState,
        thumb = {
            SliderDefaults.Thumb(
                modifier = Modifier.clip(CircleShape),
                interactionSource = interactionSource,
                thumbSize = DpSize(16.dp, 16.dp),
            )
        },
        track = {
            SliderDefaults.Track(
                modifier = Modifier.height(8.dp)
                    .pulsatingEffect(
                        thumbOffsetFloat = sliderState.value,
                        enable = true,
                        color = Color.Gray,
                        ),
                sliderState = sliderState,
                thumbTrackGapSize = 0.dp,
                drawStopIndicator = {}
            )
        }
    )
}

fun Modifier.pulsatingEffect(
    thumbOffsetFloat: Float,
    color: Color = Color.Gray,
    enable: Boolean,
): Modifier = composed {
    var trackWidth by remember { mutableFloatStateOf(0f) }
    val thumbOffset by remember(thumbOffsetFloat, trackWidth) {
        mutableFloatStateOf(trackWidth * thumbOffsetFloat)
    }

    val transition = rememberInfiniteTransition(label = "pulsating animation")
    val pulsatingAnimateFloat by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 800,
                delayMillis = 200,
            )
        ), label = "pulsating animation"
    )

    this then Modifier
        .onGloballyPositioned { coordinates ->
            trackWidth = coordinates.size.width.toFloat()
        }
        .drawWithContent {
            drawContent()

            val strokeWidth = size.height
            val strokeOffsetY = size.height / 2f
            val startOffsetX = thumbOffset
            val endOffsetX = thumbOffset + pulsatingAnimateFloat * (trackWidth - thumbOffset)
            val alpha = (1f - pulsatingAnimateFloat).coerceIn(0f, 1f)

            if (enable) {
                drawLine(
                    color = color.copy(alpha = alpha),
                    start = Offset(startOffsetX, strokeOffsetY),
                    end = Offset(endOffsetX, strokeOffsetY),
                    strokeWidth = strokeWidth,
                    cap = StrokeCap.Round,
                )
            }
        }
}

// ----------- END - PulsatingSlider -------------