Jetpack Compose : 一学就会的自定义下拉刷新&加载更多
Posted 上马定江山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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: 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
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
谢谢~~
源代码地址
作者:miaowmiaow
链接:https://juejin.cn/post/7185159395519496250
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
以上是关于Jetpack Compose : 一学就会的自定义下拉刷新&加载更多的主要内容,如果未能解决你的问题,请参考以下文章
满天星空小游戏—小白一学就会的python游戏开发实战源码+教程
一学就会的Vue slot插槽,真的不看看吗?(使用脚手架)