Jetpack Compose Banner out of the box

Keywords: Android Design Pattern Interview Programmer

Jetpack Compose does not have an official Banner control at present, so it can only be completed by writing and searching some materials. Thank you very much for sharing these contents before.

design sketch

accompanist group Library

accompanist

The group library aims to provide supplementary functions for jetpack composition. There are many experimental functions that are easy to use. The remeberimagepainter used to load network pictures is one of them, and the Pager library needs to be used as a Banner.

//Import dependency 
implementation "com.google.accompanist:accompanist-pager:$accompanist_pager"

I use 0.16.1 here, because other libraries are also this version. At present, the latest version is 0.18.0

critical code

1,rememberPagerState

The variable used to record paging status has five parameters, four of which are used, and the other is initialPageOffset, which can set the offset

val pagerState = rememberPagerState(
    //PageCount 
    pageCount = list.size,
    //Number of preloads
    initialOffscreenLimit = 1,
    //Infinite loop
    infiniteLoop = true,
    //Initial page
    initialPage = 0
)
2,HorizontalPager

It is used to create a page layout that can slide horizontally, and pass in the rememberPagerState above. Nothing else

HorizontalPager(
    state = pagerState,
    modifier = Modifier
        .fillMaxSize(),
) { page ->
    Image(
        painter = rememberImagePainter(list[page].imageUrl),
        modifier = Modifier.fillMaxSize(),
        contentScale = ContentScale.Crop,
        contentDescription = null
    )
}
3. Let HorizontalPager move by itself

There are two methods to make HorizontalPager move. One is animateScrollToPage * * * and the other is scrollToPage *. From the name, we can see that the method with animate has animation effect, which is exactly what I want.

//Auto scroll
LaunchedEffect(pagerState.currentPage) {
    if (pagerState.pageCount > 0) {
        delay(timeMillis)
        pagerState.animateScrollToPage((pagerState.currentPage + 1) % pagerState.pageCount)
    }
}

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

But this is a piece of code that looks ok

Assume that the total number of pages pagestate.pagecount is 2. When ((pagestate. CurrentPage + 1)% pagestate. Pagecount) = = 0, jump to the first page, but the final effect is like this

The rotation chart slides to the left, and the picture of the middle page of the rotation chart also appears, which feels a little flickering.

After modification
//Auto scroll
LaunchedEffect(pagerState.currentPage) {
    if (pagerState.pageCount > 0) {
        delay(timeMillis)
        //Here, you can cycle directly + 1, provided that the infiniteloop of pagerState = = true
        pagerState.animateScrollToPage(pagerState.currentPage + 1)
    }
}

Only the value of animateScrollToPage parameter has been modified. Some people may ask: will pagerState.currentPage + 1 not report an error?

Not really!

Because when the infiniteloop parameter in the rememberPagerState is set to true, the maximum page number is Int.MAX_VALUE, and currentPage is only the index of the current page, not the real page number.

In other words, when the Banner has four pages and a 5 is passed here, no error will be reported, and animateScrollToPage will automatically convert this "5" into a page index to ensure that there will be no error when using currentPage next time. (rookie, I've been looking at the source code for a while, but I haven't seen where this is turned)

However, some points deserve attention:

When pagerState.animateScrollToPage(target) is called

  • When target > pagecount or target > CurrentPage, the control slides to the right
  • When target < pagecount and target < CurrentPage, the control slides to the left
  • In addition, if the difference between currentPage and target is greater than 4, only four pages (currentPage, currentPage + 1, target - 1 and target) will be displayed in the animation

By analogy, if it is changed to - 1, it will automatically slide to the left

pagerState.animateScrollToPage(pagerState.currentPage - 1)

Several parameters are defined in the Banner. indicatorAlignment can set the position of the indication point, which is centered at the bottom by default

/** 
 * Rotation chart
 * [timeMillis] residence time
 * [loadImage] Load the layout displayed in
 * [indicatorAlignment] Indicates the position of the point. By default, it is the middle below the rotation chart, with a point padding
 * [onClick] Click event of rotation chart
 */
@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 = {}
)
Alignment.BottomStart

Alignment.BottomEnd

Found a strange problem

//Auto scroll
LaunchedEffect(pagerState.currentPage) {
    if (pagerState.pageCount > 0) {
        delay(timeMillis)
        //Here, you can cycle directly + 1 on the premise that infiniteLoop == true
        pagerState.animateScrollToPage(pagerState.currentPage - 1)
    }
}

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

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

like this

Problem solving

The solution to the problem is not complicated. You only need to record the current page index when your finger is pressed, judge whether the current page index has changed when your finger is raised, and manually trigger the animation if it has not changed.

PointerInput Modifier

This is a Modifier for handling gesture operations. It provides us with PointerInputScope scope, in which we can use some API s about gestures.

For example: detectDragGestures

We can get the callback of drag start / drag time / drag cancel / drag end in detectdragstyles, but ondrag (trigger callback during drag) is a required parameter, which will cause the drag gesture of HorizontalPager control to fail.

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

So finally, we use the more basic API - awaitPointerEvent. We need to use it within the scope of awaitpointereventscope provided by awaitpointereventscope method.

