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

文章目录

  • 概述
  • [1 效果对比](#1 效果对比)
    • [1.1 使用添加Item的办法:](#1.1 使用添加Item的办法:)
    • [1.2 使用自定义的方法](#1.2 使用自定义的方法)
  • [2. 效果实现](#2. 效果实现)
    • [2.1 列表为空时的提示页面实现](#2.1 列表为空时的提示页面实现)
    • [2.2 添加Item的方式代码实现](#2.2 添加Item的方式代码实现)
    • [2.3 使用自定义的方式实现](#2.3 使用自定义的方式实现)
  • [3. UI工具类](#3. UI工具类)

概述

目前大多数的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()
    }
}
相关推荐
tangweiguo030519874 天前
Android Compose Activity 页面跳转动画详解
android·compose
tangweiguo030519874 天前
在 Jetpack Compose 中实现 iOS 风格输入框
android·compose
tangweiguo0305198712 天前
Android Compose 权限申请完整指南
compose
tangweiguo0305198717 天前
androd的XML页面 跳转 Compose Activity 卡顿问题
compose
tangweiguo0305198717 天前
iOS 风格弹框组件集 (Compose版)
compose
tangweiguo0305198717 天前
Android Material Design 3 主题配色终极指南:XML 与 Compose 全解析
compose
tangweiguo0305198718 天前
Android Compose 中获取和使用 Context 的完整指南
android·compose
tangweiguo0305198720 天前
Jetpack Compose 自定义组件完全指南
compose
tangweiguo0305198721 天前
打破界限:Android XML与Jetpack Compose深度互操作指南
android·kotlin·compose
wavky1 个月前
零经验选手,Compose 一天开发一款小游戏!
compose