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:
parent
444f199225
commit
d09949e2bb
3 changed files with 123 additions and 38 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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].
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue