android compose PullToRefreshBox 下拉刷新 使用

android compose PullToRefreshBox 下拉刷新 使用

使用 PullToRefreshBox 可组合项来实现下拉刷新,该可组合项充当可滚动内容的容器。以下关键参数可控制刷新行为和外观:

  • isRefreshing:一个布尔值,用于指示刷新操作是否正在进行。
  • onRefresh:当用户发起刷新时执行的 lambda 函数。
  • indicator:自定义系统在下拉刷新时绘制的指示器。

代码要点

  • 上一个代码段使用了库提供的 Indicator。此代码段会创建一个名为 MyCustomIndicator 的自定义指示器可组合项。在此可组合项中,pullToRefreshIndicator 修饰符负责处理定位和触发刷新。
  • 与上一个代码段一样,此示例提取了 PullToRefreshState 实例,因此您可以将同一实例传递给 PullToRefreshBoxpullToRefreshModifier
  • 此示例使用了 PullToRefreshDefaults 类中的容器颜色和位置阈值。这样一来,您就可以重复使用 Material 库中的默认行为和样式,同时仅自定义您感兴趣的元素。
  • MyCustomIndicator 使用 Crossfade 在云图标和 CircularProgressIndicator 之间进行过渡。当用户下拉时,云图标会放大,并在刷新操作开始时转换为 CircularProgressIndicator
    • targetState 使用 isRefreshing 来确定要显示的状态(云图标或圆形进度指示器)。
    • animationSpec 定义了过渡的 tween 动画,并指定了时长 CROSSFADE_DURATION_MILLIS
    • state.distanceFraction 表示用户下拉的距离,范围从 0f(未下拉)到 1f(完全下拉)。
    • graphicsLayer 修饰符可修改缩放比例和透明度。
复制代码
package com.wn.androidcomposedemo1.basegoogle

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
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.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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.wn.androidcomposedemo1.ui.theme.AndroidComposeDemo1Theme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * Author : wn
 * Email : maoning20080808@163.com
 * Date : 2026/6/27 16:15
 * Description : 下拉刷新
 */
class PullToRefreshActivity : ComponentActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AndroidComposeDemo1Theme(
            ) {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    PullToRefreshDemo()
                }
            }
        }
    }

    @Composable
    fun PullToRefreshDemo(){
        Column() {
            Spacer(Modifier.height(20.dp))
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center
            ) {
                Text("下拉刷新例子", color = Color.Red, fontSize = 30.sp)
            }

            //刷新状态
            var isRefreshing by remember { mutableStateOf(false) }
            val items = remember { (1..100).map { "item $it" } }
            PullToRefreshCustomIndicatorSample(items, isRefreshing, {
                isRefreshing = true
                CoroutineScope(Dispatchers.IO).launch {
                    //5秒后消失
                    delay(5000)
                    withContext(Dispatchers.Main){
                        isRefreshing = false
                    }
                }
            }, Modifier.fillMaxSize())
        }
    }


    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun PullToRefreshCustomIndicatorSample(
        items : List<String>,
        isRefreshing: Boolean,
        onRefresh:() -> Unit,
        modifier: Modifier
    ){
        val state = rememberPullToRefreshState()

        PullToRefreshBox(
            isRefreshing = isRefreshing,
            onRefresh = onRefresh,
            modifier = modifier,
            state = state,
            indicator = {
                MyCustomIndicator(
                    state = state,
                    isRefreshing = isRefreshing,
                    modifier = Modifier.align(Alignment.TopCenter).padding(top = 20.dp)
                )
            }
        ) {
            LazyColumn(Modifier.fillMaxSize()) {
                items(items){
                    ListItem({
                        Text(text = it)
                    })
                }
            }
        }
    }


    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun MyCustomIndicator(
        state : PullToRefreshState,
        isRefreshing : Boolean,
        modifier: Modifier
    ){
        Box(
            modifier = modifier,
            contentAlignment = Alignment.Center
        ) {
            Crossfade(
                targetState = isRefreshing,
                animationSpec = tween(durationMillis = 300),
                modifier = Modifier.align(Alignment.Center)
            ) { refreshing ->
                if(refreshing){
                    CircularProgressIndicator(Modifier.size(50.dp))
                } else {
                    val distanceFraction = {state.distanceFraction.coerceIn(0f, 1f)}
                    Icon(
                        imageVector = Icons.Filled.Refresh,
                        contentDescription = "Refresh",
                        modifier = Modifier
                            .size(18.dp)
                            .graphicsLayer{
                                val progress = distanceFraction()
                                this.alpha = progress
                                this.scaleX = progress
                                this.scaleY = progress
                            }
                    )
                }
            }
        }
    }
}