Jetpack Compose currently has no official Banner control, so I had to write it by myself. I searched a lot of information to complete it. Thank you very much for sharing this content.

rendering

Accompanist group library

accompanist

RememberImagePainter – a library that complements Jetpack Compose. It has a number of excellent experimental functions. One of them is the rememberImagePainter that I used before for loading web images.

/ / import depend on the implementation of "com. Google. Accompanist: accompanist - pager: $accompanist_pager"Copy the code

I’m using 0.16.1 here because it’s the same version as the other libraries, the latest being 0.18.0

The key code

1, rememberPagerState

The variable used to record the paging status. We used four of the five parameters, plus initialPageOffset, which can be set as an offset

PageCount = list.size, // rememberPagerState = rememberPagerState // If infiniteLoop = true, // initialPage = 0)Copy the code
2, HorizontalPager

Use to create a paging layout that slides horizontally, pass in the rememberPagerState above, and nothing else

HorizontalPager(
    state = pagerState,
    modifier = Modifier
        .fillMaxSize(),
) { page ->
    Image(
        painter = rememberImagePainter(list[page].imageUrl),
        modifier = Modifier.fillMaxSize(),
        contentScale = ContentScale.Crop,
        contentDescription = null
    )
}
Copy the code
3. Make the HorizontalPager work on its own

There are two methods that animate the HorizontalPager, animateScrollToPage and scrollToPage. After animate, you can animate the HorizontalPager using animateScrollToPage.

LaunchedEffect(pagerState.currentPage) {if (pagerState.pagecount > 0) {delay(timeMillis) pagerState.animateScrollToPage((pagerState.currentPage + 1) % pagerState.pageCount) } }Copy the code

Add this line of code to the control to make it automatically rise

But this is a piece of code that looks fine

If ((pagerState.CurrentPage + 1) % pagerState.pagecount) == 0, it will jump to the first page, but it will look like this

The wheel image slid to the left, and the middle page of the wheel image appeared, which finally caused the page to flicker a little.

The modified
LaunchedEffect(pagerState.currentPage) {if (pagerState.pagecount > 0) {delay(timeMillis) The premise is pagerState infiniteLoop = = true pagerState. AnimateScrollToPage (pagerState. CurrentPage + 1)}}Copy the code

Pagerstate.currentpage + 1 does not return an error.

No!

Because the maximum page number when the infiniteLoop parameter in rememberPagerState is set to true is actually in.max_value and currentPage is just the index of the currentPage, not the actual page number.

This means that no error is reported when the Banner has 4 pages and a 5 is passed, and animateScrollToPage will automatically convert the “5” to the page index to ensure that currentPage will not fail next time. Rookie, me! I haven’t seen the source code for a while.

But there are a few things worth noting:

Call pagerState. AnimateScrollToPage (target)

  • The control slides to the right when target > pageCount or Target > currentPage
  • When target < pageCount and target < currentPage, the control slides left
  • If the difference between currentPage and target is greater than 4, only 4 pages will be displayed in the animation (currentPage, currentPage + 1, target-1, target)

And so on, if you change it to minus 1 it automatically slides to the left

pagerState.animateScrollToPage(pagerState.currentPage - 1)
Copy the code

IndicatorAlignment Several parameters are defined in the Banner. IndicatorAlignment can set the position of indicator points. By default, the bottom is centered

/** * [timeMillis] residence time * [loadImage] Layout displayed during loading * [indicatorAlignment] Position of points, default is the lower middle of the alignment, with a bit of padding * [onClick] */ @experimentalcoilAPI @experimentalPagerAPI @composable fun Banner(list: list <BannerData>? , timeMillis: Long = 3000, @DrawableRes loadImage: Int = R.mipmap.ic_web, indicatorAlignment: Alignment = Alignment.BottomCenter, onClick: (link: String) -> Unit = {} )Copy the code
Alignment.BottomStart

Alignment.BottomEnd

I found a strange problem

LaunchedEffect(pagerState.currentPage) {if (pagerState.pagecount > 0) {delay(timeMillis) Premise is infiniteLoop = = true pagerState. AnimateScrollToPage (pagerState. CurrentPage - 1)}}Copy the code

In this code, the ReCompose timing is when the pagerState.currentPage value changes; When we touch the HorizontalPager control, the animation will suspend and cancel.

Therefore, when we slide but do not slide to the previous or next page, and only release the finger after the jump page animation is triggered, the auto-scrolling stop problem will occur.

Like this,

Problem solving

The solution to the problem is not complicated, only need to record the current page index when the finger is pressed, the finger is lifted to determine whether the current page index has changed, if not changed, manually trigger animation.

PointerInput Modifier

This is the Modifier used to handle gestures. It provides us with the PointerInputScope scope, where we can use some gestures apis.

For example: detectDragGestures

DetectDragGestures we can get the drag-start/drag-start/drag-cancel/drag-end callback in detectDragGestures, but onDrag(drag-trigger callback) is a required parameter, which invalidates the HorizontalPager drag gesture.

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)
Copy the code

So we ended up using the more basic API, AWaitPoInterEventEvent, which we need to use within the awaitPointerEventScope scope provided to us by the awaitPointerEventScope method.

