Replace deprecated SwipeRefresh with recomended PullRefreshIndicator and implement pull to refresh for Feeds Page. (#850)

* Flow Page: replace SwipeRefresh with PullRefreshIndicator.

* Feed Page: use PullRefreshIndicator for pull down refresh.

* SwipeRefresh: Removed due to deprecation.

* Switch from MD2 PullRefresh to MD3 PullToRefresh

---------

Co-authored-by: Will McCall <532b81611e7e@mail.fordibben.zone>
This commit is contained in:
Will McCall 2024-10-14 11:57:22 +01:00 committed by GitHub
parent 006396c310
commit 24087803b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 165 additions and 192 deletions

View File

@ -1,31 +0,0 @@
package me.ash.reader.ui.component.base
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.google.accompanist.swiperefresh.SwipeRefreshIndicator
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import me.ash.reader.ui.theme.palette.onDark
@Composable
fun SwipeRefresh(
isRefresh: Boolean = false,
onRefresh: () -> Unit = {},
content: @Composable () -> Unit = {},
) {
com.google.accompanist.swiperefresh.SwipeRefresh(
state = rememberSwipeRefreshState(isRefresh),
onRefresh = onRefresh,
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
fade = true,
scale = true,
contentColor = MaterialTheme.colorScheme.primary,
backgroundColor = MaterialTheme.colorScheme.surface onDark MaterialTheme.colorScheme.surfaceVariant,
)
}
) {
content()
}
}

View File

