MangaController overhaul (#7244)

This commit is contained in:
Ivan Iskandar
2022-06-25 22:03:48 +07:00
committed by GitHub
parent cf7ca5bd28
commit 33a778873a
57 changed files with 3701 additions and 2955 deletions

View File

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

View File

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

View File

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