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
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)

View file

@ -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(

View file

@ -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].
*