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
This commit is contained in:
Cuong-Tran 2024-09-24 00:52:51 +07:00 committed by GitHub
parent 444f199225
commit d09949e2bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 38 deletions

View file

@ -1,5 +1,6 @@
package eu.kanade.domain.ui package eu.kanade.domain.ui
import androidx.compose.material3.FabPosition
import com.materialkolor.PaletteStyle import com.materialkolor.PaletteStyle
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
@ -61,6 +62,8 @@ class UiPreferences(
fun relatedMangasInOverflow() = preferenceStore.getBoolean("related_mangas_in_overflow", false) fun relatedMangasInOverflow() = preferenceStore.getBoolean("related_mangas_in_overflow", false)
fun showHomeOnRelatedMangas() = preferenceStore.getBoolean("show_home_on_related_mangas", true) fun showHomeOnRelatedMangas() = preferenceStore.getBoolean("show_home_on_related_mangas", true)
fun readButtonPosition() = preferenceStore.getString("reading_button_position", FabPosition.End.toString())
// KMK <-- // KMK <--
fun recommendsInOverflow() = preferenceStore.getBoolean("recommends_in_overflow", false) fun recommendsInOverflow() = preferenceStore.getBoolean("recommends_in_overflow", false)

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
@ -26,6 +28,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.FabPosition
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -36,19 +39,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.hapticfeedback.HapticFeedbackType 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.onSizeChanged
import androidx.compose.ui.layout.positionOnScreen
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp 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.unit.dp
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
@ -128,6 +138,7 @@ import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import kotlin.math.roundToInt
@Composable @Composable
fun MangaScreen( fun MangaScreen(
@ -416,6 +427,13 @@ private fun MangaScreenSmallImpl(
val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState() val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState()
val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState() val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState()
val fullCoverBackground = MaterialTheme.colorScheme.surfaceTint.blend(MaterialTheme.colorScheme.surface) 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 <-- // KMK <--
val internalOnBackPressed = { val internalOnBackPressed = {
@ -502,6 +520,29 @@ private fun MangaScreenSmallImpl(
visible = isFABVisible, visible = isFABVisible,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), 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( ExtendedFloatingActionButton(
text = { text = {
@ -522,7 +563,15 @@ private fun MangaScreenSmallImpl(
} }
}, },
// KMK --> // KMK -->
floatingActionButtonPosition = if (fabPosition == FabPosition.End.toString()) {
FabPosition.End
} else {
FabPosition.Start
},
modifier = Modifier modifier = Modifier
.onGloballyPositioned { coordinates ->
layoutSize = coordinates.size
}
.haze( .haze(
state = hazeState, state = hazeState,
style = HazeStyle( style = HazeStyle(
@ -835,6 +884,13 @@ private fun MangaScreenLargeImpl(
val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState() val expandRelatedMangas by uiPreferences.expandRelatedMangas().collectAsState()
val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState() val showRelatedMangasInOverflow by uiPreferences.relatedMangasInOverflow().collectAsState()
val fullCoverBackground = MaterialTheme.colorScheme.surfaceTint.blend(MaterialTheme.colorScheme.surface) 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 <-- // KMK <--
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
@ -918,6 +974,29 @@ private fun MangaScreenLargeImpl(
visible = isFABVisible, visible = isFABVisible,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), 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( ExtendedFloatingActionButton(
text = { text = {
@ -940,7 +1019,15 @@ private fun MangaScreenLargeImpl(
} }
}, },
// KMK --> // KMK -->
floatingActionButtonPosition = if (fabPosition == FabPosition.End.toString()) {
FabPosition.End
} else {
FabPosition.Start
},
modifier = Modifier modifier = Modifier
.onGloballyPositioned { coordinates ->
layoutSize = coordinates.size
}
.haze( .haze(
state = hazeState, state = hazeState,
style = HazeStyle( style = HazeStyle(

View file

@ -27,7 +27,11 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior 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.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapNotNull
import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastMaxBy
import kotlin.math.max 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 * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
* [content], typically a [NavigationBar]. * [content], typically a [NavigationBar].
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ScaffoldLayout( private fun ScaffoldLayout(
fabPosition: FabPosition, fabPosition: FabPosition,
@ -210,8 +215,12 @@ private fun ScaffoldLayout(
} }
val fabPlaceables = val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable -> subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable ->
measurable.measure(looseConstraints) measurable
// KMK -->
.measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset))
.takeIf { it.height != 0 && it.width != 0 }
// KMK <--
} }
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0 val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
@ -220,14 +229,28 @@ private fun ScaffoldLayout(
val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) {
// FAB distance from the left of the layout, taking into account LTR / RTL // FAB distance from the left of the layout, taking into account LTR / RTL
// Tachiyomi: Calculate insets for fab placement offset // Tachiyomi: Calculate insets for fab placement offset
val fabLeftOffset = if (fabPosition == FabPosition.End) { val fabLeftOffset = when (fabPosition) {
if (layoutDirection == LayoutDirection.Ltr) { // KMK -->
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset FabPosition.Start -> {
} else { if (layoutDirection == LayoutDirection.Ltr) {
FabSpacing.roundToPx() + leftInset 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( 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]. * Placement information for a [FloatingActionButton] inside a [Scaffold].
* *