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

发布时间:2026/11/3 0:18:46

Jetpack Compose : 一学就会的自定义下拉刷新加载更多 前言一个成熟Androider的标志是自定义下拉刷新加载更多自定义下拉刷新你会怎么做因为我这个人比较懒其实就是菜所以直接拿Compose自带的下拉刷新来修改。这里先上效果图第一张是Compose自带的下拉刷新第二张是我们想要的下拉刷新。通过对比我们很轻松找到需要改造的点列表跟随手指滑动指示器样式修改接下来我们看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: ListT, 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以上就是本篇文章的全部内容如有问题欢迎指出我们一起进步。如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧您的鼓励是我前进的动力。谢谢~~源代码地址MyCollectFragment.kt · miaowmiaow/fragmject作者miaowmiaow链接https://juejin.cn/post/7185159395519496250最后如果想要成为架构师或想突破20~30K薪资范畴那就不要局限在编码业务要会选型、扩展提升编程思维。此外良好的职业规划也很重要学习的习惯很重要但是最重要的还是要能持之以恒任何不能坚持落实的计划都是空谈。如果你没有方向这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》帮大家将杂乱、零散、碎片化的知识进行体系化的整理让大家系统而高效地掌握Android开发的各个知识点。相对于我们平时看的碎片化内容这份笔记的知识点更系统化更容易理解和记忆是严格按照知识体系编排的。全套视频资料一、面试合集二、源码解析合集三、开源框架合集欢迎大家一键三连支持若需要文中资料直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
延伸阅读

更多相关文章

2026/11/3 9:20:30

腾讯、阿里团队共同出品,Android音视频开发进阶指南

随着5G时代的到来,短视频逐步占据了人们的日常生活。现在,也有越来越多的公司高薪招聘音视频开发的人才,一个稍有经验的音视频人才现在可能会有3-4家公司抢着要。因此,对音视频人才的需求也从小众变成了大众,这更多的是…

2026/10/25 21:07:06

浅谈Android音视频开发那点事儿,音视频究竟如何入门?

在如今快节奏,网络发达的社会。人们对于静态的图片和文字往往很难有精力去安心完整的阅读完网上内容。为了满足人们可以更生动的阅读网上的内容,也为了使网络交互更加生动。我们便需要应用到音频和视频来增加人们在网络上动作交互的丰富性,音…

2026/11/3 9:15:36

【Docker深入浅出】【四】单体应用容器化与Dockerfile怎么写

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 9:15:36

解决LTX-Best-Face-ID常见问题:从安装错误到人脸模糊的完整解决方案

解决LTX-Best-Face-ID常见问题:从安装错误到人脸模糊的完整解决方案 LTX-Best-Face-ID是一款基于LTX-2.3模型的身份保留参考视频生成LoRA插件,它能通过一张参考照片和文本提示生成保持人物身份的视频。本文将针对使用过程中可能遇到的安装错误、人脸模糊…

2026/11/3 9:15:36

AndroidGen-GLM-4-9B自定义扩展指南:打造专属Android智能助手

AndroidGen-GLM-4-9B自定义扩展指南:打造专属Android智能助手 AndroidGen-GLM-4-9B是一款由智谱AI开发的强大Android智能生成模型,通过自定义扩展,开发者可以打造完全符合自身需求的Android智能助手。本文将详细介绍如何通过配置文件修改、令…

2026/11/3 9:15:36

te_PP-OCRv5_mobile_rec_onnx配置详解:从入门到精通

te_PP-OCRv5_mobile_rec_onnx配置详解:从入门到精通 te_PP-OCRv5_mobile_rec_onnx是基于飞桨PaddlePaddle开发的移动端优化OCR模型,为移动设备提供高效的文本识别能力。本文将详细介绍该模型的配置方法,帮助新手用户快速掌握从安装到高级配置…

2026/11/3 9:15:36

Folia 安装完整指南:四条路线快速装好全屏歌词动画播放器

Folia 安装完整指南:四条路线快速装好全屏歌词动画播放器 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major Folia 是一款把歌词渲染成…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32实战:基于MAX14431数字隔离器的SPI隔离通信方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

工业SPI隔离通信设计:MAX14431BAEE+与STM32L4A6RG实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32+NRF24L01无线抢答器:硬件设计、通信协议与低功耗实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