mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 03:58:56 +01:00
MangaController overhaul (#7244)
This commit is contained in:
@@ -1,5 +1,29 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrollingUp(): Boolean {
|
||||
var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
|
||||
var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
|
||||
return remember {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset >= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
}
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.animateDecay
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.TopAppBarScrollState
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* A [TopAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a top
|
||||
* app bar.
|
||||
*
|
||||
* A top app bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse when
|
||||
* the nested content is pulled up, and will expand back the collapsed area when the content is
|
||||
* pulled all the way down.
|
||||
*
|
||||
* @param decayAnimationSpec a [DecayAnimationSpec] that will be used by the top app bar motion
|
||||
* when the user flings the content. Preferably, this should match the animation spec used by the
|
||||
* scrollable content. See also [androidx.compose.animation.rememberSplineBasedDecay] for a
|
||||
* default [DecayAnimationSpec] that can be used with this behavior.
|
||||
* @param canScroll a callback used to determine whether scroll events are to be
|
||||
* handled by this [ExitUntilCollapsedScrollBehavior]
|
||||
*/
|
||||
class ExitUntilCollapsedScrollBehavior(
|
||||
override val state: TopAppBarScrollState,
|
||||
val decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
val canScroll: () -> Boolean = { true },
|
||||
) : TopAppBarScrollBehavior {
|
||||
override val scrollFraction: Float
|
||||
get() = if (state.offsetLimit != 0f) state.offset / state.offsetLimit else 0f
|
||||
override var nestedScrollConnection =
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
// Don't intercept if scrolling down.
|
||||
if (!canScroll() || available.y > 0f) return Offset.Zero
|
||||
|
||||
val newOffset = (state.offset + available.y)
|
||||
val coerced =
|
||||
newOffset.coerceIn(minimumValue = state.offsetLimit, maximumValue = 0f)
|
||||
return if (newOffset == coerced) {
|
||||
// Nothing coerced, meaning we're in the middle of top app bar collapse or
|
||||
// expand.
|
||||
state.offset = coerced
|
||||
// Consume only the scroll on the Y axis.
|
||||
available.copy(x = 0f)
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
if (!canScroll()) return Offset.Zero
|
||||
state.contentOffset += consumed.y
|
||||
|
||||
if (available.y < 0f || consumed.y < 0f) {
|
||||
// When scrolling up, just update the state's offset.
|
||||
val oldOffset = state.offset
|
||||
state.offset = (state.offset + consumed.y).coerceIn(
|
||||
minimumValue = state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
return Offset(0f, state.offset - oldOffset)
|
||||
}
|
||||
|
||||
if (consumed.y == 0f && available.y > 0) {
|
||||
// Reset the total offset to zero when scrolling all the way down. This will
|
||||
// eliminate some float precision inaccuracies.
|
||||
state.contentOffset = 0f
|
||||
}
|
||||
|
||||
if (available.y > 0f) {
|
||||
// Adjust the offset in case the consumed delta Y is less than what was recorded
|
||||
// as available delta Y in the pre-scroll.
|
||||
val oldOffset = state.offset
|
||||
state.offset = (state.offset + available.y).coerceIn(
|
||||
minimumValue = state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
return Offset(0f, state.offset - oldOffset)
|
||||
}
|
||||
return Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val result = super.onPostFling(consumed, available)
|
||||
if ((available.y < 0f && state.contentOffset == 0f) ||
|
||||
(available.y > 0f && state.offset < 0f)
|
||||
) {
|
||||
return result +
|
||||
onTopBarFling(
|
||||
scrollBehavior = this@ExitUntilCollapsedScrollBehavior,
|
||||
initialVelocity = available.y,
|
||||
decayAnimationSpec = decayAnimationSpec,
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tachiyomi: Remove snap behavior
|
||||
*/
|
||||
private suspend fun onTopBarFling(
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
initialVelocity: Float,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
): Velocity {
|
||||
if (abs(initialVelocity) > 1f) {
|
||||
var remainingVelocity = initialVelocity
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
.animateDecay(decayAnimationSpec) {
|
||||
val delta = value - lastValue
|
||||
val initialOffset = scrollBehavior.state.offset
|
||||
scrollBehavior.state.offset =
|
||||
(initialOffset + delta).coerceIn(
|
||||
minimumValue = scrollBehavior.state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
val consumed = abs(initialOffset - scrollBehavior.state.offset)
|
||||
lastValue = value
|
||||
remainingVelocity = this.velocity
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
|
||||
}
|
||||
return Velocity(0f, remainingVelocity)
|
||||
}
|
||||
return Velocity.Zero
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun calculateWindowWidthSizeClass(): WindowWidthSizeClass {
|
||||
val configuration = LocalConfiguration.current
|
||||
return fromWidth(configuration.smallestScreenWidthDp.dp)
|
||||
}
|
||||
|
||||
private fun fromWidth(width: Dp): WindowWidthSizeClass {
|
||||
require(width >= 0.dp) { "Width must not be negative" }
|
||||
return when {
|
||||
width < 720.dp -> WindowWidthSizeClass.Compact // Was 600
|
||||
width < 840.dp -> WindowWidthSizeClass.Medium
|
||||
else -> WindowWidthSizeClass.Expanded
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user