前语
一个老练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