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 = 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状态显示对应的动画和进度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值