HorizontalPager( state = pagerState, modifier = Modifier.pointerInput(pagerState.currentPage) { awaitPointerEventScope { while (true) { / / PointerEventPass. Initial - this controls priority gestures, Val event = awaitPointerEvent(pointerEventPass.initial) // Retrieve the first finger val dragEvent = Event. Changes. FirstOrNull () the when {/ / whether the current mobile gestures have been spending dragEvent!! .positionChangeconsumed () -> {return@awaitPointerEventScope} DragEvent. ChangedToDownIgnoreConsumed () - > {/ / record the current page index value currentPageIndex = pagerState. CurrentPage} / / have to lift (ignoring press gesture has consumption markup) dragEvent. ChangedToUpIgnoreConsumed () - > {/ / when pageCount is greater than 1, and finger up if the page has not changed, If (currentPageIndex == PagerState.CurrentPage && PagerState.pagecount > 1) {executeChangePage =! executeChangePage } } } } } } ... )Copy the code

In addition, since the wheel diagram can be clicked to jump to the detail page, there is a need to distinguish between click events and slide events, using pagerState.targetPage (whether there is any scroll/animation currently executing on the page), and returning null if not.

But as long as the user drags the Banner, the targetPage will not be null when the user lets go.

/ / have to lift (ignoring press gesture has consumption markup) dragEvent. ChangedToUpIgnoreConsumed () - > {/ / when the page without any scrolling/animation pagerState. TargetPage is null, If (pagerState.targetPage == null) return@awaitPointerEventScope // If pageCount is greater than 1 and the page does not change when the finger is lifted, If (currentPageIndex == PagerState.CurrentPage && PagerState.pagecount > 1) {executeChangePage =! executeChangePage } }Copy the code

Solve! (THE GIF image is stuck while switching, no problem on the real phone)

Namely take the box

Give Xiao Lin a star

import androidx.annotation.DrawableRes import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import coil.annotation.ExperimentalCoilApi import coil.compose.rememberImagePainter import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import Kotlinx.coroutines. delay /** * Castchart * [timeMillis] dwell time * [loadImage] Layout shown in loading * [indicatorAlignment] */ @experimentalcoilAPI @experimentalPagerAPI @composable fun Banner(  list: List<BannerData>?, timeMillis: Long = 3000, @DrawableRes loadImage: Int = R.mipmap.ic_web, indicatorAlignment: Alignment = Alignment.BottomCenter, onClick: (link: String) -> Unit = {} ) { Box( modifier = Modifier.background(MaterialTheme.colors.background).fillMaxWidth() .height(220.dp)) {if (list == null) {mysql_modifier (painterResource(loadImage), mysql = mysql.fillmaxSize (), contentDescription = null, ContentScale = contentscale.crop)} else {rememberPagerState = rememberPagerState(// InitialOffscreenLimit = 1, // If infiniteLoop = true, Var executeChangePage by remember {mutableStateOf(false)} var currentPageIndex = 0 // Automatically scroll LaunchedEffect(pagerState.currentPage, ExecuteChangePage) {if (pagerstate.pagecount > 0) {delay(timeMillis) Premise is infiniteLoop = = true pagerState. AnimateScrollToPage (pagerState. CurrentPage + 1)}} HorizontalPager (state = pagerState, modifier = Modifier.pointerInput(pagerState.currentPage) { awaitPointerEventScope { while (true) { / / PointerEventPass. Initial - this controls priority gestures, Val event = awaitPointerEvent(pointerEventPass.initial) // Retrieve the first finger val dragEvent = Event. Changes. FirstOrNull () the when {/ / whether the current mobile gestures have been spending dragEvent!! .positionChangeconsumed () -> {return@awaitPointerEventScope} DragEvent. ChangedToDownIgnoreConsumed () - > {/ / record the current page index value currentPageIndex = pagerState. CurrentPage} / / have to lift (ignoring press gesture has consumption markup) dragEvent. ChangedToUpIgnoreConsumed () - > {/ / when the page without any scrolling/animation pagerState. TargetPage is null, If (pagerState.targetPage == null) return@awaitPointerEventScope // If pageCount is greater than 1 and the page does not change when the finger is lifted, If (currentPageIndex == PagerState.CurrentPage && PagerState.pagecount > 1) {executeChangePage =! executeChangePage } } } } } } .clickable(onClick = { onClick(list[pagerState.currentPage].linkUrl) }) .fillMaxSize(), ) { page -> Image( painter = rememberImagePainter(list[page].imageUrl), modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, contentDescription = null ) } Box( modifier = Modifier.align(indicatorAlignment) .padding(bottom = 6.dp, start = 6.dp, End = 6.dp)) {Row(horizontalArrangement = Arrangement.center, VerticalAlignment = Alignment. CenterVertically) {for (I in the list. The indices) {/ var/size size by remember { MutableStateOf (5.dp)} size = if (pagerState.currentPage == I) 7.dp else 5.dp val color = if (pagerState.currentPage == i) MaterialTheme.colors.primary else Color.Gray Box( modifier = AnimateContentSize ().size(size)) // Indicate the interval between points if (I! Spacer(modifier = modifier.height (0.dp).width(4.dp))}}}}}} /** ** data */ data class BannerData( val imageUrl: String, val linkUrl: String )Copy the code

Special thanks to

RugerMc gesture processing

Apk download link

The project address

Welcome to Star ~ PlayAndroid