Jetpack Compose 实战之仿微信UI -实现朋友圈(三)

前言

回顾下之前的内容,我们已经实现了登陆页和首页的相关页面,它们分别是:

Jetpack Compose 实战之仿微信UI -实现登陆页(一)

Jetpack Compose 实战之仿微信UI -实现首页(二)

在这一篇文章中,我将使用 Jetpack Compose 去实现微信朋友圈的页面。

先看下效果图

页面构成

这一期的页面构成比较简单,我就使用一个Activity和一个组合函数页面去实现,它们分别是:

页面结构梳理

我将页面主要拆为三部位,分别为顶部的背景图标题栏朋友圈列表,接下来将一步一步去实现它们。

背景图

背景图部位主要为一张网络图片和右下角的个人头像组成,网络图片将使用前面提到的 coil,引入的方式为:

arduino 复制代码
implementation "com.google.accompanist:accompanist-coil:0.11.0"

代码的具体实现

ini 复制代码
@Composable
fun MomentTopItem() {
    Box(modifier = Modifier
        .fillMaxWidth()
        .height(380.dp)) {
        /**
         * 背景图片
         */
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
        ) {
            Image(
                painter = rememberCoilPainter(request = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202007%2F01%2F20200701134954_yVnHK.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704867043&t=210af5b7a7cb8def124dac87711ebf47"),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxSize()
            )
            Column(
                modifier = Modifier
                    .fillMaxSize()
            ) {
                //Spacer(modifier = Modifier.statusBarsHeight())
            }
        }
        /**
         * 右下角的个人信息
         */
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentWidth(Alignment.End)
                .padding(top = 260.dp, end = 15.dp),
            contentAlignment = Alignment.BottomEnd
        ) {
            Row() {
                Text(
                    text = "李莫愁",
                    color = Color.White,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold,
                    modifier = Modifier.padding(end = 6.dp, top = 13.dp)
                )
                Box(
                    modifier = Modifier
                        .size(60.dp)
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.White)
                ) {
                    Image(
                        painter = rememberCoilPainter(request = myAvatar),
                        contentDescription = null,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .fillMaxSize()
                            .clip(RoundedCornerShape(8.dp))
                    )
                }
            }
        }
    }
}

这部分的代码比较好阅读,主要使用帧布局的方式将个人信息部分叠加在背景图片上面,通过设置 BoxcontentAlignment = Alignment.BottomEnd 就可以将个人信息部分位于右下角。

标题栏

这个标题栏内容比较少,只有一个返回图标,发动态的相机图标和中间的标题,为啥还要拿出来说呢?

我们先看下微信的效果:

通过观察我们发现,标题栏一开始并没有显示标题,而是向下滑动到一定距离后才显示,而且是由浅到深出现的,图标的样式也是在变化的(开始是白色的,出现标题后变黑色),这里我们要怎样实现呢?

首先,因为有从浅到深的样式的变化,所以我们很容易就是通过修改组件的透明度样式,但是 Modifier 有没有这个属性呢,我们通过Modifier的拓展函数找到了,它为:

kotlin 复制代码
@Stable
fun Modifier.alpha(
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float
) = if (alpha != 1.0f) graphicsLayer(alpha = alpha, clip = true) else this

有了这个属性,我们是可以通过透明度的变化达到我们的效果的,但是我们怎样拿到这个变化的值呢。

前面我们说过,它是通过距离的滚动变化的,所以我们可以通过滚动的状态来计算这个变化的值

具体的代码实现

