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
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!