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,这一期比较复杂的就是滑动时标题栏和背景图的那个交互的问题,其他没有特别困难实现的。其实朋友圈还有很多内容没有实现,比如评论,视频播放等等,这一期只是实现了基本的页面内容。下一期计划开发朋友圈图片的预览功能。

相关推荐
x02415 天前
Android Room(SQLite) too many SQL variables异常
sqlite·安卓·android jetpack·1024程序员节
alexhilton18 天前
深入理解观察者模式
android·kotlin·android jetpack
Wgllss18 天前
花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
android·性能优化·android jetpack
上官阳阳21 天前
使用Compose创造有趣的动画:使用Compose共享元素
android·android jetpack
沐言人生25 天前
Android10 Framework—Init进程-15.属性变化控制Service
android·android studio·android jetpack
IAM四十二1 个月前
Android Jetpack Core
android·android studio·android jetpack
王能1 个月前
Kotlin真·全平台——Kotlin Compose Multiplatform Mobile(kotlin跨平台方案、KMP、KMM)
android·ios·kotlin·web·android jetpack·kmp·kmm
alexhilton1 个月前
让Activity更加优雅地跳转
android·kotlin·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-11.客户端操作属性
android·android studio·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack