美文网首页
Jetpack Compose : 一学就会的自定义下拉刷新&加

Jetpack Compose : 一学就会的自定义下拉刷新&加

作者: 进击的老六 | 来源:发表于2023-11-06 16:27 被阅读0次

    前言

    一个成熟Androider的标志是自定义下拉刷新&加载更多😁

    自定义下拉刷新你会怎么做?

    因为我这个人比较懒(其实就是菜),所以直接拿Compose自带的下拉刷新来修改。
    这里先上效果图,第一张是Compose自带的下拉刷新,第二张是我们想要的下拉刷新。

    通过对比我们很轻松找到需要改造的点:

    1. 列表跟随手指滑动
    2. 指示器样式修改

    接下来我们看Compose自带的下拉刷新是如何使用的:

        //refreshing:下拉刷新状态
        //onRefresh:下拉刷新回调方法
        val state = rememberPullRefreshState(refreshing, onRefresh)
        //设置下拉刷新
        Box(Modifier.pullRefresh(state)) {
            //列表
            LazyColumn() {
                //...省略部分代码...
            }
            //下拉刷新指示器
            PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
        }
    

    想要让列表跟随手指滑动,咱们很容易就能联想到指示器。
    所以先读下指示器的源码,看它的滑动是怎么实现的:

    @Composable
    @ExperimentalMaterialApi
    fun PullRefreshIndicator(
        refreshing: Boolean,
        state: PullRefreshState,
        modifier: Modifier = Modifier,
        backgroundColor: Color = MaterialTheme.colors.surface,
        contentColor: Color = contentColorFor(backgroundColor),
        scale: Boolean = false
    ) {
        Surface(
            modifier = modifier
                .size(IndicatorSize)
                //下拉刷新相关代码
                .pullRefreshIndicatorTransform(state, scale),
            shape = SpinnerShape,
            color = backgroundColor,
            elevation = if (showElevation) Elevation else 0.dp,
        ) {
            省略部分代码...
        }
    }
    

    很容易找到 pullRefreshIndicatorTransform(state, scale),继续点进去看源码:

    @ExperimentalMaterialApi
    fun Modifier.pullRefreshIndicatorTransform(
        state: PullRefreshState,
        scale: Boolean = false,
    ) = composed(inspectorInfo = debugInspectorInfo {
        name = "pullRefreshIndicatorTransform"
        properties["state"] = state
        properties["scale"] = scale
    }) {
        var height by remember { mutableStateOf(0) }
    
        Modifier
            .onSizeChanged { height = it.height }
            .graphicsLayer {
                //原来滑动处理这么的简单
                //注意:state.position是internal无法直接使用,如何处理后面再讲
                translationY = state.position - height
                //...省略部分代码...
            }
    }
    

    接下来我们思考指示器样式问题。
    指示器说白就是一个动画,这里用最简单的帧动画来实现:

    //动画资源id
    val loadingResId = listOf(
        R.drawable.loading_big_1,
        R.drawable.loading_big_4,
        R.drawable.loading_big_7,
        R.drawable.loading_big_10,
        R.drawable.loading_big_13,
        R.drawable.loading_big_16,
        R.drawable.loading_big_19,
    )
    
    //取模获得图片id
    val id = state.position % loadingResId.size
    
    //通过Image展示
    Image(
        painter = painterResource(loadingResId[id.toInt()]),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .size(40.dp, 16.dp)
            .align(Alignment.TopCenter)
            //刚才找到的下拉刷新核心代码
            .graphicsLayer {
                // 你不会再思考为什么 * 0.5f吧,别急看到后面就清楚啦
                translationY = state.position * 0.5f
            }
    )
    

    完整的自定义下拉刷新&加载更多代码:

    @OptIn(ExperimentalMaterialApi::class)
    @Composable
    fun <T> PullRefreshLayout(
        modifier: Modifier = Modifier,
        contentPadding: PaddingValues = PaddingValues(0.dp),
        verticalArrangement: Arrangement.Vertical = Arrangement.Top,
        refreshing: Boolean,
        onRefresh: () -> Unit,
        loading: Boolean,
        onLoad: () -> Unit,
        items: List<T>,
        itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
    ) {
    
        val loadingResId = listOf(
            R.drawable.loading_big_1,
            R.drawable.loading_big_4,
            R.drawable.loading_big_7,
            R.drawable.loading_big_10,
            R.drawable.loading_big_13,
            R.drawable.loading_big_16,
            R.drawable.loading_big_19,
        )
        //指示器图片高度
        val loadingHeightPx: Float
        with(LocalDensity.current) {
            loadingHeightPx = 16.dp.toPx()
        }
        //指示器循环动画
        val loadingAnimate by rememberInfiniteTransition().animateFloat(
            initialValue = 0f,
            targetValue = loadingResId.size.toFloat(),
            animationSpec = infiniteRepeatable(
                animation = tween(250, easing = LinearEasing),
                repeatMode = RepeatMode.Reverse
            )
        )
    
        //前面说过PullRefreshState.position是internal无法直接使用,
        //所以我们就把rememberPullRefresh的代码copy过来小改下
        val state = rememberPullRefreshLayoutState(refreshing, onRefresh)
    
        Box(Modifier.pullRefreshLayout(state)) {
            LazyColumn(
                //让列表跟随手指滑动
                modifier = modifier.graphicsLayer {
                    translationY = state.position
                },
                contentPadding = contentPadding,
                verticalArrangement = verticalArrangement,
            ) {
                itemsIndexed(items) { index, item ->
                    itemContent(index, item)
                    //自动加载更多,这里的触发值是5
                    if (loading && items.size - index < 5) {
                        LaunchedEffect(items.size) {
                            onLoad()
                        }
                    }
                }
                if (items.isNotEmpty()) {
                    item {
                        //加载更多的样式,这里用文本简单显示下
                        Box(modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp)
                            .clickable {
                                onLoad()
                            }
                        ) {
                            Text(
                                text = "👆👆👇👇👈👉👈👉🅱🅰🅱🅰",
                                fontSize = 12.sp,
                                color = Color.Gray,
                                modifier = Modifier.align(alignment = Alignment.Center)
                            )
                        }
                    }
                }
            }
            // Custom progress indicator
            val id = if (refreshing) loadingAnimate else state.position % loadingResId.size
            if (refreshing || (state.position >= loadingHeightPx * 0.5f)) {
                Image(
                    painter = painterResource(loadingResId[id.toInt()]),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .size(40.dp, 16.dp)
                        .align(Alignment.TopCenter)
                        //让指示器跟随手指滑动
                        .graphicsLayer {
                            translationY = state.position * 0.5f
                        }
                )
            }
        }
    }
    
    //不用看就改个名字而已
    @Composable
    @ExperimentalMaterialApi
    fun rememberPullRefreshLayoutState(
        refreshing: Boolean,
        onRefresh: () -> Unit,
        refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
        refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
    ): PullRefreshLayoutState {
        require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
    
        val scope = rememberCoroutineScope()
        val onRefreshState = rememberUpdatedState(onRefresh)
        val thresholdPx: Float
        val refreshingOffsetPx: Float
    
        with(LocalDensity.current) {
            thresholdPx = refreshThreshold.toPx()
            refreshingOffsetPx = refreshingOffset.toPx()
        }
    
        val state = remember(scope) {
            PullRefreshLayoutState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
        }
    
        SideEffect {
            state.setRefreshing(refreshing)
        }
    
        return state
    }
    
    //不用看就是改个名字并把position的internal去掉
    @ExperimentalMaterialApi
    fun Modifier.pullRefreshLayout(
        state: PullRefreshLayoutState,
        enabled: Boolean = true
    ) = inspectable(inspectorInfo = debugInspectorInfo {
        name = "pullRefresh"
        properties["state"] = state
        properties["enabled"] = enabled
    }) {
        Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled)
    }
    
    @ExperimentalMaterialApi
    class PullRefreshLayoutState internal constructor(
        private val animationScope: CoroutineScope,
        private val onRefreshState: State<() -> Unit>,
        private val refreshingOffset: Float,
        internal val threshold: Float
    ) {
    
        val progress get() = adjustedDistancePulled / threshold
    
        internal val refreshing get() = _refreshing
        //唯一的变化去掉internal
        val position get() = _position
    
        private val adjustedDistancePulled by derivedStateOf { distancePulled * 0.5f }
    
        private var _refreshing by mutableStateOf(false)
        private var _position by mutableStateOf(0f)
        private var distancePulled by mutableStateOf(0f)
    
        internal fun onPull(pullDelta: Float): Float {
            if (this._refreshing) return 0f
    
            val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
            val dragConsumed = newOffset - distancePulled
            distancePulled = newOffset
            _position = calculateIndicatorPosition()
            return dragConsumed
        }
    
        internal fun onRelease() {
            if (!this._refreshing) {
                if (adjustedDistancePulled > threshold) {
                    onRefreshState.value()
                } else {
                    animateIndicatorTo(0f)
                }
            }
            distancePulled = 0f
        }
    
        internal fun setRefreshing(refreshing: Boolean) {
            if (this._refreshing != refreshing) {
                this._refreshing = refreshing
                this.distancePulled = 0f
                animateIndicatorTo(if (refreshing) refreshingOffset else 0f)
            }
        }
    
        private fun animateIndicatorTo(offset: Float) = animationScope.launch {
            animate(initialValue = _position, targetValue = offset) { value, _ ->
                _position = value
            }
        }
    
        private fun calculateIndicatorPosition(): Float = when {
            adjustedDistancePulled <= threshold -> adjustedDistancePulled
            else -> {
                val overshootPercent = abs(progress) - 1.0f
                val linearTension = overshootPercent.coerceIn(0f, 2f)
                val tensionPercent = linearTension - linearTension.pow(2) / 4
                val extraOffset = threshold * tensionPercent
                threshold + extraOffset
            }
        }
    }
    

    最后

    写之前有好多东西想要表达,真正写的时候又变成贴代码,写作能力还有待提高呀😅
    这篇文章更多是讲开发思路,下拉刷新源码解析没有多少,点赞多的话到时候整一篇源码解析哈。

    Thanks

    以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
    如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
    谢谢~~

    最后

    如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

    如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

    相关文章

      网友评论

          本文标题:Jetpack Compose : 一学就会的自定义下拉刷新&加

          本文链接:https://www.haomeiwen.com/subject/rnqxwdtx.html