jetpack compose實戰(zhàn)——Banner輪播圖的使用和封裝

前言

Banner框架介紹和使用

效果

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

輪播圖效果圖.gif
框架使用
Banner(
    data = viewModel.bannerData,//設置數(shù)據(jù)
    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 數(shù)據(jù)來源
 * @param onImagePath 設置圖片的url
 * @param pagerModifier HorizontalPager的Modifier
 * @param ratio 圖片寬高壓縮比
 * @param contentScale 圖片裁剪方式
 * @param isShowPagerIndicator 是否顯示指示器
 * @param pagerIndicatorModifier 指示器Row的整個樣式
 * @param activeColor 選中的指示器樣式
 * @param inactiveColor 未選中的指示器樣式
 * @param isLoopBanner 是否自動播放輪播圖
 * @param loopDelay 任務執(zhí)行前的延遲(毫秒)
 * @param loopPeriod 連續(xù)任務執(zhí)行之間的時間(毫秒)骆膝。
 * @param horizontalArrangement 指示器Row中文本和指示器的排版樣式
 * @param desc 文本內容
 * @param onBannerItemClick Banner的item點擊事件
 */

上面是我們已經封裝好框架的介紹和使用,那么怎么封裝的呢灶体?讓我?guī)阋徊揭徊綄崿F(xiàn)它

Banner輪播圖的封裝實現(xiàn)

def accompanist_version = "0.24.7-alpha"
api "com.google.accompanist:accompanist-pager:${accompanist_version}"
api "com.google.accompanist:accompanist-pager-indicators:${accompanist_version}"
  • 考慮到對數(shù)據(jù)進行解耦阅签, 我們把耗時的任務和數(shù)據(jù)放到ViewModel,這時候我們需要另一些庫
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))
        }
    }
}
  • 通過viewModel: HomeFragmentViewModel = viewModel()進行數(shù)據(jù)和UI解耦
  • TopAppBarCenter是我們上篇文章封裝的TopAppBar蝎抽,大家可以看上篇文章政钟,這里不再闡述
  • rememberPagerState記住當前的頁面的狀態(tài),方法只有一個參數(shù)樟结,可以傳入初始頁面养交,不傳的話默認是0
  • HorizontalPager必須傳入兩個參數(shù),count代表HorizontalPager的數(shù)量瓢宦,pagerState就是上面的rememberPagerState
  • AsyncImage用到的是coil庫碎连,添加依賴
//圖片加載
api("io.coil-kt:coil-compose:2.0.0-rc01")

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

效果圖.gif
demo2:添加循環(huán)輪播
  • demo1還是非常簡單的,就顯示一張圖
  • HorizontalPager其實就相當于Android中的ViewPager驮履。
  • 現(xiàn)在我們滑倒最后一張的時候鱼辙,實際是不可滑動了,現(xiàn)在想讓它在最后一張的時候玫镐,再向左滑動顯示第一張倒戏,怎么解決?
  • 官方其實有現(xiàn)成的demo:HorizontalPagerLoopingSample.kt
    修改后的代碼
@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
}
  • 我們設置HorizontalPager數(shù)量為整型的最大值
  • 初始化顯示的位置為整型的最大值的一半恐似,這樣數(shù)據(jù)邊界左右就一定有值
  • 大家可能對floorMod方法不是很理解
    • 我們假設Int.MAX_VALUE=200,那么initialIndex=100
    • 圖片的大小是4
    • 我們第一次進來的index實際就是initialIndex杜跷,也就是100
    • floorDiv的作用是第一個參數(shù)/第二個參數(shù),然后向下取整,如125/25=2
    • 如下圖葱椭,假設我們現(xiàn)在是104捂寿,那么它實際下標應該是0,(104-100)-(104-100)/4 * 4,再比如107孵运,下標實際是3=(107-100)-(107-100)/4 * 4
image.png
demo3:輪播圖自動輪播
  • 上面的代碼我們已經實現(xiàn)了圖片的左右輪詢滑動秦陋,現(xiàn)在再添加一個功能,讓它自己動起來

  • 這里我們需要先講下Compose的生命周期
    生命周期

  • 官方網(wǎng)址:https://developer.android.google.cn/jetpack/compose/lifecycle

  • LanuchedEffect

    • 如果需要在 Compasable 內安全調用掛起函數(shù)治笨,可以使用 LaunchedEffect
    • LaunchedEffect 會自動啟動一個協(xié)程驳概,并將代碼塊作為參數(shù)傳遞
    • 當 LaunchedEffect 離開 Composable 或 Composable 銷毀時,協(xié)程也將取消
    • 如果 LaunchedEffect的 key 值改變了旷赖,系統(tǒng)將取消現(xiàn)有協(xié)程顺又,并在新的協(xié)程中啟動新的掛起函數(shù)
  • rememberCoroutineScope

    • LaunchedEffect是Compose函數(shù),只能在其他Compose中使用
    • 如果想在Compose之外使用協(xié)程等孵,并且能夠自動取消稚照,我們可以使用rememberCoroutineScope
    • 如果需要手動控制協(xié)程的生命周期時,也可以使用 rememberCoroutineScope
  • DisposableEffect

    • 對于需要對于某個值改變時或 Composable 退出后進行銷毀或清理操作時俯萌,可以使用DisposableEffect
    • 當DisposableEffect的 key 發(fā)生改變時果录,會調用onDispose方法,可以在方法中作清理操作咐熙,然后再次調用重啟
  • produceState

    • produceState 可讓您將非 Compose 狀態(tài)轉換為 Compose 狀態(tài)

