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:
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user