前言
一个成熟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
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
谢谢~~
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
网友评论