From d09949e2bbd128ff92c682456151eee44276149f Mon Sep 17 00:00:00 2001 From: Cuong-Tran Date: Tue, 24 Sep 2024 00:52:51 +0700 Subject: [PATCH] feat(ui): read/resume button movable * material3: update scaffold to support FabPosition.Start also fix fab's width calculation with inset * Moveable Read button & sticky to either size of screen --- .../java/eu/kanade/domain/ui/UiPreferences.kt | 3 + .../kanade/presentation/manga/MangaScreen.kt | 87 +++++++++++++++++++ .../core/components/material/Scaffold.kt | 71 +++++++-------- 3 files changed, 123 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index 7f3652bf1..244eb155d 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -1,5 +1,6 @@ package eu.kanade.domain.ui +import androidx.compose.material3.FabPosition import com.materialkolor.PaletteStyle import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.TabletUiMode @@ -61,6 +62,8 @@ class UiPreferences( fun relatedMangasInOverflow() = preferenceStore.getBoolean("related_mangas_in_overflow", false) fun showHomeOnRelatedMangas() = preferenceStore.getBoolean("show_home_on_related_mangas", true) + + fun readButtonPosition() = preferenceStore.getString("reading_button_position", FabPosition.End.toString()) // KMK <-- fun recommendsInOverflow() = preferenceStore.getBoolean("recommends_in_overflow", false) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index c3eddad31..5c03ae126 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars @@ -26,6 +28,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.FabPosition import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -36,19 +39,26 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny @@ -128,6 +138,7 @@ import uy.kohesive.injekt.api.get import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime +import kotlin.math.roundToInt @Composable fun MangaScreen( @@ -416,6 +427,13 @@ private fun MangaScreenSmallImpl( val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState() val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState() val fullCoverBackground = MaterialTheme.colorScheme.surfaceTint.blend(MaterialTheme.colorScheme.surface) + + var layoutSize by remember { mutableStateOf(IntSize.Zero) } + var fabSize by remember { mutableStateOf(IntSize.Zero) } + var positionOnScreen by remember { mutableStateOf(Offset.Zero) } + var offsetX by remember { mutableFloatStateOf(0f) } + val fabPosition by uiPreferences.readButtonPosition().collectAsState() + val readButtonPosition = uiPreferences.readButtonPosition() // KMK <-- val internalOnBackPressed = { @@ -502,6 +520,29 @@ private fun MangaScreenSmallImpl( visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), + // KMK --> + modifier = Modifier + .offset { IntOffset(offsetX.roundToInt(), 0) } + .onGloballyPositioned { coordinates -> + fabSize = coordinates.size + positionOnScreen = coordinates.positionOnScreen() + } + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + if (positionOnScreen.x + fabSize.width / 2 >= layoutSize.width / 2) { + readButtonPosition.set(FabPosition.End.toString()) + } else { + readButtonPosition.set(FabPosition.Start.toString()) + } + offsetX = 0f + }, + ) { change, dragAmount -> + change.consume() + offsetX += dragAmount + } + }, + // KMK <-- ) { ExtendedFloatingActionButton( text = { @@ -522,7 +563,15 @@ private fun MangaScreenSmallImpl( } }, // KMK --> + floatingActionButtonPosition = if (fabPosition == FabPosition.End.toString()) { + FabPosition.End + } else { + FabPosition.Start + }, modifier = Modifier + .onGloballyPositioned { coordinates -> + layoutSize = coordinates.size + } .haze( state = hazeState, style = HazeStyle( @@ -835,6 +884,13 @@ private fun MangaScreenLargeImpl( val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState() val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState() val fullCoverBackground = MaterialTheme.colorScheme.surfaceTint.blend(MaterialTheme.colorScheme.surface) + + var layoutSize by remember { mutableStateOf(IntSize.Zero) } + var fabSize by remember { mutableStateOf(IntSize.Zero) } + var positionOnScreen by remember { mutableStateOf(Offset.Zero) } + var offsetX by remember { mutableFloatStateOf(0f) } + val fabPosition by uiPreferences.readButtonPosition().collectAsState() + val readButtonPosition = uiPreferences.readButtonPosition() // KMK <-- val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() @@ -918,6 +974,29 @@ private fun MangaScreenLargeImpl( visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), + // KMK --> + modifier = Modifier + .offset { IntOffset(offsetX.roundToInt(), 0) } + .onGloballyPositioned { coordinates -> + fabSize = coordinates.size + positionOnScreen = coordinates.positionOnScreen() + } + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + if (positionOnScreen.x + fabSize.width / 2 >= layoutSize.width / 2) { + readButtonPosition.set(FabPosition.End.toString()) + } else { + readButtonPosition.set(FabPosition.Start.toString()) + } + offsetX = 0f + }, + ) { change, dragAmount -> + change.consume() + offsetX += dragAmount + } + }, + // KMK <-- ) { ExtendedFloatingActionButton( text = { @@ -940,7 +1019,15 @@ private fun MangaScreenLargeImpl( } }, // KMK --> + floatingActionButtonPosition = if (fabPosition == FabPosition.End.toString()) { + FabPosition.End + } else { + FabPosition.Start + }, modifier = Modifier + .onGloballyPositioned { coordinates -> + layoutSize = coordinates.size + } .haze( state = hazeState, style = HazeStyle( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt index 335ce6c96..a4ac52822 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt @@ -27,7 +27,11 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -44,8 +48,10 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap +import androidx.compose.ui.util.fastMapNotNull import androidx.compose.ui.util.fastMaxBy import kotlin.math.max @@ -152,7 +158,6 @@ fun Scaffold( * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the * [content], typically a [NavigationBar]. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ScaffoldLayout( fabPosition: FabPosition, @@ -210,8 +215,12 @@ private fun ScaffoldLayout( } val fabPlaceables = - subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable -> - measurable.measure(looseConstraints) + subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable -> + measurable + // KMK --> + .measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset)) + .takeIf { it.height != 0 && it.width != 0 } + // KMK <-- } val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0 @@ -220,14 +229,28 @@ private fun ScaffoldLayout( val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { // FAB distance from the left of the layout, taking into account LTR / RTL // Tachiyomi: Calculate insets for fab placement offset - val fabLeftOffset = if (fabPosition == FabPosition.End) { - if (layoutDirection == LayoutDirection.Ltr) { - layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset - } else { - FabSpacing.roundToPx() + leftInset + val fabLeftOffset = when (fabPosition) { + // KMK --> + FabPosition.Start -> { + if (layoutDirection == LayoutDirection.Ltr) { + FabSpacing.roundToPx() + leftInset + } else { + layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset + } + } + FabPosition.EndOverlay, + // KMK <-- + FabPosition.End, + -> { + if (layoutDirection == LayoutDirection.Ltr) { + layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset + } else { + FabSpacing.roundToPx() + leftInset + } + } + else -> { + leftInset + ((insetLayoutWidth - fabWidth) / 2) } - } else { - leftInset + ((insetLayoutWidth - fabWidth) / 2) } FabPlacement( @@ -311,34 +334,6 @@ private fun ScaffoldLayout( } } -/** - * The possible positions for a [FloatingActionButton] attached to a [Scaffold]. - */ -@ExperimentalMaterial3Api -@JvmInline -value class FabPosition internal constructor(@Suppress("unused") private val value: Int) { - companion object { - /** - * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it - * exists) - */ - val Center = FabPosition(0) - - /** - * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it - * exists) - */ - val End = FabPosition(1) - } - - override fun toString(): String { - return when (this) { - Center -> "FabPosition.Center" - else -> "FabPosition.End" - } - } -} - /** * Placement information for a [FloatingActionButton] inside a [Scaffold]. *