一个简单的微信界面

  • 简述
  • 效果视频
  • 底部导航栏
    • 导航元素
    • 导航栏
    • 放入插槽
    • 绘制地图
  • 消息列表
    • 效果图
    • 实现
  • 聊天
    • 效果图
    • 实现
      • 气泡背景
  • 联系人界面
    • 效果图
    • 实现
  • 好友详情
    • 效果图
    • 实现
  • 发现
    • 效果图
    • 实现
      • 未读红点
      • 未读条数
  • 朋友圈
    • 效果图
    • 实现
      • 上拉加载
  • 个人设置
    • 效果图
    • 实现
      • 个人信息
      • 功能区
  • 钱包
    • 效果图
    • 实现
  • 切换主题
    • 效果图
    • 实现
  • 一键切换主题
    • 建立颜色实体类
    • 创建主题样式
    • 主题
    • 主题状态
    • 切换主题
    • 应用
  • 沉浸式状态栏
    • 依赖
  • Git链接

简述

此Demo用于熟悉Jetpack Compose,故仿造微信写了部分界面,其中Icon、Theme部分引用扔老师视频中元素

效果视频

Android Compose——一个简单的微信界面

底部导航栏

导航元素

使用封闭类建立底部导航栏四个元素

sealed class BottomNavItem(var title:String,var normalIcon:Int,var selectIcon:Int,var route:String){    object Message: BottomNavItem("微信", R.drawable.ic_chat_outlined,R.drawable.ic_chat_filled,"Message")    object MailList: BottomNavItem("通讯录", R.drawable.ic_contacts_outlined,R.drawable.ic_contacts_filled,"MailList")    object Finding: BottomNavItem("发现", R.drawable.ic_discovery_outlined,R.drawable.ic_discovery_filled,"Finding")    object Mine: BottomNavItem("我", R.drawable.ic_me_outlined,R.drawable.ic_me_filled,"Mine")}

导航栏

构建底部导航栏,其中NavControllerCompose用来导航路由(页面切换),unselectedContentColorselectedContentColor分别对应当此Item未选中和被选中两种状态颜色,使用主题颜色填充,方便后面切换主题的时候,发生相应变化

/** * 底部导航条*/@Composablefun BottomNavBar(navController: NavController){    /**     * 底部导航元素*/    val items = listOf(        BottomNavItem.Message,        BottomNavItem.MailList,        BottomNavItem.Finding,        BottomNavItem.Mine    )    BottomNavigation(        backgroundColor = BaseElementComposeTheme.colors.bottomBar    ) {        //存储了导航中回退栈的信息        val navBackStackEntry by navController.currentBackStackEntryAsState()        //获取当前的路由状态        val currentRoute = navBackStackEntry?.destination?.route        /**         * 遍历列表生成四个底部Item*/        items.forEach { item ->            val curSelected = currentRoute == item.route;            BottomNavigationItem(                icon = {                    Icon(                        painterResource(id = if(curSelected) item.selectIcon else item.normalIcon),                        item.title, modifier = Modifier.size(24.dp)) },                label = { Text(item.title, fontSize = 12.sp) },                alwaysShowLabel = true,                selected = curSelected,                unselectedContentColor = BaseElementComposeTheme.colors.icon,                selectedContentColor = BaseElementComposeTheme.colors.iconCurrent,                onClick = {                    navController.navigate(item.route){                        //弹出到图形的开始目的地                        // 避免建立大量目的地                        // 在用户选择项目时显示在后堆栈上                        navController.graph.startDestinationRoute?.let {                                route ->                            popUpTo(route){                                saveState = true                            }                        }                        //在以下情况下避免同一目标的多个副本                        //重新选择同一项目                        launchSingleTop = true                        //重新选择以前选定的项目时恢复状态                        restoreState = true                    }                }            )        }    }}

放入插槽

Scaffold相当于一个插槽,因为它在屏幕中预留了很多空位,比如底部导航栏、顶栏、FAB等,只需要通过命名可选参数填充即可

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")@Composablefun MainScreenView(chatController: NavHostController,chatModel: ChatModel){    val navController = rememberNavController()    Scaffold(        bottomBar = {BottomNavBar(navController)},    ){        NavigationGraph(navController,chatController,chatModel)    }}

绘制地图

每一个NavHostController都必须绑定一个NavHost,其中NavHost就相当于是绘制了一个联通图,每一个结点之间都是联通的,而NavHostController就是哪个驱动,负责切换两个结点,此处结点是声明的Compose函数,一般为一个页面的入口处;下面定义了四个结点,也就是底部导航栏四个结点,NavHostController只能切换绑定的NavHost中已经声明的结点,没有声明的结点,不能相互进行切换

/** * 每个 NavController 都必须与一个 NavHost 可组合项相关联 * route:路线是一个 String,用于定义指向可组合项的路径。您可以将其视为指向特定目的地的隐式深层链接。每个目的地都应该有一条唯一的路线。*/@Composablefun NavigationGraph(navHostController: NavHostController,chatController: NavHostController,chatModel: ChatModel){    /**     * 底部导航栏四个界面路线图*/    NavHost(navHostController, startDestination = BottomNavItem.Message.route){        composable(BottomNavItem.Message.route){            MessagePageView(chatController,chatModel)        }        composable(BottomNavItem.MailList.route){            MailListPageView(chatController,chatModel)        }        composable(BottomNavItem.Finding.route){            FindingPageView(chatController)        }        composable(BottomNavItem.Mine.route){            MinePageView(chatController)        }    }}

消息列表

效果图

实现

布局主体是Column+TopBar+LazyColumn,其中Spacer组件用占位,例如两个组件之间需要隔开一点间距,则可使用,具体是上下还是左右,设置其modifier的width和height属性即可

@Composablefun MessagePageView(chatController: NavHostController, chatModel: ChatModel){    Column(        Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxSize()    ) {        TopTitleBar("微信",R.drawable.ic_add)        Spacer(modifier = Modifier.height(10.dp))        MessageList(Modifier.weight(1f),chatModel){            chatController.navigate(RoutePoint.Chat.route)        }    }}

LazyColumn对应的是RecyclerView,但使用起来更方便,无需建立Adapter,而且还可在其中插入不同类型的子组件;其中Divider组件是分界线,例如两个组件之间需要一条直线进行分割,即可使用

@Composablefun MessageList(modifier: Modifier,chatModel: ChatModel,chatCallback: ()->Unit){    LazyColumn(        modifier            .background(BaseElementComposeTheme.colors.listItem)            .padding(10.dp),        verticalArrangement = Arrangement.spacedBy(10.dp),    ){        itemsIndexed(chatModel.chats){ index,item->            MessageItem(item){                chatModel.startChat(it)                chatCallback()            }            if(index < chatModel.chats.size-1)                Divider(                    startIndent = 68.dp,                    thickness = 0.8f.dp,                    color = BaseElementComposeTheme.colors.chatListDivider,                )        }    }}

其中ConstraintLayout对应命令式UI中的约束布局,使用效果一致,首先通过createRefs创建引用实体,然后在每个modifier.constrainAs()属性中进行引用;clip(shape = RoundedCornerShape(4.dp))用于给图片四个角进行圆角处理,具体数值可通过参数进行传入;实现modifier.clickable即可实现点击事件

/** * 使用ConstraintLayout布局构建Item*/@Composablefun MessageItem(chatBean: ChatBean,chatCallback: (ChatBean)->Unit){    ConstraintLayout(        modifier = Modifier            .fillMaxWidth()            .clickable {                chatCallback(chatBean)            }    ) {        //声明ConstraintLayout实例        val (image,title,content,time) = createRefs()        Image(            painter = painterResource(chatBean.userBean.wechatIcon),            contentDescription = chatBean.userBean.wechatName,            contentScale = ContentScale.Crop,            modifier = Modifier                .padding(4.dp)                .size(48.dp)                .clip(shape = RoundedCornerShape(4.dp))                .constrainAs(image) {                    //引用实例进行排版                    top.linkTo(parent.top)                    start.linkTo(parent.start)                    bottom.linkTo(parent.bottom)                }        )        useText(text = chatBean.userBean.wechatName, fontSize = 16, color = BaseElementComposeTheme.colors.textPrimary, modifier = Modifier            .constrainAs(title){                top.linkTo(image.top,2.dp)                start.linkTo(image.end,10.dp)            })        useText(text = chatBean.messageBeans.last().text,fontSize = 14, color = BaseElementComposeTheme.colors.textSecondary,modifier = Modifier            .fillMaxWidth()            .constrainAs(content) {                top.linkTo(title.bottom)                bottom.linkTo(image.bottom, 2.dp)                start.linkTo(image.end, 10.dp)                width = Dimension.fillToConstraints            })        useText(text = chatBean.messageBeans.last().time, fontSize = 12,color = BaseElementComposeTheme.colors.textSecondary,modifier = Modifier            .constrainAs(time){                top.linkTo(image.top)                end.linkTo(parent.end)            })    }}

聊天

聊天数据全为静态数据,通过ViewModelmutableStateOf创建一个有状态的数据进行存放,然后当当聊天框发送信息后,获取此ViewModel的实体,在此记录尾部添加一条信息,然后,监听此实体的组件就会进行重组,然后进行刷新改变

效果图

实现

布局主体是TopBar+LazyColumn+BottomBar

/** * 聊天界面*/@Composablefun ChatPagePreview(navHostController: NavHostController, chatModel: ChatModel){    val chat = chatModel.chatting    if (chat != null){        Column(            modifier = Modifier                .fillMaxSize()                .background(BaseElementComposeTheme.colors.background)        ) {            TitleBar(                title = chat.userBean.wechatName,                searchId = R.drawable.icon_more,                Modifier.padding(end = 10.dp)){                chatModel.contacting = chat.userBean                navHostController.navigate(RoutePoint.ContactDetail.route)            }            Spacer(modifier = Modifier.height(5.dp))            ChatList(chat,Modifier.weight(1f))            BottomInputBar{                val time = calculateTime()                chat.messageBeans.add(MessageBean(UserBean.ME,it,time))            }        }    }else{        Box(modifier = Modifier            .fillMaxSize()            .background(BaseElementComposeTheme.colors.background),            Alignment.Center){            useText(                text = "内容加载失败,请重试!",                color = BaseElementComposeTheme.colors.textPrimaryMe,                modifier = Modifier.fillMaxWidth(),                textAlign = TextAlign.Center)        }    }}

BasicTextField输入框的软键盘的回车键改为发送,然后对发送键进行点击事件监听

   keyboardActions = KeyboardActions (                onSend = {                    onInputListener(inputText)                    inputText = ""                }            ),            keyboardOptions = KeyboardOptions(                keyboardType = KeyboardType.Text,                imeAction = ImeAction.Send            )

己方发送消息,并对有状态的列表进行改变

`chat.messageBeans.add(MessageBean(UserBean.ME,it,time))`

通过判断list数据中发送消息的人是否为"自己"而进行左右排放

/** * 聊天记录*/@Composablefun ChatList(bean: ChatBean,modifier: Modifier){    LazyColumn(        verticalArrangement = Arrangement.spacedBy(10.dp),        modifier = modifier            .background(BaseElementComposeTheme.colors.chatPage)            .padding(top = 10.dp, start = 10.dp, end = 10.dp)    ){        items(bean.messageBeans.size){            if (bean.messageBeans[it].userBean == UserBean.ME){                MeMessage(bean.messageBeans[it])            }else{                OtherMessage(bean.messageBeans[it])            }        }    }}

气泡背景

此为己方发送消息是的消息气泡背景

fun Modifier.meBackground(color: Color):Modifier = this    .drawBehind {        val bubble = Path().apply {            val rect = RoundRect(                10.dp.toPx(),                0f,                size.width - 10.dp.toPx(),                size.height,                4.dp.toPx(),                4.dp.toPx()            )            addRoundRect(rect)            moveTo(size.width - 10.dp.toPx(), 15.dp.toPx())            lineTo(size.width - 5.dp.toPx(), 20.dp.toPx())            lineTo(size.width - 10.dp.toPx(), 25.dp.toPx())            close()        }        drawPath(bubble, color)    }    .padding(20.dp, 10.dp)

此为对方发送消息是的消息气泡背景

fun Modifier.otherBackground(color: Color):Modifier = this    .drawBehind {        val bubble = Path().apply {            val rect = RoundRect(                10.dp.toPx(),                0f,                size.width - 10.dp.toPx(),                size.height,                4.dp.toPx(),                4.dp.toPx()            )            addRoundRect(rect)            moveTo(10.dp.toPx(), 15.dp.toPx())            lineTo(5.dp.toPx(), 20.dp.toPx())            lineTo(10.dp.toPx(), 25.dp.toPx())            close()        }        drawPath(bubble, color)    }    .padding(20.dp, 10.dp)

联系人界面

效果图

实现

布局主体部分由Column+TopBar+LazyColumn

@Composablefun MailListPageView(chatController: NavHostController,chatModel: ChatModel){    Column(        Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxSize(),    ) {        TopTitleBar("通讯录", R.drawable.icon_add_friend)        Spacer(modifier = Modifier.height(10.dp))        ContactList(){            UserBean.AllFriend.forEach { bean ->                if(bean.wechatName == it){                    chatModel.contacting = bean                    chatController.navigate(RoutePoint.ContactDetail.route)                }            }        }    }}

LazyColumn由下面可展现其优势,item、items可插入不同的子组件,整体呈垂直排列

@Composablefun ContactList(onClick:(String)->Unit){    LazyColumn(        modifier = Modifier.background(BaseElementComposeTheme.colors.listItem),        contentPadding = PaddingValues(bottom = 50.dp)    ){        itemsIndexed(contactList) { index, item -> ContactItem(item, index, contactList.size){} }        item { useText("我的企业", fontSize = 14, color = BaseElementComposeTheme.colors.textSecondary,modifier = Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxWidth()            .padding(10.dp)) }        itemsIndexed(schoolList) { index, item -> ContactItem(item, index, schoolList.size){} }        item { useText("我的好友", fontSize = 14, color = BaseElementComposeTheme.colors.textSecondary,modifier = Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxWidth()            .padding(10.dp)) }        itemsIndexed(friendList) { index, item -> ContactItem(item, index, friendList.size){            onClick(item.title)        } }    }}

好友详情

效果图

实现

通过在ViewModel创建一个有状态的用户变量,点击哪个联系人,就把当前联系人信息赋值给VM中的值,然后联系人详情界面读取此VM值即可

/** * 联系人详情*/@Composablefun ContactDetailPreview(chatModel: ChatModel) {    val contact = chatModel.contacting    if (contact != null){        Box(            modifier = Modifier                .fillMaxSize()                .background(BaseElementComposeTheme.colors.background))        {            Column(modifier = Modifier                .background(BaseElementComposeTheme.colors.listItem)) {                DetailTopBar()                Spacer(modifier = Modifier.height(20.dp))                contactInfo(bean = contact)                Spacer(modifier = Modifier.height(30.dp))                Divider(                    thickness = 0.2.dp,                    color = BaseElementComposeTheme.colors.chatListDivider,                )                ContactFuncList()                VideoAndMessage()            }        }    }}

发现

效果图

实现

@Composablefun FindingPageView(chatController: NavHostController){    Column(        Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxSize()    ) {        TitleBar("发现",-1,Modifier.padding(10.dp)){}        Spacer(modifier = Modifier.height(10.dp))        FindingList(chatController)    }}@Composablefun FindingList(chatController: NavHostController){    LazyColumn(        modifier = Modifier            .background(BaseElementComposeTheme.colors.listItem)    ){        itemsIndexed(findingList){            index, item ->  FindingItem(item){if (it == "朋友圈"){chatController.navigate(RoutePoint.SpacePage.route)} }            if (index == 2){                Divider(                    thickness = 0.2.dp,                    color = BaseElementComposeTheme.colors.chatListDivider,                )            }else if (index < findingList.size){                Spacer(modifier = Modifier                    .height(10.dp)                    .fillMaxWidth()                    .background(BaseElementComposeTheme.colors.background))            }        }    }}

未读红点

未读消息红点

fun Modifier.unread(show: Boolean, color: Color): Modifier = this.drawWithContent {    drawContent()    if (show) drawCircle(color, 5.dp.toPx(), Offset(size.width - 1.dp.toPx(), 1.dp.toPx()))}

未读条数

一个红色圆圈内包含一个数字,可通过modifier的属性完成构建

@Composablefun AgreeNumber(number:Int){    useText(text = "$number", color = BaseElementComposeTheme.colors.onBadge, textAlign = TextAlign.Center,        modifier = Modifier            .background(BaseElementComposeTheme.colors.badge, shape = CircleShape)            .size(18.dp))}

朋友圈

效果图

实现

数据为存放在ViewModel中的有状态的数据变量中;上拉加载中,然后改变VM中的值,进而进行刷新,添加到第一个数据实体

上拉加载

使用协程模拟网络加载,并在协程中添加数据,即每下拉一次自己发送一个动态,即添加一条数据

 var refreshing by remember { mutableStateOf(false) }    //启用协程    val scope = rememberCoroutineScope()    val state = rememberPullRefreshState(refreshing = refreshing, onRefresh = {        scope.launch {            /**             * 在协程中使用延迟模拟网络延迟,然后加载数据*/            refreshing = true            delay(1000)            chatModel.spaceList.add(0,                SpaceBean(                    UserBean.ME,                    "让我们看看这是几啊:${chatModel.count.value++}",                "刚刚",                null))            refreshing = false        }    })

为最外层布局添加下拉刷新状态

 ConstraintLayout(            modifier = Modifier                .fillMaxSize()                .pullRefresh(state)        ){...}

下拉加载指示器,因为我的外层使用的是ConstraintLayout约束布局,所以使用以下方式放到顶端中部位置,backgroundColor为背景颜色,contentColor为指示器内部元素颜色

   PullRefreshIndicator(                refreshing = refreshing,                state = state,                backgroundColor = green4,                contentColor = white,                modifier = Modifier.constrainAs(refreshRef){                    top.linkTo(parent.top)                    start.linkTo(parent.start)                    end.linkTo(parent.end)                }            )

个人设置

效果图

实现

分为两部分,顶部个人信息区域、下面功能区域

个人信息

使用ConstraintLayout布局进行构建,约束布局对复杂页面构建较为方便,嵌套较少,当然在Compose中嵌套深浅对性能的影响不是很大,与命令式UI有显著差距

@Composablefun MineInfo(){    ConstraintLayout(        modifier = Modifier            .background(BaseElementComposeTheme.colors.listItem)            .fillMaxWidth()            .padding(20.dp)            .height(100.dp)) {        val (iconRef,nameRef,idRef,qrcodeRef,addStatusRef,statesRef,moreRef) = createRefs()        Image(            painter = painterResource(id = UserBean.ME.wechatIcon),            contentDescription = UserBean.ME.wechatName,            contentScale = ContentScale.Crop,            modifier = Modifier                .size(64.dp)                .clip(RoundedCornerShape(4.dp))                .constrainAs(iconRef) {                    top.linkTo(parent.top)                    bottom.linkTo(parent.bottom)                    start.linkTo(parent.start)                })        useText(            text = UserBean.ME.wechatName,            color = BaseElementComposeTheme.colors.textPrimary,            fontSize = 18,            fontWeight = FontWeight.Bold,            modifier = Modifier.constrainAs(nameRef){                top.linkTo(iconRef.top,2.dp)                start.linkTo(iconRef.end,15.dp)            }        )        useText(            text = "微信号: ${UserBean.ME.wechatId}",            color = BaseElementComposeTheme.colors.textSecondary,            fontSize = 14,            modifier = Modifier.constrainAs(idRef){                top.linkTo(nameRef.bottom)                bottom.linkTo(iconRef.bottom,2.dp)                start.linkTo(iconRef.end,15.dp)            }        )        Icon(            painter = painterResource(id = R.drawable.ic_qrcode),            contentDescription = "QRCode",            tint = BaseElementComposeTheme.colors.onBackground,            modifier = Modifier                .size(16.dp)                .constrainAs(qrcodeRef) {                    top.linkTo(idRef.top)                    //start.linkTo(idRef.end)                    end.linkTo(moreRef.start, 20.dp)                }        )        Icon(            painter = painterResource(id = R.drawable.ic_arrow_more),            contentDescription = "更多",            tint = BaseElementComposeTheme.colors.more,            modifier = Modifier                .size(16.dp)                .constrainAs(moreRef) {                    top.linkTo(idRef.top)                    end.linkTo(parent.end, (-10).dp)                }        )        addStates(            icon = R.drawable.icon_addition,            text = "状态",            modifier = Modifier.constrainAs(addStatusRef){                top.linkTo(idRef.bottom,10.dp)                start.linkTo(idRef.start)            })        addStates(            icon = R.drawable.image_friend_three,            text = "1个朋友",            modifier = Modifier.constrainAs(statesRef){                top.linkTo(addStatusRef.top)                start.linkTo(addStatusRef.end,10.dp)            })    }}

功能区

NavHostControllernavigate用于导航路由,传入的参数为目的地页面定义时的昵称,类型为String类型

@Composablefun MineList(chatController: NavHostController){    LazyColumn(modifier = Modifier        .background(BaseElementComposeTheme.colors.listItem)        .wrapContentHeight()        .fillMaxWidth()) {        item {            Spacer(modifier = Modifier                .height(10.dp)                .fillMaxWidth()                .background(BaseElementComposeTheme.colors.background))        }        itemsIndexed(mineList){            index, item ->  MineItem(bean = item){            when(it){                "服务" -> chatController.navigate(RoutePoint.ServicePage.route)                "设置" -> chatController.navigate(RoutePoint.ThemePage.route)            } }            if (index == 0 || index == mineList.size-2){                Spacer(modifier = Modifier                    .height(10.dp)                    .fillMaxWidth()                    .background(BaseElementComposeTheme.colors.background))            }else if (index < mineList.size-1){                Divider(                    thickness = 0.2.dp,                    color = BaseElementComposeTheme.colors.chatListDivider,                )            }        }    }}

钱包

效果图

实现

布局主体是Column+LazyColumn+LazyVerticalGrid

@Composablefun ServiceList(){    LazyColumn(    ){        item {            WalletArea()            Spacer(modifier = Modifier.height(10.dp))        }        items(serviceList.size){            ServiceItem(serviceList[it])            if (it < serviceList.size - 1){                Spacer(modifier = Modifier.height(10.dp))            }        }    }}

LazyVerticalGrid对应GridView,使用GridCells.Fixed(4)定义列数

@Composablefun ServiceItem(bean: ServiceBean){    Column(        modifier = Modifier            .fillMaxWidth()            .background(BaseElementComposeTheme.colors.listItem, shape = RoundedCornerShape(10.dp))            .padding(10.dp)            .height(if (bean.services.size > 4) 200.dp else 100.dp)    )    {        useText(            text = bean.name,            color = grey5,            textAlign = TextAlign.Start)        Spacer(modifier = Modifier.height(20.dp))        LazyVerticalGrid(            columns = GridCells.Fixed(4),            verticalArrangement = Arrangement.spacedBy(40.dp),            horizontalArrangement = Arrangement.SpaceEvenly,            modifier = Modifier.fillMaxWidth()        ){            items(bean.services.size){                Service(bean.services[it])            }        }    }}

切换主题

效果图

实现

四个颜色块对应四个主题,然后使用LazyVerticalGrid进行布局构建

@Composablefun ThemePagePreview(chatModel: ChatModel) {    var text by remember { mutableStateOf("古典灰") }    Box(        modifier = Modifier            .background(BaseElementComposeTheme.colors.background)            .fillMaxSize()            .padding(start = 20.dp, end = 20.dp))    {        Column(            modifier = Modifier                .background(white, shape = RoundedCornerShape(10.dp))                .fillMaxWidth()                .wrapContentHeight()                .padding(20.dp)                .align(Alignment.Center)                .shadow(elevation = 5.dp, ambientColor = green4, spotColor = Color.Transparent),            horizontalAlignment = Alignment.CenterHorizontally)        {            useText(text = "请选择一个主题", color = BaseElementComposeTheme.colors.textPrimaryMe, fontSize = 14)            Spacer(modifier = Modifier.height(5.dp))            useText(text = text, color = BaseElementComposeTheme.colors.textSecondary, fontSize = 12)            Spacer(modifier = Modifier.height(10.dp))            ThemeList(){                /**                 * 将选择的主题进行刷新,然后保存到缓存中*/                chatModel.theme.value = when(it){                    0-> {                        text = "古典灰"                        SPUtil.getInstance().PutData("Theme",0)                        BaseElementComposeTheme.Theme.Light                    }                    1-> {                        text = "哑光黑"                        SPUtil.getInstance().PutData("Theme",1)                        BaseElementComposeTheme.Theme.Dark                    }                    2-> {                        text = "活力红"                        SPUtil.getInstance().PutData("Theme",2)                        BaseElementComposeTheme.Theme.NewYear                    }                    3-> {                        text = "青春绿"                        SPUtil.getInstance().PutData("Theme",3)                        BaseElementComposeTheme.Theme.Green                    }                    else -> {                        text = "古典灰"                        SPUtil.getInstance().PutData("Theme",0)                        BaseElementComposeTheme.Theme.Light                    }                }            }        }    }}@Composablefun ThemeList(onClick:(Int)->Unit){    val colors = listOf(        white2,        black2,        red5,        green4    )    LazyVerticalGrid(        columns = GridCells.Fixed(2),        verticalArrangement = Arrangement.spacedBy(20.dp),        horizontalArrangement = Arrangement.spacedBy(20.dp))    {        items(colors.size){            Card(                backgroundColor = colors[it],                shape = RoundedCornerShape(10.dp),                modifier = Modifier                    .size(100.dp)                    .clickable {onClick(it)}            ) {            }        }    }}

一键切换主题

建立颜色实体类

首先建立一个颜色实体类,包括你所要使用的所有颜色,以下为例

@Stableclass BaseElementComposeColors(  bottomBar: Color,  background: Color) {  var bottomBar: Color by mutableStateOf(bottomBar)    private set  var background: Color by mutableStateOf(background)    private set}

创建主题样式

亮色主题

private val LightColorPalette = BaseElementComposeColors(  bottomBar = white1,//底部导航栏背景颜色  background = white2,//主题背景颜色)

暗黑主题

private val DarkColorPalette = BaseElementComposeColors(  bottomBar = black1,  background = black2,)

红色主题

private val NewYearColorPalette = BaseElementComposeColors(  bottomBar = red4,  background = red5,)

绿色主题

private val GreenColorPalette = BaseElementComposeColors(  bottomBar = green4,  background = green5,)

设置默认主题

private val LocalWeComposeColors = compositionLocalOf {  LightColorPalette}

使用枚举类将创建的主题进行包裹,使用一个别名

object BaseElementComposeTheme {  val colors: BaseElementComposeColors    @Composable    get() = LocalWeComposeColors.current  enum class Theme {    Light, Dark, NewYear,Green  }}

主题

判断当前系统主题是否为暗黑主题

@Composablefun isSystemDark():Boolean = isSystemInDarkTheme()

对主题内所有颜色进行切换,此处没有对形状、排版等主题进行切换

@Composablefun BaseElementComposeTheme(theme: BaseElementComposeTheme.Theme = BaseElementComposeTheme.Theme.Light, content: @Composable() () -> Unit) {  val targetColors = if (isSystemDark()){     DarkColorPalette  }else{    when (theme) {      BaseElementComposeTheme.Theme.Light -> LightColorPalette      BaseElementComposeTheme.Theme.Dark -> DarkColorPalette      BaseElementComposeTheme.Theme.NewYear -> NewYearColorPalette      BaseElementComposeTheme.Theme.Green -> GreenColorPalette    }  }  /**   * 动画渐变切换主题*/  val bottomBar = animateColorAsState(targetColors.bottomBar, TweenSpec(600))  val background = animateColorAsState(targetColors.background, TweenSpec(600))  val colors = BaseElementComposeColors(    bottomBar = bottomBar.value,    background = background.value,  )  CompositionLocalProvider(LocalWeComposeColors provides colors) {    MaterialTheme(      shapes = shapes,      content = content    )  }}

主题状态

通过ViewModel中创建一个有状态的变量存储当前主题,然后从SharedPreferences中读取当前主题

    /**     * 当前主题     * 默认白灰色主题*/    var theme = mutableStateOf(getTheme())    /**     * 从缓存里面读取主题*/    private fun getTheme():BaseElementComposeTheme.Theme{        return when(SPUtil.getInstance().GetData("Theme",-1)){            0-> BaseElementComposeTheme.Theme.Light            1-> BaseElementComposeTheme.Theme.Dark            2-> BaseElementComposeTheme.Theme.NewYear            3-> BaseElementComposeTheme.Theme.Green            else -> BaseElementComposeTheme.Theme.Light        }    }

切换主题

切换当前主题,同时改变缓存中的值以及ViewModel的值,并应用到系统中

      chatModel.theme.value = when(it){                    0-> {                        SPUtil.getInstance().PutData("Theme",0)                        BaseElementComposeTheme.Theme.Light                    }                    1-> {                        SPUtil.getInstance().PutData("Theme",1)                        BaseElementComposeTheme.Theme.Dark                    }                    2-> {                        SPUtil.getInstance().PutData("Theme",2)                        BaseElementComposeTheme.Theme.NewYear                    }                    3-> {                        SPUtil.getInstance().PutData("Theme",3)                        BaseElementComposeTheme.Theme.Green                    }                    else -> {                        SPUtil.getInstance().PutData("Theme",0)                        BaseElementComposeTheme.Theme.Light                    }                }

应用

然后在Activity应用主题即可

     setContent{            BaseElementComposeTheme(viewModel.theme.value) {            //...            }        }

沉浸式状态栏

依赖

    implementation "com.google.accompanist:accompanist-insets:0.15.0"    implementation "com.google.accompanist:accompanist-insets-ui:0.15.0"    implementation "com.google.accompanist:accompanist-systemuicontroller:0.15.0"

让屏幕内容延伸到状态栏

 WindowCompat.setDecorFitsSystemWindows(window,false)

使用ProvideWindowInsets包裹Activity根布局,然后通过remember创建当前系统栏状态的变量,下方只设置状态栏为隐藏

   setContent{            BaseElementComposeTheme(viewModel.theme.value) {                ProvideWindowInsets() {                    val systemUiController = rememberSystemUiController()                    SideEffect {                        systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)                    }                }            }        }

如果布局中使用了底部导航栏,使用如上会导致底部导航栏消失;在根布局应用如下代码即可,给一个间隔就行

Modifier.navigationBarsPadding()

Git链接

Git链接

https://gitee.com/FranzLiszt1847/fake-we-chat