mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-05 10:15:30 +08:00
Merge pull request #10 from oleksandrbalan/feature/dynamic-max
Add dynamic max support
This commit is contained in:
37
README.md
37
README.md
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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()
|
||||
|
@@ -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])
|
||||
}
|
||||
}
|
||||
|
@@ -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])
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,104 @@
|
||||
@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>() {
|
||||
|
||||
@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 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)
|
||||
}
|
||||
}
|
@@ -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])
|
||||
}
|
||||
|
||||
|
@@ -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])
|
||||
}
|
||||
}
|
||||
|
@@ -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])
|
||||
}
|
||||
}
|
||||
|
@@ -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") {
|
||||
|
@@ -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,16 +20,17 @@ 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.setup(constraints)
|
||||
state.setup(count, constraints)
|
||||
|
||||
val updatedCurrent by rememberUpdatedState(state.current)
|
||||
val internalState by rememberUpdatedState(state.internalState ?: return@BoxWithConstraints)
|
||||
|
||||
val config by rememberUpdatedState(state.config)
|
||||
@@ -82,3 +84,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,
|
||||
)
|
||||
}
|
||||
|
@@ -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,9 +116,18 @@ public class PageCurlState(
|
||||
*/
|
||||
public val progress: Float get() = internalState?.progress ?: 0f
|
||||
|
||||
internal var internalState: InternalState? by mutableStateOf(null)
|
||||
internal var max: Int = initialMax
|
||||
private set
|
||||
|
||||
internal var internalState: InternalState? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
internal fun setup(count: Int, constraints: Constraints) {
|
||||
max = count
|
||||
if (current >= count) {
|
||||
current = count - 1
|
||||
}
|
||||
|
||||
internal fun setup(constraints: Constraints) {
|
||||
if (internalState?.constraints == constraints) {
|
||||
return
|
||||
}
|
||||
|
Reference in New Issue
Block a user