Merge pull request #12 from oleksandrbalan/feature/keys

Add PageCurl override with page keys
This commit is contained in:
Oleksandr Balan
2023-03-28 17:56:09 +02:00
committed by GitHub
4 changed files with 109 additions and 18 deletions

View File

@@ -96,6 +96,34 @@ Column {
}
```
Optionally `key` lambda could be provided with stable key for each item. PageCurl with keys for each page will correctly preserve a current position when items are added or removed.
* **key** - The lambda to provide stable key for each item. Useful when adding and removing items before current page.
```
Column {
var pages by remember { mutableStateOf(listOf("Four", "Five", "Six")) }
Button(onClick = { pages = listOf("One", "Two", "Three") + pages }) {
Text(text = "Prepend new pages")
}
PageCurl(
count = pages.size,
key = { pages[it].hashCode() },
) { index ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
) {
Text(
text = pages[index],
style = MaterialTheme.typography.h1,
)
}
}
}
```
See Demo application and [examples](demo/src/main/kotlin/eu/wewox/pagecurl/screens) for more usage examples.
https://user-images.githubusercontent.com/20944869/185782671-2861c2ed-c033-4318-bf12-1d8db74fc8b5.mp4

View File

@@ -3,7 +3,9 @@
package eu.wewox.pagecurl.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
@@ -12,7 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.paging.LoadState
import androidx.paging.LoadState.Loading
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
@@ -21,6 +23,7 @@ import androidx.paging.PagingState
import androidx.paging.compose.collectAsLazyPagingItems
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.page.PageCurl
import eu.wewox.pagecurl.page.rememberPageCurlState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -30,23 +33,38 @@ import kotlinx.coroutines.flow.Flow
*/
@Composable
fun PagingPageCurlScreen() {
val items = rememberPager().collectAsLazyPagingItems()
val startPage = 4 // in range 0..9
val startPageOffset = 5 // in range 0..9
val extraItem = items.loadState.append is LoadState.Loading || items.loadState.refresh is LoadState.Loading
val count = items.itemCount + (if (extraItem) 1 else 0)
val items = rememberPager(startPage).collectAsLazyPagingItems()
val loading = with(items.loadState) { refresh is Loading || append is Loading || prepend is Loading }
Box(Modifier.fillMaxSize()) {
PageCurl(count = count) { index ->
if (items.loadState.refresh is Loading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
if (index < items.itemCount) {
CircularProgressIndicator()
}
} else {
PageCurl(
count = items.itemCount,
key = { index -> items.peek(index).hashCode() },
state = rememberPageCurlState(initialCurrent = startPageOffset),
) { index ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
Text(items[index]?.content.orEmpty())
} else {
CircularProgressIndicator()
Text(if (loading) "Loading..." else "")
}
}
}
@@ -54,43 +72,47 @@ fun PagingPageCurlScreen() {
}
@Composable
private fun rememberPager(): Flow<PagingData<Item>> =
private fun rememberPager(startPage: Int): Flow<PagingData<Item>> =
remember {
Pager(
config = PagingConfig(
pageSize = 10,
prefetchDistance = 3,
),
pagingSourceFactory = { ItemPagingSource(BackendService()) }
pagingSourceFactory = { ItemPagingSource(startPage, BackendService()) }
).flow
}
private class Item(val content: String)
private class Response(val items: List<Item>, val nextPageNumber: Int)
private class Response(val items: List<Item>, val prePageNumber: Int, val nextPageNumber: Int)
private class BackendService {
suspend fun searchItems(page: Int): Response {
delay(5_000L)
delay(1_000L)
return Response(
items = List(10) { Item("Content #${page * 10 + it}") },
nextPageNumber = page + 1
prePageNumber = page - 1,
nextPageNumber = page + 1,
)
}
}
private class ItemPagingSource(private val backend: BackendService) : PagingSource<Int, Item>() {
private class ItemPagingSource(
private val startPage: Int,
private val backend: BackendService
) : PagingSource<Int, Item>() {
@Suppress("TooGenericExceptionCaught") // It is only for demo purpose
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
try {
val nextPageNumber = params.key ?: 0
val nextPageNumber = params.key ?: startPage
val response = backend.searchItems(nextPageNumber)
LoadResult.Page(
data = response.items,
prevKey = null, // Only paging forward.
nextKey = response.nextPageNumber
prevKey = response.prePageNumber.takeIf { it >= 0 },
nextKey = response.nextPageNumber.takeIf { it < 10 }
)
} catch (e: Exception) {
LoadResult.Error(e)

View File

@@ -4,8 +4,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import eu.wewox.pagecurl.ExperimentalPageCurlApi
@@ -85,6 +88,44 @@ public fun PageCurl(
}
}
/**
* Shows the pages which may be turned by drag or tap gestures.
*
* @param count The count of pages.
* @param key The lambda to provide stable key for each item. Useful when adding and removing items before current page.
* @param state The state of the PageCurl. Use this to programmatically change the current page or observe changes.
* @param modifier The modifier for this composable.
* @param content The content lambda to provide the page composable. Receives the page number.
*/
@ExperimentalPageCurlApi
@Composable
public fun PageCurl(
count: Int,
key: (Int) -> Any,
modifier: Modifier = Modifier,
state: PageCurlState = rememberPageCurlState(),
content: @Composable (Int) -> Unit
) {
var lastKey by remember(state.current) { mutableStateOf(if (count > 0) key(state.current) else null) }
remember(count) {
val newKey = if (count > 0) key(state.current) else null
if (newKey != lastKey) {
val index = List(count, key).indexOf(lastKey).coerceIn(0, count - 1)
lastKey = newKey
state.current = index
}
count
}
PageCurl(
count = count,
state = state,
content = content,
modifier = modifier,
)
}
/**
* Shows the pages which may be turned by drag or tap gestures.
*

View File

@@ -125,7 +125,7 @@ public class PageCurlState(
internal fun setup(count: Int, constraints: Constraints) {
max = count
if (current >= count) {
current = count - 1
current = (count - 1).coerceAtLeast(0)
}
if (internalState?.constraints == constraints) {