ini 复制代码
@Composable
fun MomentHeader(
    scrollState: LazyListState,
    statusBarHeight: Dp,
    systemUiController: SystemUiController
) {
    val context = LocalContext.current as Activity
    /**
     *  滚动距离的目标值
     */
    val target = LocalDensity.current.run { 250.dp.toPx() }
    /**
     * 列表的索引值
     */
    val firstVisibleItemIndex = remember { derivedStateOf { scrollState.firstVisibleItemIndex } }
    /**
     * 当前滚动的值(Y轴距离)
     */
    val firstVisibleItemScrollOffset = remember { derivedStateOf { scrollState.firstVisibleItemScrollOffset } }
    /**
     * 滚动的百分比
     */
    val scrollPercent: Float = if (firstVisibleItemIndex.value > 0) {
        1f
    } else {
        firstVisibleItemScrollOffset.value / target
    }

    /**
     * 定义一个变量记录状态栏的颜色设置,避免滚动时一直在修改
     */
    var isTransparent by rememberSaveable { mutableStateOf(true) }
    if (scrollPercent > 0) {
        if (isTransparent) {
            systemUiController.setSystemBarsColor(
                color = Color(0xFFEDEDED),
                darkIcons = true,
            )
            isTransparent = false
        }
    } else {
        systemUiController.setSystemBarsColor(
            color = Color.Transparent,
            darkIcons = false,
        )
        isTransparent = true
    }
    val backgroundColor = Color(0xFFEDEDED)
    Column {
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = statusBarHeight)
                .statusBarsHeight()
                .alpha(scrollPercent)
                .background(backgroundColor)
        )
        Box(modifier = Modifier.height(44.dp)) {
            Spacer(
                modifier = Modifier
                    .fillMaxSize()
                    .alpha(scrollPercent)
                    .background(backgroundColor)
            )
            Row(verticalAlignment = Alignment.CenterVertically) {
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .padding(start = 15.dp)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.CenterStart
                ) {
                    Icon(
                        imageVector = Icons.Filled.ArrowBackIos,
                        contentDescription = null,
                        modifier = Modifier
                            .size(20.dp)
                            .align(Alignment.CenterStart)
                            .clickable {
                                context.finish()
                            },
                        tint = if (scrollPercent > 0) Color(0xff2E2E2E) else Color.White
                    )
                }
                Box(
                    modifier = Modifier
                        .weight(4f)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "朋友圈",
                        fontSize = 16.sp,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.alpha(scrollPercent)
                    )
                }
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .padding(end = 15.dp)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.CenterEnd
                ) {
                    Icon(
                        imageVector = Icons.Filled.PhotoCamera,
                        contentDescription = null,
                        modifier = Modifier
                            .size(20.dp)
                            .align(Alignment.CenterEnd),
                        tint = if (scrollPercent > 0) Color(0xff2E2E2E) else Color.White
                    )
                }
            }
        }
    }
}

实现逻辑说明

通过设置一个滚动的目标值,然后根据当前滚动的距离就可以计算得到一个百分比,如目标值为100,当前滚动的距离为100,它们的百分比就是1,即透明度就为了。

val target = LocalDensity.current.run { 250.dp.toPx() }

这个是我们设定的滚动目标值(前面我们设置的背景图片的高度为300,所以这个需要比300小,不然滚动超过了背景图透明度还没达到1,尽量也别太小,不然滚动一点点距离透明度就为1了)

其他的属性都有注释,阅读难度不高,到这里我们就实现了我们要的效果了。

朋友圈列表

在这里,首先我们定义我们的数据对象:

arduino 复制代码
data class MomentItem(
    /**
     * 头像
     */
    val avatar: String,
    /**
     * 姓名
     */
    val name: String,
    /**
     * 朋友圈内容
     */
    val content: String,
    /**
     * 图片列表
     */
    val images: List<String>,
    /**
     * 发布时间
     */
    val createTime: String,
    /**
     * 评论的内容(正常是一个比较复杂的结构,我这里只是单纯的一个文本)
     */
    val comment: String? = null,
)

Item的具体代码实现

ini 复制代码
@Composable
fun MomentItemView(it: MomentItem, context: Context) {
    Box(modifier = Modifier
        .fillMaxWidth()
        .padding(10.dp)
    ) {
        Row() {
            Box(modifier = Modifier
                .padding(top = 4.dp)
                .width(45.dp)
                .height(45.dp)
                ) {
                Image(
                    painter = rememberCoilPainter(request = it.avatar),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .fillMaxSize()
                        .clip(RoundedCornerShape(6.dp))
                )
            }
            MomentImageItemView(it, context)
        }
    }
}

