Skip to content

Commit

Permalink
Refactor code
Browse files Browse the repository at this point in the history
Also changes the type for wave height animation spec from `Float` to `Dp`
  • Loading branch information
mahozad committed Jan 23, 2024
1 parent 379b4b4 commit b6c777a
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.isActive
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sin
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
Expand Down Expand Up @@ -52,7 +54,7 @@ enum class WaveMovement(internal inline val factor: (LayoutDirection) -> Int) {
* @param waveHeightAnimationSpec used for changes in wave height.
*/
data class WaveAnimationSpecs(
val waveHeightAnimationSpec: AnimationSpec<Float>
val waveHeightAnimationSpec: AnimationSpec<Dp>
)

internal val defaultIncremental = false
Expand All @@ -75,37 +77,37 @@ internal expect val KeyEvent.isPgUp: Boolean
internal expect val KeyEvent.isPgDn: Boolean

@Composable
internal inline fun animatePhaseShiftPx(
waveLengthPx: Float,
internal inline fun animatePhaseShift(
waveLength: Dp,
wavePeriod: Duration,
waveMovement: WaveMovement
): State<Float> {
val shift = waveLengthPx * waveMovement.factor(LocalLayoutDirection.current)
val phaseShiftPxAnimated = remember { mutableFloatStateOf(0f) }
val phaseShiftPxAnimation = remember(shift, wavePeriod) {
): State<Dp> {
val shift = waveLength * waveMovement.factor(LocalLayoutDirection.current)
val phaseShiftAnimated = remember { mutableStateOf(0.dp) }
val phaseShiftAnimation = remember(shift, wavePeriod) {
val wavePeriodAdjusted = wavePeriod.toAdjustedMilliseconds()
val shiftAdjusted = if (wavePeriodAdjusted == Int.MAX_VALUE) 0f else shift
val shiftAdjusted = if (wavePeriodAdjusted == Int.MAX_VALUE) 0.dp else shift
TargetBasedAnimation(
animationSpec = infiniteRepeatable(
animation = tween(wavePeriodAdjusted, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
typeConverter = Float.VectorConverter,
// Instead of simply 0 and shift, they are added to current phaseShiftPxAnimated to
typeConverter = Dp.VectorConverter,
// Instead of simply 0 and shift, they are added to current phaseShiftAnimated to
// smoothly continue the wave shift when wavePeriod or waveMovement is changed
initialValue = 0 + phaseShiftPxAnimated.value,
targetValue = shiftAdjusted + phaseShiftPxAnimated.value
initialValue = 0.dp + phaseShiftAnimated.value,
targetValue = shiftAdjusted + phaseShiftAnimated.value
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(phaseShiftPxAnimation) {
LaunchedEffect(phaseShiftAnimation) {
val startTime = withFrameNanos { it }
while (isActive) {
playTime = withFrameNanos { it } - startTime
phaseShiftPxAnimated.value = phaseShiftPxAnimation.getValueFromNanos(playTime)
phaseShiftAnimated.value = phaseShiftAnimation.getValueFromNanos(playTime)
}
}
return phaseShiftPxAnimated
return phaseShiftAnimated
}

private inline fun Duration.toAdjustedMilliseconds() = this
Expand All @@ -117,54 +119,54 @@ private inline fun Duration.toAdjustedMilliseconds() = this
?: Int.MAX_VALUE

@Composable
internal inline fun animateWaveHeightPx(
waveHeightPx: Float,
animationSpec: AnimationSpec<Float>
): State<Float> = animateFloatAsState(
targetValue = waveHeightPx,
internal inline fun animateWaveHeight(
waveHeight: Dp,
animationSpec: AnimationSpec<Dp>
): State<Dp> = animateDpAsState(
targetValue = waveHeight,
animationSpec = animationSpec
)

internal inline fun DrawScope.drawTrack(
sliderStart: Offset,
sliderValueOffset: Offset,
sliderEnd: Offset,
waveLengthPx: Float,
waveHeightPx: Float,
waveThicknessPx: Float,
trackThicknessPx: Float,
phaseShiftPx: Float,
waveLength: Dp,
waveHeight: Dp,
waveThickness: Dp,
trackThickness: Dp,
phaseShift: Dp,
incremental: Boolean,
inactiveTrackColor: Color,
activeTrackColor: Color
) {
drawTrackActivePart(
startOffset = sliderStart,
valueOffset = sliderValueOffset,
waveLengthPx = waveLengthPx,
waveHeightPx = waveHeightPx,
waveThicknessPx = waveThicknessPx,
phaseShiftPx = phaseShiftPx,
waveLength = waveLength,
waveHeight = waveHeight,
waveThickness = waveThickness,
phaseShift = phaseShift,
incremental = incremental,
color = activeTrackColor
)
drawTrackInactivePart(
color = inactiveTrackColor,
thicknessPx = trackThicknessPx,
thickness = trackThickness,
startOffset = sliderValueOffset,
endOffset = sliderEnd,
)
}

private inline fun DrawScope.drawTrackInactivePart(
color: Color,
thicknessPx: Float,
thickness: Dp,
startOffset: Offset,
endOffset: Offset
) {
if (thicknessPx <= 0f) return
if (thickness <= 0.dp) return
drawLine(
strokeWidth = thicknessPx,
strokeWidth = thickness.toPx(),
color = color,
start = startOffset,
end = endOffset,
Expand All @@ -175,20 +177,23 @@ private inline fun DrawScope.drawTrackInactivePart(
private inline fun DrawScope.drawTrackActivePart(
startOffset: Offset,
valueOffset: Offset,
waveLengthPx: Float,
waveHeightPx: Float,
waveThicknessPx: Float,
phaseShiftPx: Float,
waveLength: Dp,
waveHeight: Dp,
waveThickness: Dp,
phaseShift: Dp,
incremental: Boolean,
color: Color
) {
if (waveThicknessPx <= 0f) return
if (waveThickness <= 0.dp) return
val wave = Path().apply {
if (waveLengthPx == 0f || waveHeightPx == 0f) {
if (waveLength <= 0.dp || waveHeight == 0.dp) {
moveTo(startOffset.x, center.y)
lineTo(valueOffset.x, center.y)
return@apply
}
val waveHeightPx = waveHeight.toPx().absoluteValue
val phaseShiftPx = phaseShift.toPx()
val waveLengthPx = waveLength.toPx()
val startHeightFactor = if (incremental) 0f else 1f
val startRadians = (startOffset.x + phaseShiftPx) % waveLengthPx / waveLengthPx * (2 * PI)
val startY = (sin(startRadians) * startHeightFactor * (waveHeightPx / 2)) + (size.height / 2)
Expand All @@ -209,7 +214,7 @@ private inline fun DrawScope.drawTrackActivePart(
path = wave,
color = color,
style = Stroke(
width = waveThicknessPx,
width = waveThickness.toPx(),
join = StrokeJoin.Round,
cap = StrokeCap.Round
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import ir.mahozad.multiplatform.wavyslider.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.math.*
import kotlin.ranges.coerceAtLeast
import kotlin.time.Duration

private val ThumbRadius = 10.dp
Expand Down Expand Up @@ -371,36 +370,22 @@ private fun Track(
) {
val inactiveTrackColor = colors.trackColor(enabled, active = false)
val activeTrackColor = colors.trackColor(enabled, active = true)
val waveLengthPx: Float
val waveHeightPx: Float
val waveThicknessPx: Float
val trackThicknessPx: Float
val density = LocalDensity.current
with(density) {
waveLengthPx = waveLength.coerceAtLeast(0.dp).toPx()
waveHeightPx = waveHeight.toPx().absoluteValue
waveThicknessPx = waveThickness.toPx()
trackThicknessPx = trackThickness.toPx()
}
val phaseShiftPxAnimated by animatePhaseShiftPx(waveLengthPx, wavePeriod, waveMovement)
val waveHeightPxAnimated by animateWaveHeightPx(waveHeightPx, animationSpecs.waveHeightAnimationSpec)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(max(with(density) { waveHeightPxAnimated.toDp() + waveThickness }, /*thumbSize*/ThumbRadius * 2))
) {
val phaseShiftAnimated by animatePhaseShift(waveLength, wavePeriod, waveMovement)
val waveHeightAnimated by animateWaveHeight(waveHeight, animationSpecs.waveHeightAnimationSpec)
val trackHeight = max(waveHeightAnimated + waveThickness, ThumbRadius * 2)
Canvas(modifier = Modifier.fillMaxWidth().height(trackHeight)) {
val isRtl = layoutDirection == LayoutDirection.Rtl
val sliderLeft = Offset(thumbPx, center.y)
val sliderRight = Offset(size.width - thumbPx, center.y)
val sliderStart = if (isRtl) sliderRight else sliderLeft
val sliderEnd = if (isRtl) sliderLeft else sliderRight
val sliderValueOffset = Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionEnd, center.y)
drawTrack(
waveLengthPx = waveLengthPx,
waveHeightPx = waveHeightPxAnimated,
phaseShiftPx = phaseShiftPxAnimated,
waveThicknessPx = waveThicknessPx,
trackThicknessPx = trackThicknessPx,
waveLength = waveLength,
waveHeight = waveHeightAnimated,
phaseShift = phaseShiftAnimated,
waveThickness = waveThickness,
trackThickness = trackThickness,
sliderValueOffset = sliderValueOffset,
sliderStart = sliderStart,
sliderEnd = sliderEnd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.disabled
Expand Down Expand Up @@ -90,41 +89,27 @@ fun SliderDefaults.Track(
incremental: Boolean = SliderDefaults.Incremental,
animationSpecs: WaveAnimationSpecs = SliderDefaults.WaveAnimationSpecs
) {
// Because trackColor() function is an internal member in Material library
// @Suppress("INVISIBLE_MEMBER") is required to be able to access and use
// trackColor() function which is marked internal in Material library
// See https://stackoverflow.com/q/62500464/8583692
val inactiveTrackColor = @Suppress("INVISIBLE_MEMBER") colors.trackColor(enabled, active = false)
val activeTrackColor = @Suppress("INVISIBLE_MEMBER") colors.trackColor(enabled, active = true)
val waveLengthPx: Float
val waveHeightPx: Float
val waveThicknessPx: Float
val trackThicknessPx: Float
val density = LocalDensity.current
with(density) {
waveLengthPx = waveLength.coerceAtLeast(0.dp).toPx()
waveHeightPx = waveHeight.toPx().absoluteValue
waveThicknessPx = waveThickness.toPx()
trackThicknessPx = trackThickness.toPx()
}
val phaseShiftPxAnimated by animatePhaseShiftPx(waveLengthPx, wavePeriod, waveMovement)
val waveHeightPxAnimated by animateWaveHeightPx(waveHeightPx, animationSpecs.waveHeightAnimationSpec)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(max(with(density) { waveHeightPxAnimated.toDp() + waveThickness }, ThumbSize.height))
) {
val phaseShiftAnimated by animatePhaseShift(waveLength, wavePeriod, waveMovement)
val waveHeightAnimated by animateWaveHeight(waveHeight, animationSpecs.waveHeightAnimationSpec)
val trackHeight = max(waveHeightAnimated + waveThickness, ThumbSize.height)
Canvas(modifier = Modifier.fillMaxWidth().height(trackHeight)) {
val isRtl = layoutDirection == LayoutDirection.Rtl
val sliderLeft = Offset(0f, center.y)
val sliderRight = Offset(size.width, center.y)
val sliderStart = if (isRtl) sliderRight else sliderLeft
val sliderEnd = if (isRtl) sliderLeft else sliderRight
val sliderValueOffset =
Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.endInclusive, center.y)
val sliderValueOffset = Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.endInclusive, center.y)
drawTrack(
waveLengthPx = waveLengthPx,
waveHeightPx = waveHeightPxAnimated,
phaseShiftPx = phaseShiftPxAnimated,
waveThicknessPx = waveThicknessPx,
trackThicknessPx = trackThicknessPx,
waveLength = waveLength,
waveHeight = waveHeightAnimated,
phaseShift = phaseShiftAnimated,
waveThickness = waveThickness,
trackThickness = trackThickness,
sliderValueOffset = sliderValueOffset,
sliderStart = sliderStart,
sliderEnd = sliderEnd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -832,14 +833,14 @@ class VisualTest {
@OptIn(ExperimentalComposeUiApi::class)
@Test
fun `Test 39`() {
val spec1 = tween<Float>(durationMillis = 1300, easing = EaseOutBounce)
val spec2 = tween<Float>(durationMillis = 150, easing = LinearEasing)
val spec1 = tween<Dp>(durationMillis = 1300, easing = EaseOutBounce)
val spec2 = tween<Dp>(durationMillis = 150, easing = LinearEasing)
val isPassed = testApp(
name = object {}.javaClass.enclosingMethod.name,
given = "Different animationSpecs for wave height when dragging vs when toggling wave height",
expected = "Should stop the wave horizontal movement"
) { value, onChange ->
var spec: AnimationSpec<Float> by remember { mutableStateOf(spec1) }
var spec: AnimationSpec<Dp> by remember { mutableStateOf(spec1) }
var waveHeight by remember { mutableStateOf(16.dp) }
val interactionSource = remember { MutableInteractionSource() }
var isPressed by remember { mutableStateOf(false) }
Expand Down

0 comments on commit b6c777a

Please sign in to comment.