Compose笔记(四十八)--PullRefresh
这一节主要了解一下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 = truecurScope.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 = truestatusText = "正在加载最新资讯..."curScope.launch(Dispatchers.IO) {delay(2500) // 模拟2.5秒加载时间val newItems = List(15) {"最新资讯 ${it + 1} (${System.currentTimeMillis() % 10000})"}newsItems = newItemsstatusText = "刷新完成,共 ${newItems.size} 条资讯"isRefreshing = falsedelay(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.currentval 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 传递上拉距离给 onPullelse -> Offset.Zero}override fun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource,): Offset =when {!enabled -> Offset.Zerosource == NestedScrollSource.UserInput && available.y > 0 ->Offset(0f, onPull(available.y)) // Pulling down 传递下拉距离给 onPullelse -> 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状态显示对应的动画和进度