@Composable
fun MomentImageItemView(it: MomentItem, context: Context) {
    val defaultHeight = 100.dp
    var height: Dp = defaultHeight
    val size = it.images.size
    if (size > 6) {
        height =  defaultHeight * 3
    } else if(size in 4..6) {
        height =  defaultHeight * 2
    }
    Column {
        Text(
            text = it.name,
            modifier = Modifier.padding(start = 6.dp),
            fontSize = 16.sp,
            color = Color(0xff61698e)

        )
        Text(
            text = it.content,
            modifier = Modifier.padding(start = 6.dp),
            fontSize = 16.sp,
            color = Color(0xff1e1e1e)
        )
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(height)
                .padding(start = 10.dp, top = 0.dp)
        ) {
            LazyVerticalGrid(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalArrangement = Arrangement.spacedBy(4.dp),
                columns = GridCells.Fixed(3),
                userScrollEnabled = false,
                content = {
                    itemsIndexed(it.images) { index, photo ->
                        Box(
                            modifier = Modifier
                                .height(100.dp)
                                .fillMaxWidth()
                                .clickable {
                                    val images: ArrayList<String> = ArrayList()
                                    it.images.forEachIndexed { _, s ->
                                        images.add(s)
                                    }
                                    ImageBrowserActivity.navigate(
                                        context = context,
                                        images = images,
                                        currentIndex = index
                                    )
                                }
                        ) {
                            Image(
                                painter = rememberCoilPainter(request = photo),
                                modifier = Modifier
                                    .fillMaxSize(),
                                contentDescription = null,
                                contentScale = ContentScale.FillBounds
                            )
                        }
                    }
                }
            )
        }
        Row(modifier = Modifier.fillMaxWidth()) {
            Text(
                text = it.createTime,
                modifier = Modifier
                    .padding(start = 6.dp)
                    .weight(1f),
                fontSize = 12.sp,
                color = Color(0xff1e1e1e),
                textAlign = TextAlign.Start
            )
            Box(
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 4.dp, bottom = 8.dp),
                contentAlignment = Alignment.CenterEnd
            ) {
                Box(
                    modifier = Modifier
                        .width(30.dp)
                        .height(20.dp)
                        .clip(RoundedCornerShape(4.dp))
                        .background(Color(0xFFEDEDED)),
                    contentAlignment = Alignment.Center
                ) {
                    Icon(
                        imageVector = Icons.Filled.MoreHoriz,
                        contentDescription = null,
                        modifier = Modifier.size(25.dp),
                        tint = Color(0xff000000)
                    )
                }
            }
        }
        if (it.comment != null && it.comment != "") {
            Box(modifier = Modifier
                .fillMaxWidth()
                .clip(RoundedCornerShape(4.dp))
                .background(Color(0xFFEDEDED))
            ) {
                Text(
                    text = it.comment,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(6.dp),
                    fontSize = 14.sp,
                    color = Color(0xff1e1e1e),
                    textAlign = TextAlign.Start
                )
            }
        }
    }
}

这里有个需要优化的地方是网格图片我使用了 LazyVerticalGrid 来实现,但是如果它的父布局没有设定高度的话会报错,没有找得设置自适应的属性,所以我通过照片的数量动态设置高度。

列表数据我是使用 Paging 来模拟数据分页,在MomentViewModel里定义模拟数据,

MomentViewModel具体代码为:

css 复制代码
class MomentViewModel : ViewModel() {
    val rankMomentItems: Flow<PagingData<MomentItem>> =
        Pager(PagingConfig(pageSize = 10, prefetchDistance = 1)) {
            FriendsMomentSource()
        }.flow
}

其中:

FriendsMomentSource

kotlin 复制代码
class FriendsMomentSource: PagingSource<Int, MomentItem>() {

