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