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