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

简述

此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未选中和被选中两种状态颜色,使用主题颜色填充,方便后面切换主题的时候,发生相应变化

/**
 * 底部导航条*/
@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)
            })
    }
}

聊天

聊天数据全为静态数据,通过ViewModelmutableStateOf创建一个有状态的数据进行存放,然后当当聊天框发送信息后,获取此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)
            })

    }
}

功能区

NavHostControllernavigate用于导航路由,传入的参数为目的地页面定义时的昵称,类型为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

猜你喜欢

转载自blog.csdn.net/News53231323/article/details/128509048