使用Android Compose实现网格列表滑到底部的提示信息展示

概述

目前大多数的APP都会使用列表的方式来呈现内容,例如淘宝,京东,腾讯体育的评论区等都会使用列表布局。在Android传统的View中主要是使用RecyclerView控件来实现大量数据的展示。而在Compose中使用的是LazyColumn或者是LazyGrid组件。这些组件的使用都很简单,网上有很多的例子,不是本文的重点,本文的重点是介绍实现当我们需要展示的数据展示完了后,即列表滑动到最底部的时候,我们需要展示给用户一个提示信息:比如:"已经到底"。比如百度的评论区翻到最后一条时:

在Compose 中,这个需求其实也不难,网上也有说明。做法就是在布局中多加一个item,用于展示最后的这条提示信息。而本文我会介绍另一种办法,是我个人在写项目的时候琢磨出来的。感觉效果会好点。

1 效果对比

本文的UI图片展示如下:
滑动到底部的时候会显示一条提示信息 :"哥,我已经到底了!!!"

1.1 使用添加Item的办法:

1.2 使用自定义的方法

经过对比上面的两个动图我们可以发现,使用添加Item的方法(也就是网上提供的办法),当我们快要滑动到底部的时候,就会看到文字已经开始展示了,感觉有点生硬,给人的感觉就是提示信息是预埋在底部的。虽然也能完成需求,而且也没啥不妥之处,但我个人就是觉得不太舒服,而第二种方式,可以看到只有我们真正的滑动到这个LazyGrid的底部的时候,提示信息才会展示。因为在上面的界面中们也发现了有个添加图片的悬浮按钮,为了展示这个悬浮按钮,我们是让内容和底部做了一定的内边距的。所以个人感觉当我们把整个LazyGrid滑完再展示信息的话才是符合逻辑的,而不是还没滑动到底部的时候就看到了下面的提示信息

看完效果图,接下来我们就分别介绍下两种实现方式吧,需要的读者按需取用,这里只介绍LazyGrid,LazyColumn的也是一样的,所以不多做赘述。

2. 效果实现

2.1 列表为空时的提示页面实现

在列表展示内容的时候,当列表中没有内容或者网络不可达导致无法获取到内容的时候,往往会展示一个提示的页面,本文也简单的实现了下,读者可参考使用。界面效果如下:

代码如下:

kotlin 复制代码
@Composable
fun ShowEmptyUI(topMargin: Dp) {
    Column(
        modifier = Modifier.fillMaxHeight(),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Spacer(modifier = Modifier.height(topMargin))
        Image(
            painter = painterResource(R.drawable.no_content),
            contentDescription = null,
            modifier = Modifier.size(81.dp),
            contentScale = ContentScale.Crop
        )
        Text(
            text = "没有内容可以看啦",
            style = TextStyle(
                fontSize = TextUnit(16f, TextUnitType.Sp),
                color = Color(0xFFE0E6EC),
            ),
            modifier = Modifier.height(22.dp)
        )
    }
}

2.2 添加Item的方式代码实现

添加Item的方式很简单,就是在LazyGrid的block语句块中的最下面添加如下的代码:

kotlin 复制代码
item(span = {
                GridItemSpan(maxLineSpan)
            }) {
                Text(
                    text = "哥,我已经到底了!!!",
                    style = TextStyle(
                        fontSize = TextUnit(14f, TextUnitType.Sp),
                        color = Color(0xFF92989E)
                    ),
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.Center
                )
            }

即可实现滑动到底部时展示提示信息的需求,但是这中方式好像无法做定制,比如我想控制提示信息动态显示隐藏好像无法做到,发现能做到的读者欢迎评论区讨论哈。 完整代码为:

kotlin 复制代码
// dataList的定义
val dataList = mutableListOf<Int>(
    R.drawable.m,
    R.drawable.m1,
    R.drawable.m2,
    R.drawable.m3,
    R.drawable.m4,
    R.drawable.m5,
    R.drawable.m6,
    R.drawable.m7,
    R.drawable.m8,
    R.drawable.m9,
    R.drawable.m10
)

