一个简单的微信界面
- 简述
- 效果视频
- 底部导航栏
- 导航元素
- 导航栏
- 放入插槽
- 绘制地图
- 消息列表
- 效果图
- 实现
- 聊天
- 效果图
- 实现
- 气泡背景
- 联系人界面
- 效果图
- 实现
- 好友详情
- 效果图
- 实现
- 发现
- 效果图
- 实现
- 未读红点
- 未读条数
- 朋友圈
- 效果图
- 实现
- 上拉加载
- 个人设置
- 效果图
- 实现
- 个人信息
- 功能区
- 钱包
- 效果图
- 实现
- 切换主题
- 效果图
- 实现
- 一键切换主题
- 建立颜色实体类
- 创建主题样式
- 主题
- 主题状态
- 切换主题
- 应用
- 沉浸式状态栏
- 依赖
- 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")}
导航栏
构建底部导航栏,其中NavController
是Compose
用来导航路由(页面切换),unselectedContentColor
和selectedContentColor
分别对应当此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) }) }}
聊天
聊天数据全为静态数据,通过ViewModel
中mutableStateOf
创建一个有状态的数据进行存放,然后当当聊天框发送信息后,获取此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) }) }}
功能区
NavHostController
的navigate
用于导航路由,传入的参数为目的地页面定义时的昵称,类型为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