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