前言
上篇文章我們講到TopAppBar的封裝,主要是封裝一個標題居中的TopAppBar驻子,包括支持沉浸式狀態(tài)欄娜遵。
今天我們來實現(xiàn)一個Banner的封裝
Banner框架介紹和使用
- 項目源碼地址:Banner.kt源碼
效果
我們首先看下我們今天要做的效果
框架使用
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)
- 我們使用的框架是Google的HorizontalPager
- 官方文檔:https://google.github.io/accompanist/pager/
- 添加依賴
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
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
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í)行一次
是不是很簡單棋恼,我們看下效果
demo4:添加底部指示器
我們已經實現(xiàn)了Banner的自動輪播返弹,那么我們現(xiàn)在就是開始添加指示器
指示器官方有個現(xiàn)成的
def accompanist_version = "0.24.7-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:${accompanist_version}"
- 官方demo:HorizontalPagerWithIndicatorSample.kt,我直接貼了
@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)圖片展示不出來
過一會兒程序還崩潰了并扇,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