前言
回顾下之前的内容,我们已经实现了登陆页和首页的相关页面,它们分别是:
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))
)
}
}
}
}
}
这部分的代码比较好阅读,主要使用帧布局的方式将个人信息部分叠加在背景图片上面,通过设置 Box 的 contentAlignment = 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,这一期比较复杂的就是滑动时标题栏和背景图的那个交互的问题,其他没有特别困难实现的。其实朋友圈还有很多内容没有实现,比如评论,视频播放等等,这一期只是实现了基本的页面内容。下一期计划开发朋友圈图片的预览功能。