Compose for Android

jetpack compose实战——Banner轮播图的使用和

2022-05-06  本文已影响0人  Peakmain

前言

Banner框架介绍和使用

效果

我们首先看下我们今天要做的效果

轮播图效果图.gif
框架使用
Banner(
    data = viewModel.bannerData,//设置数据
    onImagePath = {//设置图片的url地址
        viewModel.bannerData[it].imagePath
    },
    pagerModifier = Modifier
        .padding(horizontal = 16.dp)
        .padding(top = 10.dp)
        .clip(RoundedCornerShape(8.dp)),//HorizontalPager的modifier
    pagerIndicatorModifier = Modifier
        .background(Color(0x90000000))
        .padding(horizontal = 10.dp)
        .padding(top = 10.dp, bottom = 10.dp),//指示器Row的整个样式
    desc = {
        //指示器文本内容,也就是标题一、标题二
        Text(text = viewModel.bannerData[it].desc, color = Color.White)
    }
) {
    //设置item的点击事件
    Log.e("TAG", viewModel.bannerData[it].imagePath)

Banner框架可设置的属性

/**
 * @param data 数据来源
 * @param onImagePath 设置图片的url
 * @param pagerModifier HorizontalPager的Modifier
 * @param ratio 图片宽高压缩比
 * @param contentScale 图片裁剪方式
 * @param isShowPagerIndicator 是否显示指示器
 * @param pagerIndicatorModifier 指示器Row的整个样式
 * @param activeColor 选中的指示器样式
 * @param inactiveColor 未选中的指示器样式
 * @param isLoopBanner 是否自动播放轮播图
 * @param loopDelay 任务执行前的延迟(毫秒)
 * @param loopPeriod 连续任务执行之间的时间(毫秒)。
 * @param horizontalArrangement 指示器Row中文本和指示器的排版样式
 * @param desc 文本内容
 * @param onBannerItemClick Banner的item点击事件
 */

上面是我们已经封装好框架的介绍和使用,那么怎么封装的呢?让我带你一步一步实现它

Banner轮播图的封装实现

def accompanist_version = "0.24.7-alpha"
api "com.google.accompanist:accompanist-pager:${accompanist_version}"
api "com.google.accompanist:accompanist-pager-indicators:${accompanist_version}"
def lifecycle_version = "2.4.1"

api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
api 'androidx.activity:activity-compose:1.4.0'
demo1:HorizontalPager的基本使用
@Composable
fun HomeFragment(viewModel: HomeFragmentViewModel = viewModel()) {
    TopAppBarCenter(title = {
        Text(text = "首页", color = Color.White)
    },
        isImmersive = true,
        modifier = Modifier.background(Brush.linearGradient(listOf(Color_149EE7, Color_2DCDF5)))) {

        Column(Modifier.fillMaxWidth().padding(it)) {
            val pagerState = rememberPagerState()

            HorizontalPager(
                count = viewModel.bannerData.size,
                state = pagerState
            ) { index ->
                AsyncImage(model = viewModel.bannerData[index].imagePath,
                    contentDescription = null,
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(7 / 3f),
                    contentScale = ContentScale.Crop)
            }

            Text(text = "我是首页", modifier = Modifier.padding(top = 10.dp))
        }
    }
}
//图片加载
api("io.coil-kt:coil-compose:2.0.0-rc01")

GitHub地址:https://github.com/coil-kt/coil

效果图.gif
demo2:添加循环轮播
@Composable
fun Banner(vm: HomeFragmentViewModel) {
    val virtualCount = Int.MAX_VALUE

    val actualCount = vm.bannerData.size
    //初始图片下标
    val initialIndex = virtualCount / 2
    val pageState = rememberPagerState(initialPage = initialIndex)
    HorizontalPager(count = virtualCount,
        state = pageState,
        modifier = Modifier
            .padding(horizontal = 16.dp)
            .clip(
                RoundedCornerShape(8.dp))) { index ->
        val actualIndex = (index - initialIndex).floorMod(actualCount)
        AsyncImage(model = vm.bannerData[actualIndex].imagePath,
            contentDescription = null,
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(7 / 3f),
            contentScale = ContentScale.Crop)
    }
}

 fun Int.floorMod(other: Int): Int = when (other) {
    0 -> this
    else -> this - floorDiv(other = other) * other
}
image.png
demo3:轮播图自动轮播

代码实现

基础知识讲完了,那我们就来实现它让它动起来

fun SwipeContent(vm: HomeFragmentViewModel) {
    val virtualCount = Int.MAX_VALUE

    val actualCount = vm.bannerData.size
    //初始图片下标
    val initialIndex = virtualCount / 2
    val pageState = rememberPagerState(initialPage = initialIndex)
    //改变地方在这里
    val coroutineScope= rememberCoroutineScope()
    DisposableEffect(Unit) {
        val timer = Timer()
        timer.schedule(object :TimerTask(){
            override fun run() {
                 coroutineScope.launch {
                     pageState.animateScrollToPage(pageState.currentPage+1)
                 }
            }

        },3000,3000)
        onDispose {
            timer.cancel()
        }
    }
    HorizontalPager(count = virtualCount,
        state = pageState,
        modifier = Modifier
            .padding(horizontal = 16.dp)
            .clip(
                RoundedCornerShape(8.dp))) { index ->
        val actualIndex = (index - initialIndex).floorMod(actualCount)
        AsyncImage(model = vm.bannerData[actualIndex].imagePath,
            contentDescription = null,
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(7 / 3f),
            contentScale = ContentScale.Crop)
    }
}
自动轮播.gif
demo4:添加底部指示器

我们已经实现了Banner的自动轮播,那么我们现在就是开始添加指示器
指示器官方有个现成的

def accompanist_version = "0.24.7-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:${accompanist_version}"
@Composable
private fun Sample() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.horiz_pager_with_indicator_title)) },
                backgroundColor = MaterialTheme.colors.surface,
            )
        },
        modifier = Modifier.fillMaxSize()
    ) { padding ->
        Column(Modifier.fillMaxSize().padding(padding)) {
            val pagerState = rememberPagerState()

            // Display 10 items
            HorizontalPager(
                count = 10,
                state = pagerState,
                // Add 32.dp horizontal padding to 'center' the pages
                contentPadding = PaddingValues(horizontal = 32.dp),
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth(),
            ) { page ->
                PagerSampleItem(
                    page = page,
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f)
                )
            }

            HorizontalPagerIndicator(
                pagerState = pagerState,
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(16.dp),
            )

            ActionsRow(
                pagerState = pagerState,
                modifier = Modifier.align(Alignment.CenterHorizontally)
            )
        }
    }
}
 AsyncImage(
     model = onImagePath(actualIndex),
     contentDescription = null,
     modifier = Modifier
         .layoutId("image")
         .aspectRatio(ratio)
         .clickable {
             onBannerItemClick?.invoke(actualIndex)
         },
     contentScale = contentScale,
 )
     Row(Modifier
         .layoutId("content")
         .fillMaxWidth()
         .then(pagerIndicatorModifier),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = horizontalArrangement
     ) {
         desc(actualIndex)
         HorizontalPagerIndicator(
             pagerState = pageState
         )

直接运行我们会发现图片展示不出来

image.png

过一会儿程序还崩溃了,what?为什么呢?

@Composable
fun HorizontalPagerIndicator(
    pagerState: PagerState,//①
    modifier: Modifier = Modifier,
    activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
    inactiveColor: Color = activeColor.copy(ContentAlpha.disabled),
    indicatorWidth: Dp = 8.dp,
    indicatorHeight: Dp = indicatorWidth,
    spacing: Dp = indicatorWidth,
    indicatorShape: Shape = CircleShape,
) {

    val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() }
    val spacingPx = LocalDensity.current.run { spacing.roundToPx() }

    Box(
        modifier = modifier,
        contentAlignment = Alignment.CenterStart
    ) {
        //②
        Row(
            horizontalArrangement = Arrangement.spacedBy(spacing),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            val indicatorModifier = Modifier
                .size(width = indicatorWidth, height = indicatorHeight)
                .background(color = inactiveColor, shape = indicatorShape)

            repeat(pagerState.pageCount) {
                Box(indicatorModifier)
            }
        }
    //③
        Box(
            Modifier
                .offset {
                    val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset)
                        .coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat())
                    IntOffset(
                        x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(),
                        y = 0
                    )
                }
                .size(width = indicatorWidth, height = indicatorHeight)
                .background(
                    color = activeColor,
                    shape = indicatorShape,
                )
        )
    }
}

