mirror of
https://github.com/Ashinch/ReadYou.git
synced 2025-02-08 16:18:40 +01:00
fix(ui): disable pull to load when no articles available
This commit is contained in:
parent
1bf597d32e
commit
571840a2fa
@ -2,8 +2,10 @@ package me.ash.reader.ui.page.home.reading
|
|||||||
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.FloatExponentialDecaySpec
|
import androidx.compose.animation.core.FloatExponentialDecaySpec
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animate
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateDecay
|
import androidx.compose.animation.core.animateDecay
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.MutatorMutex
|
import androidx.compose.foundation.MutatorMutex
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
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.abs
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
private const val TAG = "PullRelease"
|
private const val TAG = "PullToLoad"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [NestedScrollConnection] that provides scroll events to a hoisted [state].
|
* A [NestedScrollConnection] that provides scroll events to a hoisted [state].
|
||||||
@ -51,7 +53,7 @@ private class ReaderNestedScrollConnection(
|
|||||||
private val enabled: Boolean,
|
private val enabled: Boolean,
|
||||||
private val onPreScroll: (Float) -> Float,
|
private val onPreScroll: (Float) -> Float,
|
||||||
private val onPostScroll: (Float) -> Float,
|
private val onPostScroll: (Float) -> Float,
|
||||||
private val onRelease: (Float) -> Unit,
|
private val onRelease: () -> Unit,
|
||||||
private val onScroll: ((Float) -> Unit)? = null
|
private val onScroll: ((Float) -> Unit)? = null
|
||||||
) : NestedScrollConnection {
|
) : NestedScrollConnection {
|
||||||
|
|
||||||
@ -81,7 +83,7 @@ private class ReaderNestedScrollConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
onRelease(available.y)
|
onRelease()
|
||||||
return Velocity.Zero
|
return Velocity.Zero
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,21 +219,22 @@ class PullToLoadState internal constructor(
|
|||||||
return if (offsetPulled.signOpposites(pullDelta)) onPull(pullDelta) else 0f
|
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) {
|
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 -> {
|
Status.PulledDown -> {
|
||||||
onLoadPrevious.value()
|
onLoadPrevious.value()
|
||||||
}
|
}
|
||||||
|
|
||||||
Status.PulledUp -> {
|
Status.PulledUp -> {
|
||||||
|
animateDistanceTo(0f)
|
||||||
onLoadNext.value()
|
onLoadNext.value()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Snap to 0f and hide the indicator
|
|
||||||
animateDistanceTo(0f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0f
|
return 0f
|
||||||
@ -247,7 +250,8 @@ class PullToLoadState internal constructor(
|
|||||||
animate(
|
animate(
|
||||||
initialValue = offsetPulled,
|
initialValue = offsetPulled,
|
||||||
targetValue = float,
|
targetValue = float,
|
||||||
initialVelocity = velocity
|
initialVelocity = velocity,
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
|
||||||
) { value, _ ->
|
) { value, _ ->
|
||||||
offsetPulled = value
|
offsetPulled = value
|
||||||
}
|
}
|
||||||
@ -310,7 +314,7 @@ fun Modifier.pullToLoad(
|
|||||||
state: PullToLoadState,
|
state: PullToLoadState,
|
||||||
contentOffsetMultiple: Int = ContentOffsetMultiple,
|
contentOffsetMultiple: Int = ContentOffsetMultiple,
|
||||||
onScroll: ((Float) -> Unit)? = null,
|
onScroll: ((Float) -> Unit)? = null,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true,
|
||||||
): Modifier =
|
): Modifier =
|
||||||
nestedScroll(
|
nestedScroll(
|
||||||
ReaderNestedScrollConnection(
|
ReaderNestedScrollConnection(
|
||||||
|
@ -20,70 +20,117 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxScope.PullToLoadIndicator(state:PullToLoadState) {
|
fun BoxScope.PullToLoadIndicator(
|
||||||
state.status.run {
|
state: PullToLoadState,
|
||||||
val fraction = state.offsetFraction
|
canLoadPrevious: Boolean = true,
|
||||||
val absFraction = abs(fraction)
|
canLoadNext: Boolean = true
|
||||||
val imageVector = when (this) {
|
) {
|
||||||
PullToLoadState.Status.PulledDown -> Icons.Outlined.KeyboardArrowUp
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
PullToLoadState.Status.PulledUp -> Icons.Outlined.KeyboardArrowDown
|
val status = state.status
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val alignment = if (fraction < 0f) {
|
LaunchedEffect(status) {
|
||||||
Alignment.BottomCenter
|
when {
|
||||||
} else {
|
canLoadPrevious && status == PulledDown -> {
|
||||||
Alignment.TopCenter
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
@ -115,10 +114,10 @@ fun ReadingPage(
|
|||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val context = LocalContext.current
|
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
|
||||||
|
|
||||||
val isNextArticleAvailable = !readerState.nextArticleId.isNullOrEmpty()
|
val isNextArticleAvailable = !readerState.nextArticleId.isNullOrEmpty()
|
||||||
|
val isPreviousArticleAvailable = !readerState.previousArticleId.isNullOrEmpty()
|
||||||
|
|
||||||
|
|
||||||
if (readerState.articleId != null) {
|
if (readerState.articleId != null) {
|
||||||
// Content
|
// 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(
|
val listState = rememberSaveable(
|
||||||
inputs = arrayOf(content),
|
inputs = arrayOf(content),
|
||||||
saver = LazyListState.Saver
|
saver = LazyListState.Saver
|
||||||
@ -210,7 +199,11 @@ fun ReadingPage(
|
|||||||
showFullScreenImageViewer = true
|
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)) },
|
onSuccess = { context.showToast(context.getString(R.string.image_saved)) },
|
||||||
onFailure = {
|
onFailure = {
|
||||||
// FIXME: crash the app for error report
|
// FIXME: crash the app for error report
|
||||||
th -> throw th
|
th ->
|
||||||
|
throw th
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user