代碼實現(xiàn)

基礎知識講完了弱恒,那我們就來實現(xiàn)它讓它動起來

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)
    }
}
  • 創(chuàng)建了一個coroutineScope用來開啟協(xié)程
  • 用DisposableEffect對Compose退出的時候做清理動作
  • Timer實際就是定時器,可設置每隔多久執(zhí)行一次
    是不是很簡單棋恼,我們看下效果
自動輪播.gif
demo4:添加底部指示器

我們已經實現(xiàn)了Banner的自動輪播返弹,那么我們現(xiàn)在就是開始添加指示器
指示器官方有個現(xiàn)成的

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)
            )
        }
    }
}
  • 我們主要關注HorizontalPagerIndicator,我們發(fā)現(xiàn)代碼非常簡單爪飘,設置了pagerState,而這個pagerState就是HorizontalPager的pagerState义起。
  • 這時候大家是不是很高心,這么簡單悦施,我直接在我們demo3中AsyncImage的下方直接添加
 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
         )

直接運行我們會發(fā)現(xiàn)圖片展示不出來

image.png

過一會兒程序還崩潰了并扇,what?為什么呢抡诞?

  • 我們看HorizontalPagerIndicator源碼
@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,
                )
        )
    }
}
  • 我們看注釋①
    就一個pagerState,而這個實際是HorizontalPagerIndicator的pagerState,還記得之前我們對HorizontalPagerIndicator設置count是多少嗎穷蛹?沒錯,是Int.MAX_VALUE,我們可能明明就只需要4個點昼汗,你給我繪制Int.MAX_VALUE肴熏,不肯定崩潰了嘛。

  • 注釋②就是繪制所有的點

  • 注釋③就是繪制被選中的點

那么問題來了顷窒,現(xiàn)在顯示失敗或者崩潰的原因是我們設置的數(shù)量是最大值蛙吏,現(xiàn)在我們把它寫死data.size源哩,運行起來我們發(fā)現(xiàn)不會報錯,但是選擇的點永遠不顯示鸦做。

既然寫死不行励烦,那我們就不用它唄,自己寫個指示器唄泼诱,多大點事坛掠。當然這個方案也是可以的,但是我用的是另一種方案治筒,修改HorizontalPagerIndicator源碼

手寫HorizontalPagerIndicator
  • 我們仔細看HorizontalPagerIndicator源碼注釋②Row下面有個repeat
repeat(pagerState.pageCount) {
    Box(indicatorModifier) 
}

它既然是因為繪制數(shù)量太多崩潰了屉栓,那我們就將它寫成我們數(shù)據(jù)的大小不就可以了

  • 再看注釋③,大家可以看到scrollPosition
val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset) 
.coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat())

是不是想到了什么耸袜?沒錯pagerState.currentPage就是我們當前選中頁面的index友多,pagerState.pageCount繼續(xù)改成我們數(shù)據(jù)的大小。我相信大家肯定已經非常清楚怎么做了堤框,我直接貼源碼了

@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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荆忍,隨后出現(xiàn)的幾起案子格带,更是在濱河造成了極大的恐慌,老刑警劉巖刹枉,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叽唱,死亡現(xiàn)場離奇詭異,居然都是意外死亡微宝,警方通過查閱死者的電腦和手機棺亭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟋软,“玉大人镶摘,你說我怎么就攤上這事≡朗兀” “怎么了凄敢?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長湿痢。 經常有香客問我涝缝,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任拒逮,我火速辦了婚禮罐氨,結果婚禮上,老公的妹妹穿的比我還像新娘滩援。我一直安慰自己栅隐,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布狠怨。 她就那樣靜靜地躺著约啊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佣赖。 梳的紋絲不亂的頭發(fā)上恰矩,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音憎蛤,去河邊找鬼外傅。 笑死,一個胖子當著我的面吹牛俩檬,可吹牛的內容都是我干的萎胰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼棚辽,長吁一口氣:“原來是場噩夢啊……” “哼技竟!你這毒婦竟也來了?” 一聲冷哼從身側響起屈藐,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤榔组,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后联逻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搓扯,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年包归,在試婚紗的時候發(fā)現(xiàn)自己被綠了锨推。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡公壤,死狀恐怖换可,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情厦幅,我是刑警寧澤锦担,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站慨削,受9級特大地震影響洞渔,放射性物質發(fā)生泄漏套媚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一磁椒、第九天 我趴在偏房一處隱蔽的房頂上張望堤瘤。 院中可真熱鬧,春花似錦浆熔、人聲如沸本辐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慎皱。三九已至,卻和暖如春叶骨,著一層夾襖步出監(jiān)牢的瞬間茫多,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工忽刽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留天揖,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓跪帝,卻偏偏與公主長得像今膊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伞剑,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容