Skip to content

Commit

Permalink
Refactor code
Browse files Browse the repository at this point in the history
Also change the type for wave height animation spec from `Float` to `Dp`
  • Loading branch information
mahozad committed Jan 23, 2024
1 parent 379b4b4 commit 2264f63
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,41 @@ 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

/**
* The horizontal movement of the whole wave.
*/
enum class WaveMovement(internal inline val factor: (LayoutDirection) -> Int) {
enum class WaveMovement(internal inline val factor: (LayoutDirection) -> Float) {
/**
* Always move from right to left (regardless of layout direction).
*/
RTL({ 1 }),
RTL({ 1f }),
/**
* Always move from left to right (regardless of layout direction).
*/
LTR({ -1 }),
LTR({ -1f }),
/**
* Move away from the thumb (depends on layout direction).
*/
BACKWARD({ if (it == LayoutDirection.Ltr) 1 else -1 }),
BACKWARD({ if (it == LayoutDirection.Ltr) 1f else -1f }),
/**
* Move toward the thumb (depends on layout direction).
*/
FORWARD({ if (it == LayoutDirection.Ltr) -1 else 1 }),
FORWARD({ if (it == LayoutDirection.Ltr) -1f else 1f }),
/**
* Do not move.
*/
STOPPED({ 0 })
STOPPED({ 0f })
}

/**
Expand All @@ -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,96 +77,96 @@ 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
.absoluteValue
.inWholeMilliseconds
.coerceAtMost(Int.MAX_VALUE.toLong())
.toInt() // Do not call before coercion
.toInt() // Do not call this before coercion
.takeIf { it != 0 }
?: 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 phaseShiftPx = phaseShift.toPx()
val waveLengthPx = waveLength.toPx()
val waveHeightPx = waveHeight.toPx().absoluteValue
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 2264f63

Please sign in to comment.