@ -1,16 +1,12 @@
package me.ash.reader.ui.page.home.feeds
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
@ -26,13 +22,15 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.UnfoldLess
import androidx.compose.material.icons.rounded.UnfoldMore
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -47,7 +45,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@ -90,7 +87,7 @@ import kotlin.collections.set
import kotlin.math.ln
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class
ExperimentalMaterial3Api::class
)
@Composable
fun FeedsPage(
@ -130,7 +127,17 @@ fun FeedsPage(
if (groupWithFeedList.isNotEmpty()) feedsUiState.listState else rememberLazyListState()
val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) }
val syncingState = rememberPullToRefreshState()
val syncingScope = rememberCoroutineScope()
val doSync:() -> Unit = {
isSyncing = true
syncingScope.launch {
homeViewModel.sync()
}
}
DisposableEffect(owner) {
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
@ -141,15 +148,6 @@ fun FeedsPage(
onDispose { homeViewModel.syncWorkLiveData.removeObservers(owner) }
}
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing)
)
)
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
val groupIndicatorAlpha by remember {
@ -221,15 +219,6 @@ fun FeedsPage(
}
},
actions = {
FeedbackIconButton(
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
enabled = !isSyncing
) {
if (!isSyncing) homeViewModel.sync()
}
if (subscribeViewModel.rssService.get().addSubscription) {
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
@ -241,153 +230,160 @@ fun FeedsPage(
}
},
content = {
LazyColumn(
state = listState
PullToRefreshBox(
state=syncingState,
isRefreshing = isSyncing,
onRefresh = doSync
) {
item {
DisplayText(
text = feedsUiState.account?.name ?: "",
desc = if (isSyncing) stringResource(R.string.syncing) else "",
) { accountTabVisible = true }
}
item {
Banner(
title = filterUiState.filter.toName(),
desc = importantSum,
icon = filterUiState.filter.iconOutline,
action = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
contentDescription = stringResource(R.string.go_to),
)
},
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = null,
)
)
LazyColumn(
modifier=Modifier.fillMaxSize(),
state = listState
) {
item {
DisplayText(
text = feedsUiState.account?.name ?: "",
desc = "",
) { accountTabVisible = true }
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 26.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(R.string.feeds),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
)
Row(
modifier = Modifier
.padding(end = 12.dp)
.size(20.dp)
.clip(CircleShape)
.clickable { if (hasGroupVisible) collapseAllGroups() else expandAllGroups() },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
item {
Banner(
title = filterUiState.filter.toName(),
desc = importantSum,
icon = filterUiState.filter.iconOutline,
action = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
contentDescription = stringResource(R.string.go_to),
)
},
) {
Icon(
imageVector = if (hasGroupVisible) Icons.Rounded.UnfoldLess else Icons.Rounded.UnfoldMore,
contentDescription = stringResource(R.string.unfold_less),
tint = MaterialTheme.colorScheme.primary,
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = null,
)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
val defaultGroupId = context.currentAccountId.getDefaultGroupId()
item {
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 26.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(R.string.feeds),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
)
Row(
modifier = Modifier
.padding(end = 12.dp)
.size(20.dp)
.clip(CircleShape)
.clickable { if (hasGroupVisible) collapseAllGroups() else expandAllGroups() },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = if (hasGroupVisible) Icons.Rounded.UnfoldLess else Icons.Rounded.UnfoldMore,
contentDescription = stringResource(R.string.unfold_less),
tint = MaterialTheme.colorScheme.primary,
)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(groupWithFeedList) { index, groupWithFeed ->
when (groupWithFeed) {
is GroupFeedsView.Group -> {
Spacer(modifier = Modifier.height(16.dp))
val defaultGroupId = context.currentAccountId.getDefaultGroupId()
if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) {
GroupItem(
isExpanded = {
groupsVisible.getOrPut(
groupWithFeed.group.id,
groupListExpand::value
)
},
group = groupWithFeed.group,
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
roundedBottomCorner = { index == groupWithFeedList.lastIndex || groupWithFeed.group.feeds == 0 },
onExpanded = {
groupsVisible[groupWithFeed.group.id] =
itemsIndexed(groupWithFeedList) { index, groupWithFeed ->
when (groupWithFeed) {
is GroupFeedsView.Group -> {
Spacer(modifier = Modifier.height(16.dp))
if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) {
GroupItem(
isExpanded = {
groupsVisible.getOrPut(
groupWithFeed.group.id,
groupListExpand::value
).not()
hasGroupVisible =
if (groupsVisible[groupWithFeed.group.id] == true) {
true
} else {
groupsVisible.any { it.value }
)
},
group = groupWithFeed.group,
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
roundedBottomCorner = { index == groupWithFeedList.lastIndex || groupWithFeed.group.feeds == 0 },
onExpanded = {
groupsVisible[groupWithFeed.group.id] =
groupsVisible.getOrPut(
groupWithFeed.group.id,
groupListExpand::value
).not()
hasGroupVisible =
if (groupsVisible[groupWithFeed.group.id] == true) {
true
} else {
groupsVisible.any { it.value }
}
},
onLongClick = {
scope.launch {
groupDrawerState.show()
}
},
onLongClick = {
scope.launch {
groupDrawerState.show()
}
}
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = groupWithFeed.group,
feed = null,
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = groupWithFeed.group,
feed = null,
)
)
)
}
}
}
}
is GroupFeedsView.Feed -> {
FeedItem(
feed = groupWithFeed.feed,
alpha = groupAlpha,
badgeAlpha = feedBadgeAlpha,
isEnded = { index == groupWithFeedList.lastIndex || groupWithFeedList[index + 1] is GroupFeedsView.Group },
isExpanded = {
groupsVisible.getOrPut(
groupWithFeed.feed.groupId,
groupListExpand::value
)
}, onClick = {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = groupWithFeed.feed,
is GroupFeedsView.Feed -> {
FeedItem(
feed = groupWithFeed.feed,
alpha = groupAlpha,
badgeAlpha = feedBadgeAlpha,
isEnded = { index == groupWithFeedList.lastIndex || groupWithFeedList[index + 1] is GroupFeedsView.Group },
isExpanded = {
groupsVisible.getOrPut(
groupWithFeed.feed.groupId,
groupListExpand::value
)
)
}, onLongClick = {
scope.launch {
feedDrawerState.show()
}, onClick = {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = groupWithFeed.feed,
)
)
}, onLongClick = {
scope.launch {
feedDrawerState.show()
}
}
}
)
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(128.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
item {
Spacer(modifier = Modifier.height(128.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
},

View File

@ -14,7 +14,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -55,13 +58,12 @@ import me.ash.reader.ui.component.base.DisplayText
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.component.base.RYExtensibleVisibility
import me.ash.reader.ui.component.base.RYScaffold
import me.ash.reader.ui.component.base.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.HomeViewModel
@OptIn(
androidx.compose.ui.ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class,
)
@Composable
fun FlowPage(
@ -94,7 +96,16 @@ fun FlowPage(
var onSearch by remember { mutableStateOf(false) }
val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) }
val syncingState = rememberPullToRefreshState()
val syncingScope = rememberCoroutineScope()
val doSync: () -> Unit = {
isSyncing = true
syncingScope.launch {
flowViewModel.sync()
}
}
DisposableEffect(owner) {
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
@ -242,14 +253,11 @@ fun FlowPage(
}
},
content = {
SwipeRefresh(
onRefresh = {
if (!isSyncing) {
flowViewModel.sync()
}
}
PullToRefreshBox(
state = syncingState,
isRefreshing = isSyncing,
onRefresh = doSync
) {
var showMenu by remember { mutableStateOf(false) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
@ -262,7 +270,7 @@ fun FlowPage(
filterUiState.feed != null -> filterUiState.feed.name
else -> filterUiState.filter.toName()
},
desc = if (isSyncing) stringResource(R.string.syncing) else "",
desc = "",
)
RYExtensibleVisibility(visible = markAsRead) {
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))