    override fun getRefreshKey(state: PagingState<Int, MomentItem>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentItem> {
        return try {
            val nextPage = params.key ?: 1
            val momentListResponse = momentList
            if (nextPage > 1) {
                delay(2000)
            }
            LoadResult.Page(
                data = momentListResponse,
                prevKey = if (nextPage == 1) null else nextPage - 1,
                nextKey = nextPage + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
perl 复制代码
val momentList = listOf(
    MomentItem(
        "https://img.duoziwang.com/2019/07/12080849900677.jpg",
        "罗33",
        "我的世界,轮不到你来指手画脚。活着不是为了取悦谁,自己开心才天下无敌",
        listOf(
            "https://img2.baidu.com/it/u=3056820671,249401292&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F0b510d5f-00e8-4491-9819-22c64cd21d49%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102466&t=69174643a822df839568a84243bcb2c8",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Faadc73ce-df89-43d1-91f5-46885e1f3967%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102466&t=623c52cee6f333a0a3e0d23621f20851",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F351f347e-6624-4894-8378-da9db92295da%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102466&t=1c877e6daf0e76bb2832f1e12f33be84",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw%2Feb4d2adf-7d0b-497d-b555-61b3b10be698%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102467&t=1123b047090687d3150462603f4aacdf",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw%2F95065e91-9de5-4135-b636-93f8190f06fd%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102467&t=bd4fe7775d2d7e8ce4b0a83232256218",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw%2Fae357ca9-04e0-43a5-ae0e-24f68b951b81%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102467&t=22d04b49ac0da4c942f08e79094ab9a9"
        ),
        "5分钟前",
        "黎明:啥时候的?"
    ),
    MomentItem(
        "https://img.duoziwang.com/2019/07/12080849900677.jpg",
        "罗33",
        "我的世界,轮不到你来指手画脚。活着不是为了取悦谁,自己开心才天下无敌",
        listOf(
            "https://img2.baidu.com/it/u=3056820671,249401292&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F0b510d5f-00e8-4491-9819-22c64cd21d49%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102466&t=69174643a822df839568a84243bcb2c8",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Faadc73ce-df89-43d1-91f5-46885e1f3967%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704102466&t=623c52cee6f333a0a3e0d23621f20851",
        ),
        "5分钟前",
        "黎明:啥时候的?"
    ),
    ...

实现的全部代码为:

ini 复制代码
@Composable
fun MomentScreen(
    viewModel: MomentViewModel = MomentViewModel(),
) {
    /**
     * 状态栏设置
     */
    val systemUiController = rememberSystemUiController()
    SideEffect {
        systemUiController.setSystemBarsColor(
            color = Color.Transparent,
            darkIcons = false,
        )
    }
    /**
     * 获取状态栏高度
     */
    val statusBarHeight = LocalDensity.current.run {
        WindowInsets.statusBars.getTop(this).toDp()
    }
    val lazyMomentItems = viewModel.rankMomentItems.collectAsLazyPagingItems()
    val scrollState = rememberLazyListState()
    val context = LocalContext.current
    Box {
        LazyColumn(
            contentPadding = PaddingValues(0.dp),
            state = scrollState,
        ) {
            item {
                MomentTopItem()
            }
            items(lazyMomentItems) {
                it?.let {
                    MomentItemView(it, context)
                }
            }

            lazyMomentItems.apply {
                when (loadState.append) {
                    is LoadState.Loading -> {
                        item {
                            Loading()
                        }
                    } else -> {
                }
                }
            }
            item {
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
        MomentHeader(scrollState, statusBarHeight, systemUiController)
    }
}

@Composable
fun MomentScreenUI() {}

@Composable
fun MomentItemView(it: MomentItem, context: Context) {
    Box(modifier = Modifier
        .fillMaxWidth()
        .padding(10.dp)
    ) {
        Row() {
            Box(modifier = Modifier
                .padding(top = 4.dp)
                .width(45.dp)
                .height(45.dp)
                ) {
                Image(
                    painter = rememberCoilPainter(request = it.avatar),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .fillMaxSize()
                        .clip(RoundedCornerShape(6.dp))
                )
            }
            MomentImageItemView(it, context)
        }
    }
}

@Composable
fun MomentImageItemView(it: MomentItem, context: Context) {
    val defaultHeight = 100.dp
    var height: Dp = defaultHeight
    val size = it.images.size
    if (size > 6) {
        height =  defaultHeight * 3
    } else if(size in 4..6) {
        height =  defaultHeight * 2
    }
    Column {
        Text(
            text = it.name,
            modifier = Modifier.padding(start = 6.dp),
            fontSize = 16.sp,
            color = Color(0xff61698e)

        )
        Text(
            text = it.content,
            modifier = Modifier.padding(start = 6.dp),
            fontSize = 16.sp,
            color = Color(0xff1e1e1e)
        )
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(height)
                .padding(start = 10.dp, top = 0.dp)
        ) {
            LazyVerticalGrid(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalArrangement = Arrangement.spacedBy(4.dp),
                columns = GridCells.Fixed(3),
                userScrollEnabled = false,
                content = {
                    itemsIndexed(it.images) { index, photo ->
                        Box(
                            modifier = Modifier
                                .height(100.dp)
                                .fillMaxWidth()
                                .clickable {
                                    val images: ArrayList<String> = ArrayList()
                                    it.images.forEachIndexed { _, s ->
                                        images.add(s)
                                    }
                                    ImageBrowserActivity.navigate(
                                        context = context,
                                        images = images,
                                        currentIndex = index
                                    )
                                }
                        ) {
                            Image(
                                painter = rememberCoilPainter(request = photo),
                                modifier = Modifier
                                    .fillMaxSize(),
                                contentDescription = null,
                                contentScale = ContentScale.FillBounds
                            )
                        }
                    }
                }
            )
        }
        Row(modifier = Modifier.fillMaxWidth()) {
            Text(
                text = it.createTime,
                modifier = Modifier
                    .padding(start = 6.dp)
                    .weight(1f),
                fontSize = 12.sp,
                color = Color(0xff1e1e1e),
                textAlign = TextAlign.Start
            )
            Box(
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 4.dp, bottom = 8.dp),
                contentAlignment = Alignment.CenterEnd
            ) {
                Box(
                    modifier = Modifier
                        .width(30.dp)
                        .height(20.dp)
                        .clip(RoundedCornerShape(4.dp))
                        .background(Color(0xFFEDEDED)),
                    contentAlignment = Alignment.Center
                ) {
                    Icon(
                        imageVector = Icons.Filled.MoreHoriz,
                        contentDescription = null,
                        modifier = Modifier.size(25.dp),
                        tint = Color(0xff000000)
                    )
                }
            }
        }
        if (it.comment != null && it.comment != "") {
            Box(modifier = Modifier
                .fillMaxWidth()
                .clip(RoundedCornerShape(4.dp))
                .background(Color(0xFFEDEDED))
            ) {
                Text(
                    text = it.comment,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(6.dp),
                    fontSize = 14.sp,
                    color = Color(0xff1e1e1e),
                    textAlign = TextAlign.Start
                )
            }
        }
    }
}

/**
 * 顶部的朋友圈背景图
 */
@Composable
fun MomentTopItem() {
    Box(modifier = Modifier
        .fillMaxWidth()
        .height(380.dp)) {
        /**
         * 背景图片
         */
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
        ) {
            Image(
                painter = rememberCoilPainter(request = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202007%2F01%2F20200701134954_yVnHK.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704867043&t=210af5b7a7cb8def124dac87711ebf47"),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxSize()
            )
            Column(
                modifier = Modifier
                    .fillMaxSize()
            ) {
                //Spacer(modifier = Modifier.statusBarsHeight())
            }
        }
        /**
         * 右下角的个人信息
         */
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentWidth(Alignment.End)
                .padding(top = 260.dp, end = 15.dp),
            contentAlignment = Alignment.BottomEnd
        ) {
            Row() {
                Text(
                    text = "李莫愁",
                    color = Color.White,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold,
                    modifier = Modifier.padding(end = 6.dp, top = 13.dp)
                )
                Box(
                    modifier = Modifier
                        .size(60.dp)
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.White)
                ) {
                    Image(
                        painter = rememberCoilPainter(request = myAvatar),
                        contentDescription = null,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .fillMaxSize()
                            .clip(RoundedCornerShape(8.dp))
                    )
                }
            }
        }
    }
}

/**
 * 朋友圈标题栏
 */
@Composable
fun MomentHeader(
    scrollState: LazyListState,
    statusBarHeight: Dp,
    systemUiController: SystemUiController
) {
    val context = LocalContext.current as Activity
    /**
     *  滚动距离的目标值
     */
    val target = LocalDensity.current.run { 250.dp.toPx() }
    /**
     * 列表的索引值
     */
    val firstVisibleItemIndex = remember { derivedStateOf { scrollState.firstVisibleItemIndex } }
    /**
     * 当前滚动的值(Y轴距离)
     */
    val firstVisibleItemScrollOffset = remember { derivedStateOf { scrollState.firstVisibleItemScrollOffset } }
    /**
     * 滚动的百分比
     */
    val scrollPercent: Float = if (firstVisibleItemIndex.value > 0) {
        1f
    } else {
        firstVisibleItemScrollOffset.value / target
    }

    /**
     * 定义一个变量记录状态栏的颜色设置,避免滚动时一直在修改
     */
    var isTransparent by rememberSaveable { mutableStateOf(true) }
    if (scrollPercent > 0) {
        if (isTransparent) {
            systemUiController.setSystemBarsColor(
                color = Color(0xFFEDEDED),
                darkIcons = true,
            )
            isTransparent = false
        }
    } else {
        systemUiController.setSystemBarsColor(
            color = Color.Transparent,
            darkIcons = false,
        )
        isTransparent = true
    }
    val backgroundColor = Color(0xFFEDEDED)
    Column {
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = statusBarHeight)
                .statusBarsHeight()
                .alpha(scrollPercent)
                .background(backgroundColor)
        )
        Box(modifier = Modifier.height(44.dp)) {
            Spacer(
                modifier = Modifier
                    .fillMaxSize()
                    .alpha(scrollPercent)
                    .background(backgroundColor)
            )
            Row(verticalAlignment = Alignment.CenterVertically) {
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .padding(start = 15.dp)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.CenterStart
                ) {
                    Icon(
                        imageVector = Icons.Filled.ArrowBackIos,
                        contentDescription = null,
                        modifier = Modifier
                            .size(20.dp)
                            .align(Alignment.CenterStart)
                            .clickable {
                                context.finish()
                            },
                        tint = if (scrollPercent > 0) Color(0xff2E2E2E) else Color.White
                    )
                }
                Box(
                    modifier = Modifier
                        .weight(4f)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "朋友圈",
                        fontSize = 16.sp,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.alpha(scrollPercent)
                    )
                }
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .padding(end = 15.dp)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.CenterEnd
                ) {
                    Icon(
                        imageVector = Icons.Filled.PhotoCamera,
                        contentDescription = null,
                        modifier = Modifier
                            .size(20.dp)
                            .align(Alignment.CenterEnd),
                        tint = if (scrollPercent > 0) Color(0xff2E2E2E) else Color.White
                    )
                }
            }
        }
    }
}

