前言

  Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。

  本次就使用 Jetpack Compose里面的分页器(pager)来实现一个常用控件——轮播图(Banner)。这里需要对Android Compose有一定的了解,可以先去学习一下Compose的基本知识,这里放上官方直达:直达官方Compose

  同时这里也放上分页器的官方文档,可以了解更多高级功能,直达:官方分页器文档

需求分析

  首先需要分析一下实现轮播图的功能。

  轮播图需要一直循环滑动播放,所以首先就是需要一个具有单页水平滑动效果的控件。在2023年3月底,Google 正式发布Jetpack Compose 的 1.4 版本,在这次的更新中,新增了 pager 等控件,它实现了View中ViewPager类似的功能。因此,水平分页器—— HorizontalPager 可以完美起这个重任。

  可以分析一下HorizontalPager的源码

@Composable
@ExperimentalFoundationApi
fun HorizontalPager(
    state: PagerState,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    pageSize: PageSize = PageSize.Fill,
    beyondBoundsPageCount: Int = 0,
    pageSpacing: Dp = 0.dp,
    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
    userScrollEnabled: Boolean = true,
    reverseLayout: Boolean = false,
    key: ((index: Int) -> Any)? = null,
    pageNestedScrollConnection: NestedScrollConnection = 
    	PagerDefaults.pageNestedScrollConnection(
        	Orientation.Horizontal
    ),
    pageContent: @Composable PagerScope.(page: Int) -> Unit
) 

  其中最重要的当然是PagerState,顾名思义,这个就是用来管理HorizontalPager内pager状态的。这里是HorizontalPager的简单使用。

 // 显示 10 个项目
 HorizontalPager(pageCount = 10) { page ->
     // 每一页的内容,比如显示个文本
     Text(
         text = "Page: $page",
         modifier = Modifier.fillMaxWidth()
     )
 }

  然后就是在人没有对其进行滑动的时候,需要轮播图进行自动循环,并具有延迟展示图片内容。PagerState提供了方法可以获得当前Pager是否处于拖动状态。所以可以先使用这个方法获得当前状态,然后进行判断。延迟则可以使用delay函数来实现。

val isDragged by pagerState.interactionSource.collectIsDraggedAsState()

  最后还需要一个指示器,也可以使用 Box 或者 Canvas 简单实现,方法众多,可以尽情发挥。

代码实现

  通过上面的分析,可以来实现轮播图Banner了,现在将实现分为三步。

水平分页器

  首先需要定义好Pager的数量——pagerCount。这里我定义了一个List用来存储图片的URL,之后也是使用了coil库来加载图片。因此,这里的 pagerCount 就等于 images.size。

  接着就是定义 HorizontalPager 的 pagerState,直接使用 rememberPagerState,然后将 pagerCount 传入。

  然后就可以使用HorizontalPager了,这里也设置了一下pageSpacing 来分隔此分页器中页面的空间量。同时写了个 BannerCard 来修饰一下图片,传入图片URL,然后使用 coil 的 AsyncImage 来加载图片。

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Banner(
    images: List<String>,
    autoScrollDuration: Long = 1500L   //延迟的时间,可以自行设置,后面要用到。
) {
    val pagerCount = images.size
    val pagerState = rememberPagerState{
        pagerCount
    }
    Box {
        HorizontalPager(
            state = pagerState,
            contentPadding = PaddingValues(horizontal = 32.dp),
            pageSpacing = 16.dp
        ) {index ->
            BannerCard(
                imageURL = images[index],
                modifier = Modifier
                    .padding(top = 10.dp)
            )
        }
    }
}

@Composable
fun BannerCard(
    imageURL : String,
    modifier : Modifier
){
    Card (modifier = modifier){
        AsyncImage(model = imageURL, contentDescription = null)
    }
}

  实现效果如下

自动循环与延迟

  首先先定义一个布尔类型的 isDragged 来获取是否处于拖动状态,然后对其进行判断,当没有人为拖动时,使用kotlin中的with 函数,在里面设立一个局部变量来储存当前页数的值,然后将其作为 LaunchedEffect (协程是Android kotlin开发中一个重要知识,在Compose中采用附带效应 effect 来实现协程开发)的参数,在协程使用 pagerState 的函数 animateScrollToPage 来实现自动滑动,nextPage 通过当前页数 + 1然后再对总页数取模来实现循环。

val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
if (isDragged.not()){
        with(pagerState){
            var currentPageKey by remember { mutableIntStateOf(0) }
            LaunchedEffect(key1 = currentPageKey){
                launch {
                    delay(timeMillis = autoScrollDuration)
                    val nextPage = (currentPage + 1).mod(pageCount)
                    animateScrollToPage(page = nextPage)
                    currentPageKey = nextPage
                }
            }
        }
    }

  将其加入后再次启动(虚拟机!启动!!!)如下,发现已经可以自己循环播放了。

添加指示器

  添加指示器,本蒟蒻采用的是 Canvas 来实现。首先在一行中使用 repeat 函数进行重复,有多少张图就 repeat 多少次,即repeat(pageCount),然后定义颜色,与当前页数相同的指示点颜色与其他点颜色进行区别,既然要画点,所以使用 Canvas 中的 drawCircle 来进行绘画,可以自行定义大小。

  因为要使用pagerState来获得当前页数,所以指示器需要传入pagerState,和pagerCount。同时需要通过 modifier 修饰符将指示器放在父 Box 容器的底部,这样指示器就可以显示在图片的上方,并处于底部。

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DotIndicators(
    pageCount : Int,
    pagerState : PagerState,
    modifier: Modifier
){
    Row (
        modifier = modifier.padding(bottom = 7.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.Bottom
    ){
        repeat(pageCount){iteration ->
            val color = if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.surface 
                          else MaterialTheme.colorScheme.onSurface
            Canvas(
                modifier = Modifier
                	.size(12.dp)
                	.padding(horizontal = 2.dp), onDraw = {
                drawCircle(color)
            })
        }
    }
}

  最后再次启动!

总结

  如此如此,这般这般,就实现了轮播图的效果,如果任觉得不满于此,也可以跟随自己的想法去将其变得更加 niu pi。同时也可以多多探索。