如需实现轮播效果,即可以左右或上下翻阅内容,我们可以分别使用 HorizontalPager 和 VerticalPager 可组合函数。这些可组合项的功能与视图系统中的 ViewPager 类似。接下来我们主要以水平轮播图HorizontalPager为例:
一、分页简单实现
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MyPagerExample()
{
//轮播图列表
val imageList = listOf(
R.drawable.sky1,
R.drawable.sky2,
R.drawable.sky3,
R.drawable.sky4
)
//创建一个管理 HorizontalPager 状态的对象:pagerState
//remember 确保状态在重组过程中保持不变
val pagerState = rememberPagerState(
//通过 lambda 表达式设置页面总数
pageCount = { imageList.size })
//pagerState 会跟踪当前页面、滚动位置和滚动状态
//VerticalPager
HorizontalPager(state = pagerState) { index ->
Image(
painter = painterResource(id = imageList[index]),
contentDescription = "图片 ${index + 1}",
modifier = Modifier.fillMaxSize(),
//裁剪
contentScale = ContentScale.Crop)
}
}
运行效果如下:
默认情况下,
占据屏幕的整个宽度,
HorizontalPager
占据整个高度,即参数
VerticalPager
Fill
pageSize默认是
二、控制页面显示位置
2.1、contentPadding
和
HorizontalPager
都支持更改内容边衬区,
VerticalPager
contentPadding用来设置内边距,可以影响页面的最大尺寸和对齐方式
2.2、pageSpacing
2.3、Modifer.padding
效果两边有空告白
三、一次快速滑动,滚动最大页数限制
上述代码涉及到两个陌生的参数,分别是beyondBoundsPageCount与flingBehavior。
beyondBoundsPageCount=M:控制的是 Compose 的布局系统。它说:“请提前组合和布局当前页面前、后共 M 个页面范围内的内容,即使它们还不可见,超出这M 个范围的页面会被销毁。
默认情况下,Compose 只组合和布局当前可见的页面以及紧邻的缓存页面(通常左右各一个),以优化性能。
的
flingBehavior
控制的是滚动动画的逻辑和目标。它说:“一次 Fling 手势,最终停下来的页面距离当前页不能超过 N 页。
PagerSnapDistance.atMost(N),
这两个参数需要配合使用,将
设置为大于或等于
beyondBoundsPageCount
所允许的最大页数跳跃值,即M>=N,目的是需要确保 Compose 为所有可能通过一次 Fling 滚动到的页面都提前准备好布局。
PagerSnapDistance
四、添加页面指示器
//分页指示器
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun pagerIndicator(state: PagerState)
{
//循环创建原点,水平排列
Row(
Modifier.wrapContentHeight().fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
//循环创建指定数量的元素
repeat(state.pageCount) { iteration ->
//确定每个指示点的颜色
val color = if (state.currentPage == iteration) Color.Green else Color.LightGray
//圆形指示点
Box(
modifier = Modifier
.padding(2.dp)// 添加2.dp的外边距
.clip(CircleShape)// 将形状裁剪为圆形
.background(color)// 设置背景颜色
.size(16.dp) // 设置固定尺寸为16x16.dp
)
}
}
}
}
接下来我们调整布局,让指示器底部居中
五、调整图片的宽高
:使组件填充其父容器在水平方向上的全部可用宽度。
Modifier.fillMaxWidth()
设置组件的宽高比为 16:9,高度会根据宽度自动计算,保持指定的比例。
aspectRatio(16/9f):
六、页面滚动时透明度平滑过渡
下面这段代码使用了
修饰符来创建一个基于页面滚动位置的视觉过渡效果。
graphicsLayer
modifier = Modifier.fillMaxWidth().aspectRatio(16/9f)
//滚动的视觉过渡效果
.graphicsLayer
{
//计算当前页面相对于滚动位置的绝对偏移量
//使用绝对值absoluteValue允许我们在两个方向上都对称
val pageOffset = ((pagerState.currentPage - index)
//当前页面偏移的分数(-1到1之间)
+ pagerState.currentPageOffsetFraction).absoluteValue
alpha = lerp(
start = 0.3f,//页面完全偏移时的透明度
stop = 1f,//当页面完全居中时的透明度(完全不透明)
//fraction透明度随着页面接近中心而增加
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
},
修饰符创建一个独立的图形层,可以应用变换而不影响布局。
graphicsLayer
七、轮播图:自动翻页
//创建一个可自动前进的水平分页视图
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun autoAdvance(pagerState: PagerState,pageItemsCount:Int)
{
//将拖动状态收集为一个可观察的 State 对象 pagerIsDragged
//pagerState.interactionSource:分页器状态的交互源,用于监听用户与分页器的交互
val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()
//监听单个页面的按压状态
val pageInteractionSource = remember { MutableInteractionSource() }
//将按压状态收集为一个可观察的 State 对象
val pageIsPressed by pageInteractionSource.collectIsPressedAsState()
// 如果用户与分页器互动,则自动切换会停止
val autoAdvance = !pagerIsDragged && !pageIsPressed
if (autoAdvance) {
// 每隔两秒自动前进一次,除非用户拖动分页器或按其中一个页面
LaunchedEffect(pagerState, pageInteractionSource) {
while (true) {
delay(2000)
val nextPage = (pagerState.currentPage + 1) % pageItemsCount
pagerState.animateScrollToPage(nextPage)
}
}
}
}
八、网络图片加载
加载网络图片,推荐使用 Coil 库(Compose Image Loader),它是 Android 上推荐的图片加载库,与 Compose 完美集成。
8.1、添加依赖
首先,在
文件中添加 Coil 依赖:
build.gradle.kts
implementation("io.coil-kt:coil-compose:2.6.0")
8.2、申请权限
因为是加载网络图片,所以需要申请权限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
8.3、创建网络安全配置文件
默认情况下,面向 Android 9 (API 28) 及更高版本的应用会阻止所有明文流量(非 HTTPS),
因为我们的图片url中需要访问 HTTP(非 HTTPS)网址,需要创建网络安全配置文件。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--允许http明文交互-->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
8.4、代码实现
val imageList = listOf("https://www.gstatic.com/webp/gallery/1.sm.webp" , "https://http.cat/images/200.jpg",
"https://http.dog/301.jpg", "https://http.dog/403.jpg")
8.5、卸载重装app
各种配置都设置好了,结果还是加载失败,后来百度了一下,发现需要卸载重装,MyGod!!!
8.6、完整代码
package com.dcy.mycomposedemo.myActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import coil.compose.AsyncImage
import com.dcy.mycomposedemo.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
class MyPagerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyPagerExample()
}
}
//https://www.gstatic.com/webp/gallery/1.sm.webp
//https://http.cat/images/200.jpg
//https://http.dog/301.jpg
//https://http.dog/403.jpg
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MyPagerExample()
{
//轮播图列表
// val imageList = listOf(R.drawable.sky1, R.drawable.sky2,
// R.drawable.sky3, R.drawable.sky4)
val imageList = listOf("https://www.gstatic.com/webp/gallery/1.sm.webp" , "https://http.cat/images/200.jpg",
"https://http.dog/301.jpg", "https://http.dog/403.jpg")
//创建一个管理 HorizontalPager 状态的对象:pagerState
//remember 确保状态在重组过程中保持不变
//pagerState 会跟踪当前页面、滚动位置和滚动状态
val pagerState = rememberPagerState(
//通过 lambda 表达式设置页面总数
pageCount = { imageList.size })
autoAdvance(pagerState,imageList.size);
//当用户快速滑动并抬手后,内容不会立即停止,
//而是会根据滑动的初速度继续滚动一段距离并逐渐减速停止,这个行为就是 Fling
val fling = PagerDefaults.flingBehavior(
state = pagerState,
//PagerSnapDistance:一个定义如何计算最大吸附距离的类
//无论用户滑得多快多猛,一次 Fling最终停下来的页面距离当前页不能超过 N 页
pagerSnapDistance = PagerSnapDistance.atMost(3)
)
//让Column占据整个可用高度,这样权重1的HorizontalPager会扩展,指示器在底部
Column(Modifier.fillMaxSize() )
{
//beyondBoundsPageCount:提前组合和布局当前页面前、后共 M 个页面范围内的内容
HorizontalPager(state = pagerState,beyondBoundsPageCount=3,
//// 权重1,占据除指示器以外的所有空间
flingBehavior = fling,modifier = Modifier.weight(1f).padding(horizontal = 16.dp))
{ index ->
AsyncImage(
model = imageList[index],
contentDescription = "图片 ${index + 1}",
onError = { // 处理错误
Log.e("AsyncImage", imageList[index]+"加载失败: ${it.result.throwable}")
},
onSuccess = { // 成功回调
Log.d("AsyncImage", "加载成功")
},
modifier = Modifier.fillMaxWidth().aspectRatio(16/9f)
//滚动的视觉过渡效果
.graphicsLayer
{
//计算当前页面相对于滚动位置的绝对偏移量
//使用绝对值absoluteValue允许我们在两个方向上都对称
val pageOffset = ((pagerState.currentPage - index)
//当前页面偏移的分数(-1到1之间)
+ pagerState.currentPageOffsetFraction).absoluteValue
alpha = lerp(
start = 0.3f,//页面完全偏移时的透明度
stop = 1f,//当页面完全居中时的透明度(完全不透明)
//fraction透明度随着页面接近中心而增加
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
},
//裁剪
contentScale = ContentScale.Crop)
}
// 添加间距
Spacer(modifier = Modifier.height(20.dp))
pagerIndicator(pagerState)
}
// //创建的协程作用域与当前组合的生命周期绑定,
// // 当组合退出时,所有在该作用域中启动的协程会自动取消,防止内存泄漏
// val coroutineScope = rememberCoroutineScope()
// Button(onClick = {
// coroutineScope.launch {
// //scrollToPage是一个挂起函数(suspend function),必须在协程中调用
//跳转到指定页面(无动画)
// pagerState.scrollToPage(2)
// //动画滚动到指定页面
// // pagerState.animateScrollToPage(2)
// }
// }) {
// Text("点击按钮跳转到第 3个图片")
// }
}
//创建一个可自动前进的水平分页视图
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun autoAdvance(pagerState: PagerState,pageItemsCount:Int)
{
//将拖动状态收集为一个可观察的 State 对象 pagerIsDragged
//pagerState.interactionSource:分页器状态的交互源,用于监听用户与分页器的交互
val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()
//监听单个页面的按压状态
val pageInteractionSource = remember { MutableInteractionSource() }
//将按压状态收集为一个可观察的 State 对象
val pageIsPressed by pageInteractionSource.collectIsPressedAsState()
// 如果用户与分页器互动,则自动切换会停止
val autoAdvance = !pagerIsDragged && !pageIsPressed
if (autoAdvance) {
// 每隔两秒自动前进一次,除非用户拖动分页器或按其中一个页面
LaunchedEffect(pagerState, pageInteractionSource) {
while (true) {
delay(2000)
val nextPage = (pagerState.currentPage + 1) % pageItemsCount
pagerState.animateScrollToPage(nextPage)
}
}
}
}
//分页指示器
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun pagerIndicator(state: PagerState)
{
//循环创建原点,水平排列
Row(
Modifier.wrapContentHeight().fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
//循环创建指定数量的元素
repeat(state.pageCount) { iteration ->
//确定每个指示点的颜色
val color = if (state.currentPage == iteration) Color.Green else Color.LightGray
//圆形指示点
Box(
modifier = Modifier
.padding(2.dp)// 添加2.dp的外边距
.clip(CircleShape)// 将形状裁剪为圆形
.background(color)// 设置背景颜色
.size(16.dp) // 设置固定尺寸为16x16.dp
)
}
}
}
}
咦,已经到底怎么还有一项,那是因为我后来发现手动滑动不能无限循环,如果想要循环呢?
简单做了下实现,不知道有没有埋坑,实现逻辑如下,图片中都是标注出来相对于上面的类的改动。
九、手动滑动也要无限循环
指示器改动:
自动滚动方法改动