其中下拉加载的Loading组件具体代码为:

ini 复制代码
@Composable
fun Loading() {
    Box(modifier = Modifier
        .fillMaxWidth()
        .height(30.dp)
        .wrapContentWidth(Alignment.CenterHorizontally)
    ) {
        Row(
            modifier = Modifier.fillMaxHeight(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center
        ) {
            CircularProgressIndicator(
                color = Color(0xFFCCCCCC),
                modifier = Modifier
                    .height(18.dp)
                    .width(18.dp),
                strokeWidth = 1.5.dp
            )
            Text(
                text = "正在加载...",
                fontSize = 13.sp,
                color = Color(0xFF888888),
                modifier = Modifier.padding(start = 8.dp)
            )
        }
    }
}

到这里,朋友圈的页面就基本完成了。

总结

在这一期的内容中,我使用的列表组件为之前用过的 LazyColumn ,分页使用 Paging,这一期比较复杂的就是滑动时标题栏和背景图的那个交互的问题,其他没有特别困难实现的。其实朋友圈还有很多内容没有实现,比如评论,视频播放等等,这一期只是实现了基本的页面内容。下一期计划开发朋友圈图片的预览功能。

相关推荐
天花板之恋1 小时前
Compose状态管理
android jetpack
alexhilton17 小时前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
Lei活在当下2 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
天花板之恋2 天前
Compose之图片加载显示
android jetpack
消失的旧时光-19433 天前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
Tans53 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Lei活在当下4 天前
【业务场景架构实战】2. 对聚合支付 SDK 的封装
架构·android jetpack
Tans56 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
alexhilton8 天前
runBlocking实践:哪里该使用,哪里不该用
android·kotlin·android jetpack
Tans510 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读