那么问题来了,现在显示失败或者崩溃的原因是我们设置的数量是最大值,现在我们把它写死data.size,运行起来我们发现不会报错,但是选择的点永远不显示。

既然写死不行,那我们就不用它呗,自己写个指示器呗,多大点事。当然这个方案也是可以的,但是我用的是另一种方案,修改HorizontalPagerIndicator源码

手写HorizontalPagerIndicator
repeat(pagerState.pageCount) {
    Box(indicatorModifier) 
}

它既然是因为绘制数量太多崩溃了,那我们就将它写成我们数据的大小不就可以了

val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset) 
.coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat())

是不是想到了什么?没错pagerState.currentPage就是我们当前选中页面的index,pagerState.pageCount继续改成我们数据的大小。我相信大家肯定已经非常清楚怎么做了,我直接贴源码了

@Composable
fun HorizontalPagerIndicator(
    pagerState: PagerState,
    count:Int,
    modifier: Modifier = Modifier,
    activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
    inactiveColor: Color = activeColor.copy(ContentAlpha.disabled),
    indicatorWidth: Dp = 8.dp,
    indicatorHeight: Dp = indicatorWidth,
    spacing: Dp = indicatorWidth,
    indicatorShape: Shape = CircleShape,
) {

    val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() }
    val spacingPx = LocalDensity.current.run { spacing.roundToPx() }

    Box(
        modifier = modifier,
        contentAlignment = Alignment.CenterStart
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(spacing),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            val indicatorModifier = Modifier
                .size(width = indicatorWidth, height = indicatorHeight)
                .background(color = inactiveColor, shape = indicatorShape)

            repeat(count) {
                Box(indicatorModifier)
            }
        }

        Box(
            Modifier
                .offset {
                    val scrollPosition = ((pagerState.currentPage-Int.MAX_VALUE/2).floorMod(count)+ pagerState.currentPageOffset)
                        .coerceIn(0f, (count - 1).coerceAtLeast(0).toFloat())
                    IntOffset(
                        x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(),
                        y = 0
                    )
                }
                .size(width = indicatorWidth, height = indicatorHeight)
                .background(
                    color = activeColor,
                    shape = indicatorShape,
                )
        )
    }
}

总结

至此呢,我们对Banner的封装已经全部完成了,大家这时候肯定说:瞎说,pager指示器怎么放到底部的你没说呀,这个就留给大家去思考了,当然,大家可以参考我上篇文章的用到的方法,也可直接看我的源码Banner.kt

上一篇下一篇

猜你喜欢

热点阅读