Add dynamic max support

This commit is contained in:
Oleksandr Balan
2023-03-21 17:35:16 +01:00
parent 2c1d7b3ef9
commit 27a3ae2e79
14 changed files with 237 additions and 26 deletions

View File

@@ -46,13 +46,12 @@ dependencies {
### Use in Composable
The `PageCurl` has 2 mandatory arguments:
* **state** - The state of the PageCurl. Use it to programmatically change the current page or observe changes, and to configure shadow, back-page and interactions.
* **content** - The content lambda to provide the page composable. Receives the page number.
* **count** - The count of pages.
* **content** - The content lambda to provide the page composable. Receives the page number.
```
val pages = listOf("One", "Two", "Three")
val state = rememberPageCurlState(max = pages.size)
PageCurl(state = state) { index ->
PageCurl(count = pages.size) { index ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
@@ -67,6 +66,36 @@ PageCurl(state = state) { index ->
}
```
Optionally `state` could be provided to observe and manage PageCurl state.
* **state** - The state of the PageCurl. Use it to programmatically change the current page or observe changes, and to configure shadow, back-page and interactions.
```
Column {
val scope = rememberCoroutineScope()
val state = rememberPageCurlState()
Button(onClick = { scope.launch { state.next() } }) {
Text(text = "Next")
}
val pages = listOf("One", "Two", "Three")
PageCurl(
count = pages.size,
state = state,
) { 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

@@ -8,6 +8,8 @@ buildscript {
compose_compiler_version = "1.4.2"
accompanist_version = "0.28.0"
activity_version = "1.6.1"
paging_version = "3.1.1"
paging_compose_version = "1.0.0-alpha18"
}
}
plugins {

View File

@@ -55,4 +55,6 @@ dependencies {
implementation("androidx.compose.material:material")
implementation("androidx.activity:activity-compose:$activity_version")
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanist_version")
implementation("androidx.paging:paging-runtime:$paging_version")
implementation("androidx.paging:paging-compose:$paging_compose_version")
}

View File

@@ -14,6 +14,10 @@ enum class Example(
"Simple Page Curl",
"Basic PageCurl usage"
),
PagingPageCurl(
"PageCurl with lazy paging",
"Example how component could be used with paging implementation"
),
SettingsPageCurl(
"Page Curl With Settings",
"Showcases how individual interactions can be toggled on / off"

View File

@@ -31,6 +31,7 @@ import androidx.core.view.WindowCompat
import eu.wewox.pagecurl.components.TopBar
import eu.wewox.pagecurl.screens.BackPagePageCurlScreen
import eu.wewox.pagecurl.screens.InteractionConfigInPageCurlScreen
import eu.wewox.pagecurl.screens.PagingPageCurlScreen
import eu.wewox.pagecurl.screens.SettingsPageCurlScreen
import eu.wewox.pagecurl.screens.ShadowInPageCurlScreen
import eu.wewox.pagecurl.screens.SimplePageCurlScreen
@@ -61,6 +62,7 @@ class MainActivity : ComponentActivity() {
when (selected) {
null -> RootScreen(onExampleClick = { example = it })
Example.SimplePageCurl -> SimplePageCurlScreen()
Example.PagingPageCurl -> PagingPageCurlScreen()
Example.SettingsPageCurl -> SettingsPageCurlScreen()
Example.StateInPageCurl -> StateInPageCurlScreen()
Example.InteractionConfigInPageCurl -> InteractionConfigInPageCurlScreen()

View File

@@ -53,7 +53,6 @@ fun BackPagePageCurlScreen() {
val pages = remember { HowToPageData.backPageHowToPages }
var zoomOut by remember { mutableStateOf(false) }
val state = rememberPageCurlState(
max = pages.size,
config = rememberPageCurlConfig(
onCustomTap = { size, position ->
// When PageCurl is zoomed out then zoom back in
@@ -76,7 +75,10 @@ fun BackPagePageCurlScreen() {
config = state.config,
bottom = { SettingsRow(state.config) },
) {
PageCurl(state = state) { index ->
PageCurl(
count = pages.size,
state = state,
) { index ->
HowToPage(index, pages[index])
}
}

View File

@@ -48,7 +48,6 @@ fun InteractionConfigInPageCurlScreen() {
val pages = remember { HowToPageData.interactionSettingsHowToPages }
var zoomOut by remember { mutableStateOf(false) }
val state = rememberPageCurlState(
max = pages.size,
config = rememberPageCurlConfig(
onCustomTap = { size, position ->
// When PageCurl is zoomed out then zoom back in
@@ -71,7 +70,10 @@ fun InteractionConfigInPageCurlScreen() {
config = state.config,
bottom = { SettingsRow(state.config) },
) {
PageCurl(state = state) { index ->
PageCurl(
count = pages.size,
state = state,
) { index ->
HowToPage(index, pages[index])
}
}

View File

@@ -0,0 +1,103 @@
@file:OptIn(ExperimentalPageCurlApi::class)
package eu.wewox.pagecurl.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.compose.collectAsLazyPagingItems
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.page.PageCurl
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
/**
* PageCurl with lazy paging.
* Example how component could be used with paging implementation.
*/
@Composable
fun PagingPageCurlScreen() {
val items = rememberPager().collectAsLazyPagingItems()
val extraItem = items.loadState.append is LoadState.Loading || items.loadState.refresh is LoadState.Loading
val count = items.itemCount + (if (extraItem) 1 else 0)
Box(Modifier.fillMaxSize()) {
PageCurl(count = count) { index ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
if (index < items.itemCount) {
Text(items[index]?.content.orEmpty())
} else {
CircularProgressIndicator()
}
}
}
}
}
@Composable
private fun rememberPager(): Flow<PagingData<Item>> =
remember {
Pager(
config = PagingConfig(
pageSize = 10,
prefetchDistance = 3,
),
pagingSourceFactory = { ItemPagingSource(BackendService()) }
).flow
}
private class Item(val content: String)
private class Response(val items: List<Item>, val nextPageNumber: Int)
private class BackendService {
suspend fun searchItems(page: Int): Response {
delay(5_000L)
return Response(
items = List(10) { Item("Content #${page * 10 + it}") },
nextPageNumber = page + 1
)
}
}
private class ItemPagingSource(private val backend: BackendService) : PagingSource<Int, Item>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
try {
val nextPageNumber = params.key ?: 0
val response = backend.searchItems(nextPageNumber)
LoadResult.Page(
data = response.items,
prevKey = null, // Only paging forward.
nextKey = response.nextPageNumber
)
} catch (e: Exception) {
LoadResult.Error(e)
}
override fun getRefreshKey(state: PagingState<Int, Item>): Int? =
state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

View File

@@ -34,7 +34,6 @@ fun SettingsPageCurlScreen() {
var showPopup by rememberSaveable { mutableStateOf(false) }
val state = rememberPageCurlState(
max = pages.size,
config = rememberPageCurlConfig(
onCustomTap = { size, position ->
// Detect tap somewhere in the center with 64 radius and show popup
@@ -48,7 +47,10 @@ fun SettingsPageCurlScreen() {
)
)
PageCurl(state = state) { index ->
PageCurl(
count = pages.size,
state = state,
) { index ->
HowToPage(index, pages[index])
}

View File

@@ -40,7 +40,6 @@ fun ShadowInPageCurlScreen() {
val pages = remember { HowToPageData.shadowHowToPages }
var zoomOut by remember { mutableStateOf(false) }
val state = rememberPageCurlState(
max = pages.size,
config = rememberPageCurlConfig(
onCustomTap = { size, position ->
// When PageCurl is zoomed out then zoom back in
@@ -63,7 +62,10 @@ fun ShadowInPageCurlScreen() {
config = state.config,
bottom = { SettingsRow(state.config) },
) {
PageCurl(state = state) { index ->
PageCurl(
count = pages.size,
state = state,
) { index ->
HowToPage(index, pages[index])
}
}

View File

@@ -11,19 +11,17 @@ import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.HowToPageData
import eu.wewox.pagecurl.components.HowToPage
import eu.wewox.pagecurl.page.PageCurl
import eu.wewox.pagecurl.page.rememberPageCurlState
/**
* Page Curl With Settings.
* Showcases how individual interactions can be toggled on / off.
* Simple Page Curl.
* Basic PageCurl usage.
*/
@Composable
fun SimplePageCurlScreen() {
Box(Modifier.fillMaxSize()) {
val pages = remember { HowToPageData.simpleHowToPages }
val state = rememberPageCurlState(max = pages.size)
PageCurl(state = state) { index ->
PageCurl(count = pages.size) { index ->
HowToPage(index, pages[index])
}
}

View File

@@ -45,7 +45,6 @@ fun StateInPageCurlScreen() {
val pages = remember { HowToPageData.stateHowToPages }
var zoomOut by remember { mutableStateOf(false) }
val state = rememberPageCurlState(
max = pages.size,
config = rememberPageCurlConfig(
onCustomTap = { size, position ->
// When PageCurl is zoomed out then zoom back in
@@ -66,9 +65,12 @@ fun StateInPageCurlScreen() {
ZoomOutLayout(
zoomOut = zoomOut,
config = state.config,
bottom = { SettingsRow(state) },
bottom = { SettingsRow(pages.size, state) },
) {
PageCurl(state = state) { index ->
PageCurl(
count = pages.size,
state = state,
) { index ->
HowToPage(index, pages[index])
}
}
@@ -77,6 +79,7 @@ fun StateInPageCurlScreen() {
@Composable
private fun SettingsRow(
max: Int,
state: PageCurlState,
modifier: Modifier = Modifier
) {
@@ -92,7 +95,7 @@ private fun SettingsRow(
}
SettingsRowButton("Snap to last") {
state.snapTo(state.max)
state.snapTo(max)
}
SettingsRowButton("Snap forward") {

View File

@@ -12,6 +12,7 @@ import eu.wewox.pagecurl.ExperimentalPageCurlApi
/**
* Shows the pages which may be turned by drag or tap gestures.
*
* @param count The count of pages.
* @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.
@@ -19,14 +20,16 @@ import eu.wewox.pagecurl.ExperimentalPageCurlApi
@ExperimentalPageCurlApi
@Composable
public fun PageCurl(
state: PageCurlState,
count: Int,
modifier: Modifier = Modifier,
state: PageCurlState = rememberPageCurlState(),
content: @Composable (Int) -> Unit
) {
val scope = rememberCoroutineScope()
val updatedCurrent by rememberUpdatedState(state.current)
BoxWithConstraints(modifier) {
state.max = count
state.setup(constraints)
val internalState by rememberUpdatedState(state.internalState ?: return@BoxWithConstraints)
@@ -82,3 +85,26 @@ public fun PageCurl(
}
}
}
/**
* Shows the pages which may be turned by drag or tap gestures.
*
* @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
@Deprecated("Specify 'max' as 'count' in PageCurl composable.")
public fun PageCurl(
state: PageCurlState,
modifier: Modifier = Modifier,
content: @Composable (Int) -> Unit
) {
PageCurl(
count = state.max,
state = state,
modifier = modifier,
content = content,
)
}

View File

@@ -24,6 +24,37 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Remembers the [PageCurlState].
*
* @param initialCurrent The initial current page.
* @param config The configuration for PageCurl.
* @return The remembered [PageCurlState].
*/
@ExperimentalPageCurlApi
@Composable
public fun rememberPageCurlState(
initialCurrent: Int = 0,
config: PageCurlConfig = rememberPageCurlConfig()
): PageCurlState =
rememberSaveable(
initialCurrent,
saver = Saver(
save = { it.current },
restore = {
PageCurlState(
initialCurrent = it,
config = config,
)
}
)
) {
PageCurlState(
initialCurrent = initialCurrent,
config = config,
)
}
/**
* Remembers the [PageCurlState].
*
@@ -34,6 +65,7 @@ import kotlinx.coroutines.withContext
*/
@ExperimentalPageCurlApi
@Composable
@Deprecated("Specify 'max' as 'count' in PageCurl composable.")
public fun rememberPageCurlState(
max: Int,
initialCurrent: Int = 0,
@@ -45,16 +77,16 @@ public fun rememberPageCurlState(
save = { it.current },
restore = {
PageCurlState(
max = max,
initialCurrent = it,
initialMax = max,
config = config,
)
}
)
) {
PageCurlState(
max = max,
initialCurrent = initialCurrent,
initialMax = max,
config = config,
)
}
@@ -62,14 +94,14 @@ public fun rememberPageCurlState(
/**
* The state of the PageCurl.
*
* @property max The max number of pages.
* @property config The configuration for PageCurl.
* @param initialMax The initial max number of pages.
* @param initialCurrent The initial current page.
*/
@ExperimentalPageCurlApi
public class PageCurlState(
public val max: Int,
public val config: PageCurlConfig,
initialMax: Int = 0,
initialCurrent: Int = 0,
) {
/**
@@ -84,6 +116,8 @@ public class PageCurlState(
*/
public val progress: Float get() = internalState?.progress ?: 0f
internal var max: Int = initialMax
internal var internalState: InternalState? by mutableStateOf(null)
internal fun setup(constraints: Constraints) {