fix(ui): disable pull to load when no articles available

This commit is contained in:
junkfood 2024-04-28 17:35:00 +08:00
parent 1bf597d32e
commit 571840a2fa
No known key found for this signature in database
GPG Key ID: 2EA5B648DB112A34
3 changed files with 125 additions and 80 deletions

View File

@ -2,8 +2,10 @@ package me.ash.reader.ui.page.home.reading
import androidx.compose.animation.core.FloatExponentialDecaySpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.spring
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.offset
import androidx.compose.material.ExperimentalMaterialApi
@ -33,7 +35,7 @@ import me.ash.reader.ui.page.home.reading.PullToLoadDefaults.ContentOffsetMultip
import kotlin.math.abs
import kotlin.math.sqrt
private const val TAG = "PullRelease"
private const val TAG = "PullToLoad"
/**
* A [NestedScrollConnection] that provides scroll events to a hoisted [state].
@ -51,7 +53,7 @@ private class ReaderNestedScrollConnection(
private val enabled: Boolean,
private val onPreScroll: (Float) -> Float,
private val onPostScroll: (Float) -> Float,
private val onRelease: (Float) -> Unit,
private val onRelease: () -> Unit,
private val onScroll: ((Float) -> Unit)? = null
) : NestedScrollConnection {
@ -81,7 +83,7 @@ private class ReaderNestedScrollConnection(
}
override suspend fun onPreFling(available: Velocity): Velocity {
onRelease(available.y)
onRelease()
return Velocity.Zero
}
}
@ -217,21 +219,22 @@ class PullToLoadState internal constructor(
return if (offsetPulled.signOpposites(pullDelta)) onPull(pullDelta) else 0f
}
internal fun onRelease(velocity: Float): Float {
internal fun onRelease(): Float {
// Snap to 0f and hide the indicator
animateDistanceTo(0f)
when (status) {
// We don't change the pull offset here because the animation for loading another content
// should be handled outside, and this state will be soon disposed
Status.PulledDown -> {
onLoadPrevious.value()
}
Status.PulledUp -> {
animateDistanceTo(0f)
onLoadNext.value()
}
else -> {
// Snap to 0f and hide the indicator
animateDistanceTo(0f)
}
}
return 0f
@ -247,7 +250,8 @@ class PullToLoadState internal constructor(
animate(
initialValue = offsetPulled,
targetValue = float,
initialVelocity = velocity
initialVelocity = velocity,
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
) { value, _ ->
offsetPulled = value
}
@ -310,7 +314,7 @@ fun Modifier.pullToLoad(
state: PullToLoadState,
contentOffsetMultiple: Int = ContentOffsetMultiple,
onScroll: ((Float) -> Unit)? = null,
enabled: Boolean = true
enabled: Boolean = true,
): Modifier =
nestedScroll(
ReaderNestedScrollConnection(

View File

@ -20,70 +20,117 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.Idle
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PulledDown
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PulledUp
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PullingDown
import me.ash.reader.ui.page.home.reading.PullToLoadState.Status.PullingUp
import kotlin.math.abs
@Composable
fun BoxScope.PullToLoadIndicator(state:PullToLoadState) {
state.status.run {
val fraction = state.offsetFraction
val absFraction = abs(fraction)
val imageVector = when (this) {
PullToLoadState.Status.PulledDown -> Icons.Outlined.KeyboardArrowUp
PullToLoadState.Status.PulledUp -> Icons.Outlined.KeyboardArrowDown
else -> null
}
fun BoxScope.PullToLoadIndicator(
state: PullToLoadState,
canLoadPrevious: Boolean = true,
canLoadNext: Boolean = true
) {
val hapticFeedback = LocalHapticFeedback.current
val status = state.status
val alignment = if (fraction < 0f) {
Alignment.BottomCenter
} else {
Alignment.TopCenter
}
if (this != PullToLoadState.Status.Idle) {
Surface(
modifier = Modifier
.align(alignment)
.padding(vertical = 80.dp)
.offset(y = (fraction * 48).dp)
.width(36.dp),
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.extraLarge
) {
Column(
modifier = Modifier
.align(Alignment.Center),
) {
AnimatedContent(
targetState = imageVector, modifier = Modifier.align(
Alignment.CenterHorizontally
), transitionSpec = {
(fadeIn(animationSpec = tween(220, delayMillis = 0)))
.togetherWith(fadeOut(animationSpec = tween(90)))
}, label = ""
) {
if (it != null) {
Icon(
imageVector = it,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.padding(horizontal = 4.dp)
.padding(vertical = (2 * absFraction).dp)
.size(32.dp)
)
} else {
Spacer(
modifier = Modifier
.width(36.dp)
.height((12 * absFraction).dp)
)
}
}
LaunchedEffect(status) {
when {
canLoadPrevious && status == PulledDown -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
canLoadNext && status == PulledUp -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
else -> {}
}
}
val fraction = state.offsetFraction
val absFraction = abs(fraction)
val imageVector = when (status) {
PulledDown -> Icons.Outlined.KeyboardArrowUp
PulledUp -> Icons.Outlined.KeyboardArrowDown
else -> null
}
val alignment = if (fraction < 0f) {
Alignment.BottomCenter
} else {
Alignment.TopCenter
}
val visible = remember(status, canLoadPrevious, canLoadNext) {
when (status) {
Idle -> {
false
}
PullingUp, PulledUp -> {
canLoadNext
}
PulledDown, PullingDown -> {
canLoadPrevious
}
}
}
if (visible) {
Surface(
modifier = Modifier
.align(alignment)
.padding(vertical = 80.dp)
.offset(y = (fraction * 48).dp)
.width(36.dp),
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.extraLarge
) {
Column(
modifier = Modifier
.align(Alignment.Center),
) {
AnimatedContent(
targetState = imageVector, modifier = Modifier.align(
Alignment.CenterHorizontally
), transitionSpec = {
(fadeIn(animationSpec = tween(220, delayMillis = 0)))
.togetherWith(fadeOut(animationSpec = tween(90)))
}, label = ""
) {
if (it != null) {
Icon(
imageVector = it,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.padding(horizontal = 4.dp)
.padding(vertical = (2 * absFraction).dp)
.size(32.dp)
)
} else {
Spacer(
modifier = Modifier
.width(36.dp)
.height((12 * absFraction).dp)
)
}
}
}
}
}
}

View File

@ -23,7 +23,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.TextUnit
@ -115,10 +114,10 @@ fun ReadingPage(
navController.popBackStack()
},
)
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
val isNextArticleAvailable = !readerState.nextArticleId.isNullOrEmpty()
val isPreviousArticleAvailable = !readerState.previousArticleId.isNullOrEmpty()
if (readerState.articleId != null) {
// Content
@ -159,16 +158,6 @@ fun ReadingPage(
)
LaunchedEffect(state.status) {
when (state.status) {
PullToLoadState.Status.PulledDown, PullToLoadState.Status.PulledUp -> {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
else -> {}
}
}
val listState = rememberSaveable(
inputs = arrayOf(content),
saver = LazyListState.Saver
@ -210,7 +199,11 @@ fun ReadingPage(
showFullScreenImageViewer = true
}
)
PullToLoadIndicator(state = state)
PullToLoadIndicator(
state = state,
canLoadPrevious = isPreviousArticleAvailable,
canLoadNext = isNextArticleAvailable
)
}
}
}
@ -252,7 +245,8 @@ fun ReadingPage(
onSuccess = { context.showToast(context.getString(R.string.image_saved)) },
onFailure = {
// FIXME: crash the app for error report
th -> throw th
th ->
throw th
}
)
},