Introduce RollingNumberPlaceholder (#1750)

* Introduce RollingNumberPlaceholder, sync page index double end style

* Cleanup

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
renovate[bot]andDude so hot authored and GitHub committed 2024-10-17 19:27:41 +08:00
1 parent 302b43c9e8
commit 34de943c9f
2 files changed
+75 -36

No files matched your search

@@ -1,6 +1,5 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.animation.core.animateIntSize
import androidx.compose.animation.core.animateOffset
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Arrangement
@@ -8,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -22,11 +22,13 @@ import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.util.unsafeLazy
import androidx.compose.ui.unit.toSize
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.cos
@@ -34,6 +36,51 @@ import kotlin.math.floor
import kotlin.math.max
import kotlin.math.sin
@Composable
fun rememberTextStyleNumberMaxSize(textStyle: TextStyle): DpSize {
val textMeasurer = rememberTextMeasurer()
val density = LocalDensity.current
return remember(textStyle, textMeasurer, density) {
with(density) {
"0123456789".maxOfWith({ (aw, _), (bw, _) -> aw - bw }) { char ->
textMeasurer.measure("$char", textStyle).size
}.toSize().toDpSize()
}
}
}
@Composable
fun extractSpacingFromTextStyle(style: TextStyle): Pair<TextStyle, Dp> {
val density = LocalDensity.current
return remember(density, style) {
if (style.letterSpacing != TextUnit.Unspecified) {
val spacing = with(density) { style.letterSpacing.toDp() }
style.copy(letterSpacing = TextUnit.Unspecified) to spacing
} else {
style to 0.dp
}
}
}
@Composable
fun RollingNumberPlaceholder(number: Int, style: TextStyle = LocalTextStyle.current) {
val (styleNoSpacing, spacing) = extractSpacingFromTextStyle(style)
val size = rememberTextStyleNumberMaxSize(styleNoSpacing)
Row(
modifier = Modifier.padding(horizontal = spacing),
horizontalArrangement = Arrangement.spacedBy(spacing, Alignment.CenterHorizontally),
) {
"$number".forEach { char ->
Text(
text = "$char",
style = styleNoSpacing,
textAlign = TextAlign.Center,
modifier = Modifier.size(size),
)
}
}
}
// We have three space to make animation looks continuous
// 1. Concept space, where we have elements 1 2 3 4 5 6 7 8 9 0 null
// 0, 1, null is connected and the have same norm(distance) to each other
@@ -88,37 +135,26 @@ private fun intermediateToNumberOffset(circleOffset: Offset): Float {
private fun normalize(degree: Float) = (degree - 2 * PI * floor(degree / (2 * PI))).toFloat()
// Convert 2d euclidean space partial circle to UI Offset
private fun numberOffsetToUIOffset(size: IntSize, number: Float) = IntOffset(0, -(size.height * 2 * number).toInt())
private fun numberOffsetToUIOffset(heightPx: Int, number: Float) = IntOffset(0, -(heightPx * 2 * number).toInt())
@Composable
fun RollingNumber(number: Int, style: TextStyle = LocalTextStyle.current, width: Int? = null) {
fun RollingNumber(number: Int, style: TextStyle = LocalTextStyle.current, length: Int? = null) {
val string = remember(number) { "$number" }
val transition = updateTransition(string)
val textMeasurer = rememberTextMeasurer()
val size by transition.animateIntSize { str ->
remember(str) { textMeasurer.measure(str, style).size }
}
val density = LocalDensity.current
val (styleNoSpacing, spacing) = remember(density, style) {
if (style.letterSpacing != TextUnit.Unspecified) {
val spacing = with(density) { style.letterSpacing.toDp() }
style.copy(letterSpacing = TextUnit.Unspecified) to spacing
} else {
style to 0.dp
}
}
val (styleNoSpacing, spacing) = extractSpacingFromTextStyle(style)
val size = rememberTextStyleNumberMaxSize(styleNoSpacing)
Row(
modifier = Modifier.clipToBounds().layout { measurable, constraints ->
modifier = Modifier.clipToBounds().padding(horizontal = spacing).layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(width = placeable.width, height = size.height) {
layout(width = placeable.width, height = size.height.roundToPx()) {
placeable.place(0, 0)
}
}.padding(horizontal = spacing),
horizontalArrangement = Arrangement.spacedBy(spacing, Alignment.End),
},
horizontalArrangement = Arrangement.spacedBy(spacing, Alignment.CenterHorizontally),
) {
val content = remember {
val meta = @Composable { reversed: Int ->
val max = width ?: with(transition) { max(currentState.length, targetState.length) }
val max = length ?: with(transition) { max(currentState.length, targetState.length) }
val rotate by transition.animateOffset { str ->
val len = str.length
val absent = max - len
@@ -129,36 +165,38 @@ fun RollingNumber(number: Int, style: TextStyle = LocalTextStyle.current, width:
Column(
modifier = Modifier.offset {
val number = intermediateToNumberOffset(rotate)
numberOffsetToUIOffset(size, number)
numberOffsetToUIOffset(size.height.roundToPx(), number)
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
repeat(10) { i ->
Text(
text = "$i",
modifier = Modifier.size(size),
style = styleNoSpacing,
textAlign = TextAlign.Center,
)
Text(
text = "",
modifier = Modifier.size(size),
style = styleNoSpacing,
textAlign = TextAlign.Center,
)
}
}
}
(0 until MAX_NUMBER).map {
unsafeLazy {
if (width != null) {
{ meta(it) }
} else {
movableContentOf { meta(it) }
}
if (length != null) {
{ meta(it) }
} else {
movableContentOf { meta(it) }
}
}
}
val max = width ?: with(transition) { max(currentState.length, targetState.length) }
val max = length ?: with(transition) { max(currentState.length, targetState.length) }
check(max <= MAX_NUMBER)
repeat(max) { index ->
content[max - index - 1].value()
content[max - index - 1]()
}
}
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.hippo.ehviewer.ui.main.RollingNumber
import com.hippo.ehviewer.ui.main.RollingNumberPlaceholder
@Composable
fun PageIndicatorText(
@@ -38,16 +39,16 @@ fun PageIndicatorText(
Row(modifier = modifier, horizontalArrangement = Arrangement.Center) {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
RollingNumber(number = currentPage, style = strokeStyle, width = total.length)
RollingNumber(number = currentPage, style = style, width = total.length)
RollingNumber(number = currentPage, style = strokeStyle, length = total.length)
RollingNumber(number = currentPage, style = style, length = total.length)
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
Text(text = " / ", style = strokeStyle)
Text(text = " / ", style = style)
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
Text(text = total, style = strokeStyle)
Text(text = total, style = style)
RollingNumberPlaceholder(number = totalPages, style = strokeStyle)
RollingNumberPlaceholder(number = totalPages, style = style)
}
}
}