// 图片资源文件下的图片,读者可以替换为自己的图片。
@Composable
fun ShowGridDemoUIByItem() {
    if (dataList.isEmpty()) {
        ShowEmptyUI(topMargin = 211.dp)
    } else {
        val lazyGridState = rememberLazyGridState()
        LazyVerticalGrid(
            state = lazyGridState,
            columns = GridCells.Fixed(2),
            contentPadding = PaddingValues(
                start = 15.dp,
                top = 10.dp,
                end = 16.dp,
                bottom = 161.dp
            ),
            verticalArrangement = Arrangement.spacedBy(12.dp),
            horizontalArrangement = Arrangement.spacedBy(11.dp),
            modifier = Modifier.background(
                Color(0xff31373d)
            ).fillMaxWidth()
                .fillMaxHeight()
        ) {
            items(dataList, key = { it.hashCode() }) {

                Image(
                    painter = painterResource(it),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier.size(200.dp)
                        .clip(shape = RoundedCornerShape(14.dp))
                )
            }

            item(span = {
                GridItemSpan(maxLineSpan)
            }) {
                Text(
                    text = "哥,我已经到底了!!!",
                    style = TextStyle(
                        fontSize = TextUnit(14f, TextUnitType.Sp),
                        color = Color(0xFF92989E)
                    ),
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

2.3 使用自定义的方式实现

自定义的方式就是在LazyGrid的基础上做扩展,当我们使用LazyGrid组件时,需要我们传入一个val lazyGridState = rememberLazyGridState() 这个lazyGridState中保存了LazyGrid组件的很多状态信息,比如当前的列表中第一个可见item的position,当前是否可以往前滑动,是否可以往后滑动,是否正在滚动以及布局信息等,我们拿到这些信息后就可以做一些自己想要实现的动作了。本功能我们就可以通过lazyGridState拿到当前是否可以继续往前滑动,如果不能,则证明滑动到底部了。API为:

kotlin 复制代码
val canScrollBack = lazyGridState.canScrollBackward

然后通过布局信息,拿到对应的内容后的内边距,网格布局的宽和网格布局的末端偏移量,然后计算出,我们要展示的提示文字的显示位置。API为:

kotlin 复制代码
// 这个值就是我们设置的  bottom = 161.dp 中的161.dp的像素值

/*
LazyVerticalGrid(
            state = lazyGridState,
            columns = GridCells.Fixed(2),
            contentPadding = PaddingValues(
                start = 15.dp,
                top = 10.dp,
                end = 16.dp,
                bottom = 161.dp
            )......
 */
 val afterPending = layoutInfo.value.afterContentPadding
                val gridViewEndOffset =
                 layoutInfo.value.viewportEndOffset
                .toFloat()
                
val gridViewW = 
                layoutInfo.value.viewportSize.width.toFloat()

由于本案例中文字需要居中展示,所以我们还需要测量出文字的宽度。使用Paint的API测量:

kotlin 复制代码
  val paint = TextPaint().apply {
                    textSize = UIUtils.sp2px(context, 14f)
                }
 val bottomText = "哥,我已经到底了!!!"
 val textW = paint.measureText(bottomText)

接着就可以计算提示文字的展示位置了,如下所示:

kotlin 复制代码
// 最后一个Item和提示的距离
                val bottomMargin = UIUtils.dp2px(
                    context,
                    18f
                )
                val xOffset = (gridViewW / 2 - textW / 2)
                val yOffset =
                    gridViewEndOffset - afterPending + bottomMargin

最后使用Modifier的drawBehind API将文字绘制出来就行了。

kotlin 复制代码
 modifier = Modifier
                .background(
                    Color(0xff31373d)
                ).fillMaxWidth()
                .fillMaxHeight()
                .drawBehind {
                ...
                 if (!canScrollForward) {
                    drawText(
                        textMeasurer = textMeasurer,
                        text = bottomText,
                        style = TextStyle(
                            fontSize = TextUnit(14f, TextUnitType.Sp),
                            color = Color(0xFF92989E)
                        ),
                        softWrap = false,
                        topLeft = Offset(x = xOffset, y = yOffset)
                    )
                }
                ...
         }

完整的代码为:

kotlin 复制代码
@OptIn(ExperimentalTextApi::class)
@Composable
fun ShowGridDemoUI() {
    if (dataList.isEmpty()) {
        ShowEmptyUI(topMargin = 211.dp)
    } else {
        val lazyGridState = rememberLazyGridState()
        val firstVisibleItemIndex by remember {
            derivedStateOf { lazyGridState.firstVisibleItemIndex }
        }

        val canScrollForward = lazyGridState.canScrollForward
        val canScrollBack = lazyGridState.canScrollBackward
        val inInScrolling = lazyGridState.isScrollInProgress

        Log.d(
            TAG, "firstVisibleItemIndex = $firstVisibleItemIndex, " +
                    "canScrollForward: $canScrollForward" +
                    " ,canScrollBack: $canScrollBack ,inInScrolling: 
                    $inInScrolling" +
                    ",mediaFileList: size : ${dataList.size}"
        )
        val layoutInfo = remember {
         derivedStateOf { lazyGridState.layoutInfo }
          }
        val context = LocalContext.current
        val textMeasurer = rememberTextMeasurer(100)
        
        LazyVerticalGrid(
            state = lazyGridState,
            columns = GridCells.Fixed(2),
            contentPadding = PaddingValues(
                start = 15.dp,
                top = 10.dp,
                end = 16.dp,
                bottom = 161.dp
            ),
            verticalArrangement = Arrangement.spacedBy(12.dp),
            horizontalArrangement = Arrangement.spacedBy(11.dp),
            modifier = Modifier
                .background(
                    Color(0xff31373d)
                ).fillMaxWidth()
                .fillMaxHeight()
                .drawBehind {
                    val paint = TextPaint().apply {
                        textSize = UIUtils.sp2px(context, 14f)
                    }

                    val afterPending = 
                    layoutInfo.value.afterContentPadding
                    val gridViewEndOffset = 
                    layoutInfo.value.viewportEndOffset.toFloat()
                    val gridViewW = 
                    layoutInfo.value.viewportSize.width.toFloat()
                    val bottomText = "哥,我已经到底了!!!"
                    val textW = paint.measureText(bottomText)
                    // 最后一个Item和提示的距离
                    val bottomMargin = UIUtils.dp2px(
                        context,
                        18f
                    )
                    val xOffset = (gridViewW / 2 - textW / 2)
                    val yOffset =
                        gridViewEndOffset - afterPending + bottomMargin

                    Log.d(
                        TAG, "xcy: canScrollForward: $canScrollForward"
                         +  " ,xOffset:$xOffset ,yOffset: $yOffset, " +
                                " ,bottomMargin: $bottomMargin" +
                                " ,afterPending: $afterPending" +
                                " ,gridViewW:$gridViewW" +
                                " ,textW: $textW")
                    if (!canScrollForward) {
                        drawText(
                            textMeasurer = textMeasurer,
                            text = bottomText,
                            style = TextStyle(
                                fontSize = TextUnit(
                                14f, 
                                TextUnitType.Sp
                                ),
                                color = Color(0xFF92989E)
                            ),
                            softWrap = false,
                            topLeft = Offset(x = xOffset, y = yOffset)
                        )
                    }
                }
        ) {
            items(dataList, key = { it.hashCode() }) {
                Image(
                    painter = painterResource(it),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier.size(200.dp)
                        .clip(shape = RoundedCornerShape(14.dp))
                )
            }
        }
    }
}

3. UI工具类

kotlin 复制代码
object UIUtils {
    fun dp2px(context: Context, dpValue: Float): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            dpValue,
            context.resources.displayMetrics
        ).toInt()
            .toFloat()
    }

    fun px2dp(context: Context, pxValue: Float): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_PX,
            pxValue,
            context.resources.displayMetrics
        ).toInt()
            .toFloat()
    }

    @SuppressLint("InternalInsetResource", "DiscouragedApi")
    fun getStatusBarHeight(context: Context): Int {
        val activity = context as Activity
        val resId = activity.resources.getIdentifier(
            "status_bar_height", "dimen", "android"
        )

        if (resId > 0) {
            return activity.resources.getDimensionPixelSize(resId)
        }

        return 0
    }

    @SuppressLint("InternalInsetResource", "DiscouragedApi")
    fun getNavigationBarHeight(context: Context): Int {
        val activity = context as Activity
        val resId = activity.resources.getIdentifier(
            "navigation_bar_height", "dimen", "android"
        )

        if (resId > 0) {
            return activity.resources.getDimensionPixelSize(resId)
        }

        return 0
    }

    fun sp2px(context: Context, spValue: Float): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            spValue,
            context.resources.displayMetrics
        ).toInt()
            .toFloat()
    }

    fun px2sp(context: Context, spValue: Float): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_PX,
            spValue,
            context.resources.displayMetrics
        ).toInt()
            .toFloat()
    }
}
相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript