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

前言

在上一篇文章中,我实现了登陆模块的几个主要页面,在这一篇文章中,我将使用 Jetpack Cmpose 去实现微信的首页的几个tab页主要包括微信,通讯录,发现和我。

页面构成

首页主要由一个Activity和四个组合函数页面,它们的结构如下:

页面结构梳理

我们通过观察发现,首页是通过点击底部的导航栏或者左右滑动切换页面的,在我们没有使用Compose开发之前,我们是使用ViewPager + TabLayout的方式实现的,在Compose里有没有这样的组件呢?

底部导航栏的实现

在上一篇文章中,我使用了 Scaffold 脚手架,我们看看这个脚手架的属性,有没有我们需要的,Scaffold的属性如下:

less 复制代码
Scaffold(
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable () -> Unit = {},
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    containerColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = contentColorFor(containerColor),
    contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
    content: @Composable (PaddingValues) -> Unit
)

由于熟悉flutter开发,看了这些属性,熟悉的身影出现了,没猜错的话就是bottomBar

底部导航栏主要包括名称和图标,接下来我将封装 NavigationItem 对象来使用

arduino 复制代码
/**
 * 首页底部导航栏
 */
data class NavigationItem(
    /**
     * 名称
     */
    val title: String,
    /**
     * 图标
     */
    val icon: ImageVector,
)

这里的图标我使用的是material自带的,通过 implementation "androidx.compose.material:material-icons-extended:$compose_version" 引入。

接下来定义导航栏的数组

less 复制代码
val navList = listOf(
    NavigationItem(
        "微信",
        Icons.Filled.Sms,
    ),
    NavigationItem(
        "通讯录",
        Icons.Filled.Contacts,
    ),
    NavigationItem(
        "发现",
        Icons.Filled.Explore,
    ),
    NavigationItem(
        "我",
        Icons.Filled.Person,
    ),
)

底部导航栏的具体实现

ini 复制代码
bottomBar = {
    NavigationBar(
        modifier = Modifier.height(60.dp),
        containerColor = Color(ContextCompat.getColor(context, if (selectIndex > 1) R.color.white else R.color.nav_bg)),
    ) {
        navList.forEachIndexed { index, nav ->
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .weight(1f)
                    .fillMaxHeight()
            ) {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Bottom
                ) {
                    Icon(
                        nav.icon, null,
                        modifier = Modifier.size(28.dp),
                        tint = Color(
                            if (selectIndex == index) ContextCompat.getColor(context, R.color.green)
                            else ContextCompat.getColor(context, R.color.gray)
                        )
                    )
                    Text(
                        text = nav.title,
                        fontSize = 12.sp,
                        color = Color(
                            if (selectIndex == index) ContextCompat.getColor(context, R.color.green)
                            else ContextCompat.getColor(context, R.color.gray)
                        )
                    )
                }
            }
        }
    }
},

在这里边,通过定义 selectIndex 来记录点击的tab,用来切换对应的样式。

看下效果

Tab页的实现

通过查看,我并没有看到 ViewPager 这样的组件,但是发现了可以实现类似功能的组件 HorizontalPager,它的结构如下:

less 复制代码
HorizontalPager(
    count: Int,
    modifier: Modifier = Modifier,
    state: PagerState = rememberPagerState(),
    reverseLayout: Boolean = false,
    itemSpacing: Dp = 0.dp,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(
        state = state,
        endContentPadding = contentPadding.calculateEndPadding(LayoutDirection.Ltr),
    ),
    key: ((page: Int) -> Any)? = null,
    userScrollEnabled: Boolean = true,
    content: @Composable PagerScope.(page: Int) -> Unit,
) 

这些属性没有很复杂的,基本可以通过名称就知道它的作用了,接下来使用 HorizontalPager 来实现我们的页面切换。

ini 复制代码
HorizontalPager(
    count = 4,
    state = pageState,
    contentPadding = PaddingValues(horizontal = 0.dp),
    modifier = Modifier.fillMaxSize()
) { page ->
    when(page) {
        0 -> Text(text = "这是微信页")
        1 -> Text(text = "这是通讯录页")
        2 -> Text(text = "这是发现页")
        3 -> Text(text = "这是我页")
    }
}

看下效果

细心的肯定可以发现,页面切换时底部导航栏没有变化啊,是的,还需要将Tab页和导航栏关联起来。

只需要在点击导航栏时通过 pageState 跳转对应的页面和切换页面时监听当前的页面将我们之前定义的 selectIndex 赋值即可,具体如下:

点击导航栏

ini 复制代码
Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
        .weight(1f)
        .fillMaxHeight()
        .click {
            selectIndex = index
            /**
             * 点击底部的tab切换对应的page
             */
            scope.launch {
                pageState.scrollToPage(index)
            }
        }
)

监听当前页面的切换

scss 复制代码
LaunchedEffect(pageState) {
    snapshotFlow { pageState.currentPage }.collect { page ->
        selectIndex = page
        println("LaunchedEffect currentPage: $page")
    }
}

看下调整后的效果

到这里,首页的主要结构和交互已经搭建完了

HomeScreen的全部实现代码如下:

