一个简单的微信界面
- 简述
- 效果视频
- 底部导航栏
- 导航元素
- 导航栏
- 放入插槽
- 绘制地图
- 消息列表
- 效果图
- 实现
- 聊天
- 效果图
- 实现
- 气泡背景
- 联系人界面
- 效果图
- 实现
- 好友详情
- 效果图
- 实现
- 发现
- 效果图
- 实现
- 未读红点
- 未读条数
- 朋友圈
- 效果图
- 实现
- 上拉加载
- 个人设置
- 效果图
- 实现
- 个人信息
- 功能区
- 钱包
- 效果图
- 实现
- 切换主题
- 效果图
- 实现
- 一键切换主题
- 建立颜色实体类
- 创建主题样式
- 主题
- 主题状态
- 切换主题
- 应用
- 沉浸式状态栏
- 依赖
- 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未选中和被选中两种状态颜色,使用主题颜色填充,方便后面切换主题的时候,发生相应变化
/**
* 底部导航条*/
@Composable
fun 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")
@Composable
fun 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,用于定义指向可组合项的路径。您可以将其视为指向特定目的地的隐式深层链接。每个目的地都应该有一条唯一的路线。*/
@Composable
fun 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属性即可
@Composable
fun 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
组件是分界线,例如两个组件之间需要一条直线进行分割,即可使用
@Composable
fun 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*/
@Composable
fun 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
;
/**
* 聊天界面*/
@Composable
fun 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数据中发送消息的人是否为"自己"
而进行左右排放
/**
* 聊天记录*/
@Composable
fun 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
@Composable
fun 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可插入不同的子组件,整体呈垂直排列
@Composable
fun 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
值即可
/**
* 联系人详情*/
@Composable
fun 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()
}
}
}
}
发现
效果图
实现
@Composable
fun FindingPageView(chatController: NavHostController){
Column(
Modifier
.background(BaseElementComposeTheme.colors.background)
.fillMaxSize()
) {
TitleBar("发现",-1,Modifier.padding(10.dp)){}
Spacer(modifier = Modifier.height(10.dp))
FindingList(chatController)
}
}
@Composable
fun 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
的属性完成构建
@Composable
fun 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有显著差距
@Composable
fun 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
类型
@Composable
fun 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
@Composable
fun 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)
定义列数
@Composable
fun 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
进行布局构建
@Composable
fun 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
}
}
}
}
}
}
@Composable
fun 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)}
) {
}
}
}
}
一键切换主题
建立颜色实体类
首先建立一个颜色实体类,包括你所要使用的所有颜色,以下为例
@Stable
class 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
}
}
主题
判断当前系统主题是否为暗黑主题
@Composable
fun isSystemDark():Boolean = isSystemInDarkTheme()
对主题内所有颜色进行切换,此处没有对形状、排版等主题进行切换
@Composable
fun 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