Refine Fablayout (#403)

* Fix read page

* Refine FabLayout

* Let Fablayout have consistent animation state, walk consistently

* Let Fablayout collapse when secondaryFab updated

* Move animation delay out of FabLayout, should not handle that within FabLayout

* Refine predictive back

* Refine predictive back

* Introduce waitCollapse

* Refine

* Simplify

* Simplify

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-11-30 11:02:58 +00:00
1 parent 64ac770c56
commit 730035aff7
5 files changed
+166 -54

No files matched your search

@@ -1,7 +1,12 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.animation.core.animateFloatAsState
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.tween
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
@@ -14,11 +19,14 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
@@ -27,10 +35,66 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
enum class FabLayoutValue {
Hidden,
Primary,
Expand,
}
@Composable
fun rememberFabLayoutState(initialValue: FabLayoutValue = FabLayoutValue.Primary): FabLayoutState {
return remember { FabLayoutState(initialValue) }
}
@Stable
class FabLayoutState(
initialValue: FabLayoutValue,
private val animationSpec: AnimationSpec<Float> = tween(FAB_ANIMATE_TIME),
) {
val mutatorMutex = MutatorMutex()
val appearProgress = Animatable(if (initialValue != FabLayoutValue.Hidden) 1f else 0f)
.apply { updateBounds(0f, 1f) }
val expandProgress = Animatable(if (initialValue == FabLayoutValue.Expand) 1f else 0f)
.apply { updateBounds(0f, 1f) }
private suspend fun downTo(value: FabLayoutValue, priority: MutatePriority) {
mutatorMutex.mutate(priority) {
if (expandProgress.value != 0f) expandProgress.animateTo(0f, animationSpec)
if (value == FabLayoutValue.Hidden) {
appearProgress.animateTo(0f, animationSpec)
}
}
}
private suspend fun upTo(value: FabLayoutValue, priority: MutatePriority) {
mutatorMutex.mutate(priority) {
if (appearProgress.value != 1f) appearProgress.animateTo(1f, animationSpec)
if (value == FabLayoutValue.Expand) {
expandProgress.animateTo(1f, animationSpec)
}
}
}
suspend fun waitCollapse() {
if (expandProgress.value != 0f) {
snapshotFlow { expandProgress.value }.first { it == 0f }
}
}
suspend fun show(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Primary, priority)
suspend fun hide(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Hidden, priority)
suspend fun collapse(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Primary, priority)
suspend fun expand(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Expand, priority)
}
fun interface FabBuilder {
fun onClick(icon: ImageVector, that: suspend () -> Unit)
}
@@ -41,36 +105,50 @@ fun FabLayout(
expanded: Boolean,
onExpandChanged: (Boolean) -> Unit,
autoCancel: Boolean,
showAddButton: Boolean = true,
delayOnShow: Boolean = false,
rotateWhenExpand: Boolean = true,
fabBuilder: FabBuilder.() -> Unit,
) {
var delayOnExpand by remember { mutableStateOf(hidden) }
var delayOnHide by remember { mutableStateOf(expanded) }
val secondaryFab = remember(fabBuilder) {
buildList {
fabBuilder { icon, action ->
add(icon to action)
val updatedHidden by rememberUpdatedState(hidden)
val updatedExpanded by rememberUpdatedState(expanded)
val newState = when {
updatedHidden -> FabLayoutValue.Hidden
updatedExpanded -> FabLayoutValue.Expand
else -> FabLayoutValue.Primary
}
val state = rememberFabLayoutState(newState)
LaunchedEffect(newState) {
when (newState) {
FabLayoutValue.Expand -> state.expand()
FabLayoutValue.Hidden -> state.hide()
FabLayoutValue.Primary -> {
state.collapse()
state.show()
}
}
}
if (expanded) {
if (autoCancel) {
Spacer(
modifier = Modifier.fillMaxSize().pointerInput(expanded) {
detectTapGestures {
onExpandChanged(false)
}
},
)
val builder by rememberUpdatedState(fabBuilder)
fun buildFab(builder: FabBuilder.() -> Unit) = buildList {
builder { icon, action ->
add(icon to action)
}
}
val secondaryFab by remember {
snapshotFlow { builder }.map { one ->
state.waitCollapse()
buildFab(one)
}
}.collectAsState(remember { buildFab(fabBuilder) })
val coroutineScope = rememberCoroutineScope()
val appearState by animateFloatAsState(
targetValue = if (hidden) 0f else 1f,
animationSpec = tween(FAB_ANIMATE_TIME, if (delayOnHide || !hidden && delayOnShow) FAB_ANIMATE_TIME else 0),
label = "hiddenState",
) { delayOnExpand = hidden }
if (updatedExpanded && autoCancel) {
Spacer(
modifier = Modifier.fillMaxSize().pointerInput(Unit) {
detectTapGestures { onExpandChanged(false) }
},
)
}
val appearState by state.appearProgress.asState()
Box(
modifier = Modifier.fillMaxSize().navigationBarsPadding().padding(16.dp),
contentAlignment = Alignment.BottomEnd,
@@ -83,22 +161,29 @@ fun FabLayout(
},
contentAlignment = Alignment.Center,
) {
val animatedProgress by animateFloatMergePredictiveBackAsState(
expanded,
tween(FAB_ANIMATE_TIME, if (delayOnExpand) FAB_ANIMATE_TIME else 0),
finishedListener = { delayOnHide = expanded },
) {
onExpandChanged(false)
val animatedProgress by state.expandProgress.asState()
PredictiveBackHandler(updatedExpanded) { flow ->
try {
state.mutatorMutex.mutate(MutatePriority.UserInput) {
flow.collect {
val eased = EaseOut.transform(it.progress)
state.expandProgress.snapTo(1 - eased)
}
}
onExpandChanged(false)
} catch (e: CancellationException) {
coroutineScope.launch {
state.expand()
}
}
}
FloatingActionButton(
onClick = { onExpandChanged(!expanded) },
) {
FloatingActionButton(onClick = { onExpandChanged(!updatedExpanded) }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = if (showAddButton) {
modifier = if (rotateWhenExpand) {
Modifier.graphicsLayer {
rotationZ = lerp(0f, -135f, animatedProgress)
rotationZ = lerp(-135f, 0f, animatedProgress)
}
} else {
Modifier
@@ -119,11 +204,11 @@ fun FabLayout(
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
val distance = lerp(150 * (size - index) + 50, 0, animatedProgress)
val distance = lerp(0, 150 * (size - index) + 50, animatedProgress)
placeable.placeRelative(0, -distance, -(size - index).toFloat())
}
}.graphicsLayer {
alpha = 1 - animatedProgress
alpha = animatedProgress
},
) {
Icon(imageVector = imageVector, contentDescription = null)
@@ -488,7 +488,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) {
expanded = selectMode,
onExpandChanged = { checkedInfoMap.clear() },
autoCancel = false,
showAddButton = false,
rotateWhenExpand = false,
) {
onClick(Icons.Default.DoneAll) {
val info = list.associateBy { it.gid }
@@ -38,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -68,6 +69,7 @@ import com.hippo.ehviewer.ui.LocalSideSheetState
import com.hippo.ehviewer.ui.LockDrawer
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.destinations.GalleryDetailScreenDestination
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.main.FabLayout
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
@@ -83,6 +85,9 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
@@ -307,17 +312,24 @@ fun FavouritesScreen(navigator: DestinationsNavigator) {
)
}
val hideFab by remember {
snapshotFlow {
hidden
}.onEach {
if (!it) delay(FAB_ANIMATE_TIME.toLong())
}.mapLatest { it }
}.collectAsState(hidden)
// Explicitly reallocate fabBuilder lambda to recompose secondary fab
val select = selectMode
FabLayout(
hidden = hidden,
hidden = hideFab,
expanded = expanded || selectMode,
onExpandChanged = {
expanded = it
checkedInfoMap.clear()
},
autoCancel = !selectMode,
delayOnShow = true,
) {
if (!select) {
onClick(EhIcons.Default.GoTo) {
@@ -77,6 +77,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.text.parseAsHtml
import androidx.lifecycle.compose.LifecycleResumeEffect
import arrow.core.partially1
import coil.imageLoader
import com.google.android.material.snackbar.Snackbar
@@ -897,19 +898,21 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
val windowSizeClass = calculateWindowSizeClass(activity)
val thumbColumns by Settings.thumbColumns.collectAsState()
val readText = stringResource(R.string.read)
val downloadText = stringResource(R.string.download)
var readButtonText by rememberSaveable { mutableStateOf(readText) }
LaunchedEffect(Unit) {
runSuspendCatching {
val queen = SpiderQueen.obtainSpiderQueen(galleryInfo, MODE_READ)
val startPage = queen.awaitStartPage()
SpiderQueen.releaseSpiderQueen(queen, MODE_READ)
readButtonText = if (startPage == 0) {
readText
} else {
context.getString(R.string.read_from, startPage + 1)
LifecycleResumeEffect(Unit) {
coroutineScope.launchIO {
runSuspendCatching {
val queen = SpiderQueen.obtainSpiderQueen(galleryInfo, MODE_READ)
val startPage = queen.awaitStartPage()
SpiderQueen.releaseSpiderQueen(queen, MODE_READ)
readButtonText = if (startPage == 0) {
readText
} else {
context.getString(R.string.read_from, startPage + 1)
}
}
}
onPauseOrDispose { }
}
val downloadState by EhDownloadManager.collectDownloadState(gid)
val downloadButtonText = when (downloadState) {
@@ -58,6 +58,7 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
@@ -66,6 +67,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -154,6 +156,8 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import java.io.File
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import sh.calvin.reorderable.ReorderableItem
@@ -806,12 +810,20 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) {
val gotoTitle = stringResource(R.string.go_to)
val invalidNum = stringResource(R.string.error_invalid_number)
val outOfRange = stringResource(R.string.error_out_of_range)
val hideFab by remember {
snapshotFlow {
hidden || showSearchLayout
}.onEach {
if (!it) delay(FAB_ANIMATE_TIME.toLong())
}.mapLatest { it }
}.collectAsState(hidden || showSearchLayout)
FabLayout(
hidden = hidden || showSearchLayout,
hidden = hideFab,
expanded = expanded,
onExpandChanged = { expanded = it },
autoCancel = true,
delayOnShow = true,
) {
onClick(Icons.Default.Refresh) {
urlBuilder.setIndex(null)