From 571840a2faaebb32649a688c8d03c7b8e68b4f28 Mon Sep 17 00:00:00 2001 From: junkfood <69683722+JunkFood02@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:35:00 +0800 Subject: [PATCH] fix(ui): disable pull to load when no articles available --- .../reader/ui/page/home/reading/PullToLoad.kt | 24 +-- .../page/home/reading/PullToLoadIndicator.kt | 157 ++++++++++++------ .../ui/page/home/reading/ReadingPage.kt | 24 +-- 3 files changed, 125 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt index 7d9b75dc..261ea549 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt @@ -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( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoadIndicator.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoadIndicator.kt index 65e21db9..242c29f0 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoadIndicator.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoadIndicator.kt @@ -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) + ) + } + } + + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index 8c7cac3c..7d2ea14c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -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 } ) },