HorizontalPager(
    state = pagerState,
    modifier = Modifier.pointerInput(pagerState.currentPage) {
        awaitPointerEventScope {
            while (true) {
                //PointerEventPass.Initial - this control gives priority to handling gestures, and then gives them to sub components
                val event = awaitPointerEvent(PointerEventPass.Initial)
                //Get the first pressed finger
                val dragEvent = event.changes.firstOrNull()
                when {
                    //Has the current mobile gesture been consumed
                    dragEvent!!.positionChangeConsumed() -> {
                        return@awaitPointerEventScope
                    }
                    //Whether it has been pressed (ignore the pressed gesture consumption mark)
                    dragEvent.changedToDownIgnoreConsumed() -> {
                        //Record the current page index value
                        currentPageIndex = pagerState.currentPage
                    }
                    //Whether it has been lifted (ignore the pressed gesture consumption mark)
                    dragEvent.changedToUpIgnoreConsumed() -> {
                        //When the pageCount is greater than 1 and the finger is raised, if the page does not change, the animation will be triggered manually
                        if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
                            executeChangePage = !executeChangePage
                        }
                    }
                }
            }
        }
    }
       ...
)

In addition, since the rotation chart can click to jump to the details page, it is also necessary to distinguish between click events and sliding events. pagerState.targetPage is required (whether there is any scrolling / animation executing on the current page). If not, null will be returned.

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

//Whether it has been lifted (ignore the pressed gesture consumption mark)
dragEvent.changedToUpIgnoreConsumed() -> {
    //When the page does not have any scrolling / animation, pagerState.targetPage is null, which is a click event
    if (pagerState.targetPage == null) return@awaitPointerEventScope
    //When the pageCount is greater than 1 and the finger is raised, if the page does not change, the animation will be triggered manually
    if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
        executeChangePage = !executeChangePage
    }
}

solve! (the gif diagram is jammed when switching, and there is no problem on the real machine)

Ready to use

Give Kobayashi one star bar

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

/**

 * Rotation chart
 * [timeMillis] residence time
 * [loadImage] Load the layout displayed in
 * [indicatorAlignment] Indicates the position of the point. By default, it is the middle below the rotation chart, with a point padding
 * [onClick] Click event of rotation chart
 */
@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) {
            //Loading pictures in
            Image(
                painterResource(loadImage),
                modifier = Modifier.fillMaxSize(),
                contentDescription = null,
                contentScale = ContentScale.Crop
            )
        } else {
            val pagerState = rememberPagerState(
                //PageCount 
                pageCount = list.size,
                //Number of preloads
                initialOffscreenLimit = 1,
                //Infinite loop
                infiniteLoop = true,
                //Initial page
                initialPage = 0
            )

            //Monitor animation execution
            var executeChangePage by remember { mutableStateOf(false) }
            var currentPageIndex = 0

            //Auto scroll
            LaunchedEffect(pagerState.currentPage, executeChangePage) {
                if (pagerState.pageCount > 0) {
                    delay(timeMillis)
                    //Here, you can cycle directly + 1 on the premise that infiniteLoop == true
                    pagerState.animateScrollToPage(pagerState.currentPage + 1)
                }
            }

            HorizontalPager(
                state = pagerState,
                modifier = Modifier.pointerInput(pagerState.currentPage) {
                    awaitPointerEventScope {
                        while (true) {
                            //PointerEventPass.Initial - this control gives priority to handling gestures, and then gives them to sub components
                            val event = awaitPointerEvent(PointerEventPass.Initial)
                            //Get the first pressed finger
                            val dragEvent = event.changes.firstOrNull()
                            when {
                                //Has the current mobile gesture been consumed
                                dragEvent!!.positionChangeConsumed() -> {
                                    return@awaitPointerEventScope
                                }
                                //Whether it has been pressed (ignore the pressed gesture consumption mark)
                                dragEvent.changedToDownIgnoreConsumed() -> {
                                    //Record the current page index value
                                    currentPageIndex = pagerState.currentPage
                                }
                                //Whether it has been lifted (ignore the pressed gesture consumption mark)
                                dragEvent.changedToUpIgnoreConsumed() -> {
                                    //When the page does not have any scrolling / animation, pagerState.targetPage is null, which is a click event
                                    if (pagerState.targetPage == null) return@awaitPointerEventScope
                                    //When the pageCount is greater than 1 and the finger is raised, if the page does not change, the animation will be triggered manually
                                    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)
            ) {

                //Indication point
                Row(
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    for (i in list.indices) {
                        //size
                        var size by remember { mutableStateOf(5.dp) }
                        size = if (pagerState.currentPage == i) 7.dp else 5.dp

                        //colour
                        val color =
                            if (pagerState.currentPage == i) MaterialTheme.colors.primary else Color.Gray

                        Box(
                            modifier = Modifier.clip(CircleShape).background(color)
                                //When the size changes, it changes in the form of animation
                                .animateContentSize().size(size)
                        )
                        //Indicates the interval between points
                        if (i != list.lastIndex) Spacer(
                            modifier = Modifier.height(0.dp).width(4.dp)
                        )
                    }
                }

            }
        }

    }

}

/**
 * Carousel chart data
 */
data class BannerData(
    val imageUrl: String,
    val linkUrl: String
)

ending

A little buddy with learning can pay attention to my official account. ❤ Program development center ❤ ⅶ technology sharing will be done regularly every week. Join me and study with me!

Posted by tinyashcities on Fri, 19 Nov 2021 02:35:35 -0800