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
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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].
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue