这一节主要了解一下Compose中的PullRefresh ,在Jetpack Compose开发中,PullRefresh是用于实现下拉刷新功能的组件,它属于androidx.compose.material.pullrefresh包,通过检测用户的下拉手势触发刷新操作。
API
state:PullRefreshState:管理下拉刷新状态的对象,包含刷新状态、进度等信息
onRefresh:()->Unit:触发刷新时的回调函数,通常在这里执行数据加载逻辑
modifier:Modifier:修饰符
enabled:Boolean:是否启用下拉刷新功能
content:@Composable()-> Unit:需要添加下拉刷新功能的内容
使用场景
1 数据列表刷新,社交媒体的动态列表,用户下拉获取最新内容;新闻应用的新闻流,确保数据实时性。
2 自定义交互体验,需要自定义刷新动画,动态控制刷新手势。
栗子:
gradle添加依赖:
implementation("androidx.compose.material:material:1.5.4")
implementation("androidx.compose.material:material-icons-extended:1.5.4")
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullRefreshExample() {
var items by remember { mutableStateOf(List(20) { "初始项目 ${it + 1}" }) }
var refreshing by remember { mutableStateOf(false) }
val curScope = rememberCoroutineScope()
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
refreshing = true
curScope.launch(Dispatchers.IO) {
delay(2000)
items = List(20) { "刷新后的项目 ${it + 1} (${System.currentTimeMillis() % 1000})" }
refreshing = false
}
}
)
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
Text(
text = item,
modifier = Modifier.padding(20.dp)
)
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullRefreshDemo() {
var newsItems by remember { mutableStateOf<List<String>>(emptyList()) }
var isRefreshing by remember { mutableStateOf(false) }
var statusText by remember { mutableStateOf("下拉刷新获取最新资讯") }
val curScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
loadInitialData {
newsItems = it
}
}
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = {
isRefreshing = true
statusText = "正在加载最新资讯..."
curScope.launch(Dispatchers.IO) {
delay(2500) // 模拟2.5秒加载时间
val newItems = List(15) {
"最新资讯 ${it + 1} (${System.currentTimeMillis() % 10000})"
}
newsItems = newItems
statusText = "刷新完成,共 ${newItems.size} 条资讯"
isRefreshing = false
delay(3000)
statusText = "下拉刷新获取最新资讯"
}
}
)
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
if (newsItems.isEmpty() && !isRefreshing) {
item {
Text(
text = "加载中...",
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
textAlign = TextAlign.Center
)
}
} else {
items(newsItems) { news ->
Text(
text = news,
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(16.dp)
)
}
}
}
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
Text(
text = statusText,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 50.dp),
color = MaterialTheme.colors.primary
)
}
}
private suspend fun loadInitialData(callback: (List<String>) -> Unit) {
delay(1500)
val initialData = List(15) { "初始资讯 ${it + 1}" }
callback(initialData)
}
注意
1 刷新操作应在协程中执行,避免阻塞UI线程
2 确保内容是可滚动的
3 可以通过PullRefreshIndicator的参数自定义指示器样式
源码:
1.PullRefreshState
这是管理下拉刷新状态的核心类,关键属性和方法:
class PullRefreshState(
// 当前刷新状态
val refreshing: Boolean,
// 下拉进度
val progress: Float,
// 内部用于更新状态的回调
private val onRefresh: () -> Unit,
// 刷新阈值
private val refreshThreshold: Float,
// 最大下拉距离
internal val threshold get() = _threshold
) {
// 处理拖动逻辑的内部方法
internal fun onPull(pullDelta: Float): Float { ... }
// 处理拖动结束
internal fun onRelease(): Boolean { ... }
}
分析:
progress:实时反映下拉距离与阈值的比例
onPull:根据手势拖动距离更新progress,返回实际消耗的拖动距离
onRelease:松手时判断是否触发刷新
2.rememberPullRefreshState
用于创建并记忆 PullRefreshState 的函数,关联刷新状态和回调:
@Composable
fun rememberPullRefreshState(
refreshing: Boolean,
onRefresh: () -> Unit,
refreshThreshold: Dp = 80.dp,
maxDrag: Dp = 120.dp
): PullRefreshState {
val density = LocalDensity.current
val thresholdPx = with(density) { refreshThreshold.toPx() }
val maxDragPx = with(density) { maxDrag.toPx() }
return remember(refreshing) {
PullRefreshState(
refreshing = refreshing,
onRefresh = onRefresh,
refreshThreshold = thresholdPx,
maxDrag = maxDragPx
)
}
}
3 pullRefresh修饰符是实现下拉检测的核心,其内部通过pointerInput处理触摸事件:
ExperimentalMaterialApi
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,// 处理下拉距离的回调
onRelease: suspend (flingVelocity: Float) -> Float,// 处理松手速度的回调
enabled: Boolean = true,// 是否启用下拉刷新
) = nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
private class PullRefreshNestedScrollConnection(
private val onPull: (pullDelta: Float) -> Float,
private val onRelease: suspend (flingVelocity: Float) -> Float,
private val enabled: Boolean,
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
when {
!enabled -> Offset.Zero // 禁用时不处理
source == NestedScrollSource.UserInput && available.y < 0 ->
Offset(0f, onPull(available.y)) // Swiping up 传递上拉距离给 onPull
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset =
when {
!enabled -> Offset.Zero
source == NestedScrollSource.UserInput && available.y > 0 ->
Offset(0f, onPull(available.y)) // Pulling down 传递下拉距离给 onPull
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity(0f, onRelease(available.y)) // 传递垂直速度给 onRelease
}
}
分析:3.1事件过滤:仅处理用户输入(NestedScrollSource.UserInput)的滚动事件,忽略其他来源(如程序触发的滚动)。
3.2方向区分:
3.2.1上滑(available.y < 0)通过onPreScroll处理,通常用于取消刷新状态。
3.2.2下拉(available.y > 0)通过onPostScroll处理,是触发下拉刷新的核心路径。
3.3数据传递:将滚动距离(pullDelta)和速度(flingVelocity)传递给外部回调,由外部处理状态更新(如下拉进度、是否触发刷新)。
3.4禁用控制:enabled参数为false时,所有方法返回Offset.Zero或空实现,不处理任何事件。
4 刷新指示器(PullRefreshIndicator)
指示器的UI和动画由PullRefreshIndicator实现,核心逻辑是根据PullRefreshState的progress和refreshing状态更新UI:
@Composable
fun PullRefreshIndicator(
refreshing: Boolean,
state: PullRefreshState,
modifier: Modifier = Modifier,
// 其他样式参数...
) {
// 1. 计算指示器位置和大小动画
val progress = state.progress.coerceIn(0f, 1f)
val indicatorSize by animateDpAsState(
targetValue = if (refreshing) 40.dp else 36.dp * (1f + progress),
animationSpec = spring(stiffness = Spring.StiffnessMedium)
)
// 2. 旋转动画(刷新时)
val rotation by animateFloatAsState(
targetValue = if (refreshing) 360f * 2 else 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing)
),
label = "Rotation"
)
// 3. 绘制指示器
Box(
modifier = modifier
.size(indicatorSize)
// 其他样式设置...
) {
CircularProgressIndicator(
progress = if (refreshing) 1f else progress,
rotationAngle = rotation,
// 样式参数...
)
}
}
分析:
下拉过程中:根据progress缩放指示器大小,同步进度条
刷新中:显示无限旋转动画,固定指示器大小
简单总结:
初始化:通过rememberPullRefreshState创建状态对象,关联refreshing状态和onRefresh回调
手势监听:pullRefresh修饰符检测到下拉手势,判断内容是否在顶部
状态更新:下拉时实时更新progress,松手时若达到阈值则触发onRefresh
UI反馈:PullRefreshIndicator根据progress和refreshing状态显示对应的动画和进度