ini 复制代码
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable
fun HomeScreen() {
    var selectIndex by rememberSaveable { mutableStateOf(0) }
    val pageState = rememberPagerState(initialPage = 0)
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    rememberSystemUiController().setStatusBarColor(Color.Transparent, darkIcons = true)
    Surface(
        Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
    ) {
        Scaffold(
            topBar = {
                CenterAlignedTopAppBar(
                    title = {
                        Text(
                            titles[selectIndex],
                            maxLines = 1,
                            fontSize = 16.sp,
                            overflow = TextOverflow.Ellipsis
                        )
                    },
                    actions = {
                        if(selectIndex != 3) {
                            IconButton(
                                onClick = {
                                    /* doSomething() */
                                }) {
                                Icon(
                                    imageVector = Icons.Filled.Search,
                                    contentDescription = null,
                                    modifier = Modifier.size(30.dp),
                                    tint = Color(ContextCompat.getColor(context, R.color.black_10))
                                )
                            }
                            IconButton(onClick = {
                                /* doSomething() */
                            }) {
                                Icon(
                                    imageVector = Icons.Filled.AddCircleOutline,
                                    contentDescription = null,
                                    modifier = Modifier.size(25.dp),
                                    tint = Color(ContextCompat.getColor(context, R.color.black_10))
                                )
                            }
                        }
                    },
                    colors = TopAppBarDefaults.mediumTopAppBarColors(
                        containerColor = Color(ContextCompat.getColor(context, if (selectIndex != 3) R.color.nav_bg else R.color.white)),
                        scrolledContainerColor = Color(ContextCompat.getColor(context, if (selectIndex != 3) R.color.nav_bg else R.color.white)),
                        navigationIconContentColor = Color.White,
                        titleContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
                        actionIconContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
                    )
                )
            },
            bottomBar = {
                NavigationBar(
                    modifier = Modifier.height(60.dp),
                    containerColor = Color(ContextCompat.getColor(context, if (selectIndex > 1) R.color.white else R.color.nav_bg)),
                ) {
                    navList.forEachIndexed { index, nav ->
                        Box(
                            contentAlignment = Alignment.Center,
                            modifier = Modifier
                                .weight(1f)
                                .fillMaxHeight()
                                .click {
                                    selectIndex = index
                                    /**
                                     * 点击底部的tab切换对应的page
                                     */
                                    scope.launch {
                                        pageState.scrollToPage(index)
                                    }
                                }
                        ) {
                            Column(
                                horizontalAlignment = Alignment.CenterHorizontally,
                                verticalArrangement = Arrangement.Bottom
                            ) {
                                Icon(
                                    nav.icon, null,
                                    modifier = Modifier.size(28.dp),
                                    tint = Color(
                                        if (selectIndex == index) ContextCompat.getColor(context, R.color.green)
                                        else ContextCompat.getColor(context, R.color.gray)
                                    )
                                )
                                Text(
                                    text = nav.title,
                                    fontSize = 12.sp,
                                    color = Color(
                                        if (selectIndex == index) ContextCompat.getColor(context, R.color.green)
                                        else ContextCompat.getColor(context, R.color.gray)
                                    )
                                )
                            }
                        }
                    }
                }
            },
            content = { innerPadding ->
                Box {
                    HorizontalPager(
                        count = 4,
                        state = pageState,
                        contentPadding = PaddingValues(horizontal = 0.dp),
                        modifier = Modifier.fillMaxSize()
                    ) { page ->
                        when(page) {
                            0 -> ChatSessionScreen(innerPadding)
                            1 -> AddrBookScreen(innerPadding)
                            2 -> FindScreen(innerPadding)
                            3 -> MineScreen(innerPadding)
                        }
                    }
                    LaunchedEffect(pageState) {
                        snapshotFlow { pageState.currentPage }.collect { page ->
                            selectIndex = page
                            println("LaunchedEffect currentPage: $page")
                        }
                    }
                }
            }
        )
    }
}

最后,补充下对应tab页的内容再看下效果

到这里,使用Jetpack Compose仿微信首页UI就开发完成了,看起来是不是有模有样了。

总结

在这一期开发中,主要使用 Scaffold 脚手架的 topBarbottomBar 分别实现了标题栏和底部导航栏,使用了 HorizontalPager 实现了页面切换。

相关推荐
程序员麻辣烫18 小时前
像AI一样思考
程序员
Junerver1 天前
在 Jetpack Compose 中扩展 useRequest 实现自定义数据处理、异常回滚
android·前端·android jetpack
一颗苹果OMG2 天前
关于进游戏公司实习的第一周
前端·程序员
沐言人生2 天前
Android10 Framework—Init进程-5.SEAndroid机制
android·android studio·android jetpack
万少2 天前
你会了吗 HarmonyOS Next 项目级别的注释规范
前端·程序员·harmonyos
楽码3 天前
彻底理解时间?在编程中使用原子钟
后端·算法·程序员
江南一点雨4 天前
又一家培训机构即将倒闭!打工人讨薪无果,想报名的小伙伴擦亮眼睛~
java·程序员
用户86178277365184 天前
ELK 搭建 & 日志集成
java·后端·程序员
河北小田4 天前
局部变量成员变量、引用类型、this、static
java·后端·程序员
文心快码 Baidu Comate4 天前
新一代的程序员如何培养自己的核心竞争力?(一)
人工智能·程序员·ai编程·文心快码·智能编程助手