diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt index 2dcee326c..12e6b4fac 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt @@ -26,9 +26,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView -import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -62,7 +60,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -80,6 +77,7 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import androidx.compose.ui.util.lerp import androidx.core.text.inSpans import androidx.core.text.parseAsHtml @@ -101,19 +99,16 @@ import com.hippo.ehviewer.ui.main.GalleryCommentCard import com.hippo.ehviewer.ui.openBrowser import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs import com.hippo.ehviewer.ui.tools.LocalDialogState +import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.ehviewer.util.ReadableTime import com.hippo.ehviewer.util.TextUrl import com.hippo.ehviewer.util.addTextToClipboard import com.hippo.ehviewer.util.findActivity import com.hippo.ehviewer.util.getParcelableCompat -import com.hippo.ehviewer.util.isAtLeastU import com.ramcosta.composedestinations.annotation.Destination import eu.kanade.tachiyomi.util.lang.launchIO -import kotlin.math.max -import kotlin.math.min import kotlin.math.roundToInt -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching import rikka.core.res.resolveColor @@ -151,6 +146,8 @@ private fun Context.generateComment( return TextUrl.handleTextUrl(ssb) } +private val MiniumContentPaddingEditText = 88.dp + @Destination @Composable fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController) { @@ -158,30 +155,9 @@ fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController val dialogState = LocalDialogState.current val coroutineScope = rememberCoroutineScope() - // Predictive animation pattern, extract it out if needed - // Principal Value - var commenting by rememberSaveable { mutableStateOf(false) } - // Natural animator state correspond with principal value - val coState by animateFloatAsState( - targetValue = if (commenting) 1f else 0f, - label = "animationProgress", - ) - // User predictive back animation progress holder && value correspond with UI State - var animationProgress by remember { mutableFloatStateOf(0F) } - // Update UI animation state - animationProgress = if (commenting || !isAtLeastU) coState else min(animationProgress, coState) - - PredictiveBackHandler(commenting) { progress -> - try { - progress.collect { - animationProgress = 1F - it.progress - } - commenting = false - } catch (e: CancellationException) { - commenting = true - animationProgress = 1F - } - } + val commentingBackField = rememberSaveable { mutableStateOf(false) } + var commenting by commentingBackField + val animationProgress by animateFloatMergePredictiveBackAsState(enable = commentingBackField) var userComment by rememberSaveable { mutableStateOf("") } var comments by rememberSaveable { mutableStateOf(galleryDetail.comments) } @@ -285,15 +261,15 @@ fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController }, ) { paddingValues -> val keylineMargin = dimensionResource(id = R.dimen.keyline_margin) - var editTextMeasured by remember { mutableStateOf(0.dp) } + var editTextMeasured by remember { mutableStateOf(MiniumContentPaddingEditText) } Box(modifier = Modifier.fillMaxSize().imePadding()) { val additionalPadding = if (commenting) { editTextMeasured } else { if (!comments.hasMore) { - 16.dp + 56.dp // Fab space + Fab size + MiniumContentPaddingEditText } else { - 16.dp // Fab space + 0.dp } } LazyColumn( @@ -403,21 +379,22 @@ fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController } } Surface( - modifier = Modifier.align(Alignment.BottomCenter).onGloballyPositioned { coordinates -> - editTextMeasured = with(density) { coordinates.size.height.toDp() } - }.layout { measurable, constraints -> - val startHeight = max(constraints.minHeight, 80.dp.roundToPx()).coerceAtMost(constraints.maxHeight) - val endWidth = constraints.maxWidth - val width = lerp(0, endWidth, animationProgress) - val height = lerp(0, startHeight, animationProgress) + modifier = Modifier.align(Alignment.BottomCenter).layout { measurable, constraints -> + val origin = measurable.measure(constraints) + val width = lerp(0, origin.width, 1 - animationProgress) + val height = lerp(0, origin.height, 1 - animationProgress) val placeable = measurable.measure(Constraints.fixed(width, height)) layout(width, height) { placeable.placeRelative(0, 0) } - }.clip(RoundedCornerShape(((1 - animationProgress) * 100).roundToInt())), + }.clip(RoundedCornerShape((animationProgress * 100).roundToInt())), color = MaterialTheme.colorScheme.primaryContainer, ) { - Row(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) { + Row( + modifier = Modifier.fillMaxWidth().navigationBarsPadding().onGloballyPositioned { coordinates -> + editTextMeasured = max(with(density) { coordinates.size.height.toDp() }, MiniumContentPaddingEditText) + }, + ) { BasicTextField2( value = userComment, onValueChange = { userComment = it }, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt new file mode 100644 index 000000000..280aa254d --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt @@ -0,0 +1,63 @@ +package com.hippo.ehviewer.ui.tools + +import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hippo.ehviewer.util.isAtLeastU +import kotlin.math.max +import kotlinx.coroutines.CancellationException + +/* enable 0 1 + * animationProgress 1F 0F + * + * BackHandler enabling: + * enable 0 -> 1 immediately + * animationProgress 1F -> 0F by animationSpec + * + * BackGesture handling: + * enable 1 -> 0 when finished + * animationProgress 0F -> 1F + * SDK >= 34 by userGesture progress + * else by animationSpec when finished + */ +@Composable +fun animateFloatMergePredictiveBackAsState( + enable: MutableState, + animationSpec: AnimationSpec = spring(), +): State { + // Principal Value + var principal by enable + + // Natural animator state correspond with principal value + val coState by animateFloatAsState( + targetValue = if (principal) 0f else 1f, + animationSpec = animationSpec, + label = "animationProgress", + ) + // User predictive back animation progress holder && value correspond with UI State + val ret = remember { mutableFloatStateOf(1F) } + var animationProgress by ret + // Update UI animation state + animationProgress = if (principal || !isAtLeastU) coState else max(animationProgress, coState) + + PredictiveBackHandler(principal) { progress -> + try { + progress.collect { + animationProgress = it.progress + } + principal = false + } catch (e: CancellationException) { + principal = true + animationProgress = 0F + } + } + return ret +}