Skip to content

Commit

Permalink
[Pager] Add PagerState.pageChanges (#278)
Browse files Browse the repository at this point in the history
Allow apps to collect any current page changes via
a Flow.

* Add sample
* Update API
* Fix build
* Rename to PageState.pageChanges
* Doc tweak
* Use filterNot
  • Loading branch information
chrisbanes authored Mar 22, 2021
1 parent 4a160f5 commit 2816ce2
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 1 deletion.
1 change: 1 addition & 0 deletions pager/api/pager.api
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public final class com/google/accompanist/pager/PagerState$Companion {
}

public final class com/google/accompanist/pager/PagerStateKt {
public static final fun getPageChanges (Lcom/google/accompanist/pager/PagerState;)Lkotlinx/coroutines/flow/Flow;
public static final fun rememberPagerState (IIFLandroidx/compose/runtime/Composer;II)Lcom/google/accompanist/pager/PagerState;
}

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.produceIn
import kotlinx.coroutines.launch
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -189,6 +196,61 @@ abstract class PagerTest(
)
}

@OptIn(FlowPreview::class)
@Test
fun pageChangesSample() {
val pagerState = setPagerContent(
layoutDirection = layoutDirection,
pageCount = 10,
offscreenLimit = offscreenLimit,
)

val coroutineScope = CoroutineScope(Dispatchers.Main)
// Collect the pageChanges flow into a Channel, allowing us to poll values
val pageChangedChannel = pagerState.pageChanges.produceIn(coroutineScope)

// Assert that the first emission is 0
coroutineScope.launch {
assertThat(pageChangedChannel.receive()).isEqualTo(0)
}

// Now swipe to page 1..
composeTestRule.onNodeWithTag("0").swipeAcrossCenter(
distancePercentage = -MediumSwipeDistance,
velocity = MediumVelocity,
)
// ...and assert that the page 2 is emitted
coroutineScope.launch {
assertThat(pageChangedChannel.receive()).isEqualTo(1)
}

// Now swipe to page 2...
composeTestRule.onNodeWithTag("1").swipeAcrossCenter(
distancePercentage = -MediumSwipeDistance,
velocity = MediumVelocity,
)
// ...and assert that the page 2 is emitted
coroutineScope.launch {
assertThat(pageChangedChannel.receive()).isEqualTo(2)
}

// Now swipe back to page 1...
composeTestRule.onNodeWithTag("2").swipeAcrossCenter(
distancePercentage = MediumSwipeDistance,
velocity = MediumVelocity,
)
// ...and assert that the page 1 is emitted
coroutineScope.launch {
assertThat(pageChangedChannel.receive()).isEqualTo(1)
}

composeTestRule.waitForIdle()

// Cancel the channel and coroutine scope
pageChangedChannel.cancel()
coroutineScope.cancel()
}

@Test
@Ignore("Currently broken") // TODO: Will fix this once we move to Modifier.scrollable()
fun a11yScroll() {
Expand Down
1 change: 0 additions & 1 deletion pager/src/main/java/com/google/accompanist/pager/Pager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ private const val SnapSpringStiffness = 2750f

@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalPagerApi

@Immutable
Expand Down
22 changes: 22 additions & 0 deletions pager/src/main/java/com/google/accompanist/pager/PagerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlin.math.absoluteValue
import kotlin.math.floor
import kotlin.math.roundToInt
Expand Down Expand Up @@ -436,3 +441,20 @@ class PagerState(
)
}
}

/**
* A flow which emits the [PagerState.currentPage] as it changes due to a scroll completing.
*
* This flow is not meant to be used for updating any UI within the attached [HorizontalPager]
* or [VerticalPager]. For that use-case, you should read
* [PagerScope.currentPage] and [PagerScope.currentPageOffset] from the content scope.
*
* @sample com.google.accompanist.sample.pager.PageChangesSample
*/
@ExperimentalPagerApi
inline val PagerState.pageChanges: Flow<Int>
get() = snapshotFlow { isScrollInProgress }
// Only emit when the scroll has finished
.filterNot { it }
.map { currentPage }
.distinctUntilChanged()
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.VerticalPager
import com.google.accompanist.pager.VerticalPagerIndicator
import com.google.accompanist.pager.pageChanges
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.flow.collect

@OptIn(ExperimentalPagerApi::class)
@Composable
Expand Down Expand Up @@ -101,3 +104,26 @@ fun VerticalPagerIndicatorSample() {
)
}
}

@Suppress("UNUSED_PARAMETER")
object AnalyticsService {
fun sendPageSelectedEvent(page: Int) = Unit
}

@OptIn(ExperimentalPagerApi::class)
@Composable
fun PageChangesSample() {
val pagerState = rememberPagerState(pageCount = 10)

LaunchedEffect(pagerState) {
// Collect from the PageState's pageChanges flow, which emits when the
// current page has changed
pagerState.pageChanges.collect { page ->
AnalyticsService.sendPageSelectedEvent(page)
}
}

VerticalPager(state = pagerState) { page ->
Text(text = "Page: $page")
}
}

0 comments on commit 2816ce2

Please sign in to comment.