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

news2024/11/16 9:17:37

一个简单的微信界面

  • 简述
  • 效果视频
  • 底部导航栏
    • 导航元素
    • 导航栏
    • 放入插槽
    • 绘制地图
  • 消息列表
    • 效果图
    • 实现
  • 聊天
    • 效果图
    • 实现
      • 气泡背景
  • 联系人界面
    • 效果图
    • 实现
  • 好友详情
    • 效果图
    • 实现
  • 发现
    • 效果图
    • 实现
      • 未读红点
      • 未读条数
  • 朋友圈
    • 效果图
    • 实现
      • 上拉加载
  • 个人设置
    • 效果图
    • 实现
      • 个人信息
      • 功能区
  • 钱包
    • 效果图
    • 实现
  • 切换主题
    • 效果图
    • 实现
  • 一键切换主题
    • 建立颜色实体类
    • 创建主题样式
    • 主题
    • 主题状态
    • 切换主题
    • 应用
  • 沉浸式状态栏
    • 依赖
  • 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未选中和被选中两种状态颜色,使用主题颜色填充,方便后面切换主题的时候,发生相应变化

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/134318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue】项目搭建规范

1. 集成editorconfig配置 EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。 VSCode需要安装一个插件&#xff1a;EditorConfig for VS Code 创建 .editorconfig 文件&#xff1a; # http://editorconfig.orgroot true[*] # 表示所有文…

js Proxy 的使用

文章目录一、什么是Proxy二、语法三、Proxy 方法1、get() 方法2、set() 方法3、apply() 方法4、has() 方法5、construct() 方法6、deleteProperty() 方法一、什么是Proxy Proxy 可以理解成&#xff0c;在目标对象之前架设一层“拦截”&#xff0c;外界对该对象的访问&#xff…

[Vulnhub] DC-2

Vlunhub下DC系列靶机第二台&#xff0c;难度与DC-1 差不多&#xff0c;为简单。共有五个Flag 下载地址&#xff1a;Vulnhub:DC-2 目录 信息搜集 cewl爬行网站字典&hydra爆破wordpress用户密码 -rbash逃逸 git提权 信息搜集 nmap -sP 192.168.236.0/24 扫描一下靶机i…

c++语法欠缺地方(持续更新)

sizeof是用来计算变量占多大内存的&#xff0c;单位是字节&#xff08;byte&#xff09;&#xff1b;sizeof 后面跟类型时&#xff0c;必须加上括号&#xff0c;例如sizeof(double);后面跟变量可以不用加括号&#xff0c;例如&#xff1a;sizeof d %d是以十进制形式输出有符号…

hadoop之kerberos权限配置(ranger基础上)(三)

文章目录一、kerberos服务端二、kerberos客户端三、hadoop集群安装HTTPS服务四、kerberos整合zk五、kerberos整合ranger六、kerberos整合hdfs七、kerberos整合yarn八、kerberos整合hive九、kerberos整合hbase十、遇到的问题一、kerberos服务端 上传kerberos安装包到/opt/rpm 安…

数据库,计算机网络、操作系统刷题笔记22

数据库&#xff0c;计算机网络、操作系统刷题笔记22 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle…

最值得推荐的3个免费PDF 转换器

当您需要将 PDF 转换为另一种格式&#xff08;如 Microsoft Word、图像&#xff08;如 JPG&#xff09;、Excel、电子书、PowerPoint 等&#xff0c;反之亦然&#xff09;时&#xff0c;最好的 PDF 转换器非常重要。 但是找到一个可靠的 PDF 转换软件来使用是具有挑战性的。因…

JAVA设计模式--行为型模式--策略模式

1.策略模式&#xff08;Strategy Pattern&#xff09; 1.1介绍 一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。 在策略模式中&#xff0c;我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对…

【C++】网络编程(TCPUDP)

网络编程是C API操作中很重要的一部分&#xff0c;包含TCP和UDP。 网络传输模型可以抽象为7个层&#xff1a;物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。 但在使用TCP/IP协议时&#xff0c;可以简化为这4层&#xff1a;网络接口、网络层、传输层、应用层。…

物理层基本概念

目录物理层的基本概念物理层传输方式串行传输并行传输同步传输异步传输单向通信&#xff08;单工&#xff09;双向交替通信&#xff08;半双工&#xff09;双向同时通信&#xff08;全双工&#xff09;编码与调制常用编码基本调制方法信道极限容量物理层的基本概念 物理层考虑的…

路由 OSPF常见4种网络类型MA、P2P、NBMA、P2MP、OSPF报头字段信息简介。

4.2.1 路由 OSPF&#xff08;OSPF常见4种网络类型、OSPF报头信息&#xff09; 目录OSPF常见的4种网络类型广播类型&#xff08;Broadcast 或 MA&#xff09;P2PNBMAP2MPOSPF报文发送形式对于不同OSPF网络类型的组网OSPF报头信息实际抓包分析OSPF常见的4种网络类型 OSPF应用于不…

Kali最强渗透工具- metasploit

数据来源 本文仅用于信息安全学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 metasploit是什么&#xff1f; msf是一款开源安全漏洞利用和测试工具&#xff0c;集成了…

C++GUI之wxWidgets(9)-编写应用涉及的类和方法(4)-事件处理(3)

目录动态事件处理如何处理事件事件如何向上传播事件处理程序链动态事件处理 void MyFrameHandler::OnFrameExit(wxCommandEvent&) {// Do something useful. }MyFrameHandler myFrameHandler;MyFrame::MyFrame() {Bind(wxEVT_MENU, &MyFrameHandler::OnFrameExit,&…

Java数组的定义与使用

Java数组的定义与使用 文章目录Java数组的定义与使用数组的基本概念什么是数组数组的创建数组的初始化数组的使用数组中元素访问遍历数组数组是引用类型初始JVM的内存分布基本类型变量与引用类型变量的区别引用变量几道例题认识null数组的应用场景保存数据作为函数的参数参数传…

TypeScript中的泛型

泛型&#xff08;Generics&#xff09;是指在定义函数、接口或类的时候&#xff0c;不预先指定具体的类型&#xff0c;而在使用的时候再指定类型的一种特性。 通常用T来指代任意输入的类型&#xff0c;除了T之外&#xff0c;以下是常见泛型变量代表的意思&#xff1a; K(Key…

信息安全技术 可信计算规范 可信平台控制模块 学习笔记(一)

声明 本文是学习信息安全技术 可信计算规范 可信平台控制模块. 下载地址而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 可信计算规范 缩略语 下列缩略语适用于本文件。 I/O&#xff1a;输入输出&#xff08;Input/Output&#xff09; IP&#xff1…

第三十八章 贪心算法——区间问题及证明(上)

第三十八章 贪心策略——区间相关问题一、什么贪心策略&#xff1f;二、区间问题合集1、思路&#xff1a;2、问题1&#xff1a; 区间选点&#xff08;1&#xff09;问题&#xff08;2&#xff09;思路和证明a.思路b.证明&#xff08;3&#xff09;代码3、问题2&#xff1a;&…

java:文件分片上传

代码下载地址&#xff1a;https://download.csdn.net/download/u013938578/87358484 1 文件上传、断点续传服务端 1.1 新建maven项目 文件结构如下&#xff1a; 1.2 引入百度开源上传组件webuploader 1.3 前端页面upload.html <!DOCTYPE html> <html lang"en&…

Java个人家乡博客源码

概述 个人博客相册家乡主题&#xff0c;用户注册后可以发布关于家乡的特色文章介绍&#xff0c;可以发布照片&#xff0c;相册管理&#xff0c;留言&#xff0c;评论&#xff0c;回复&#xff0c;收藏&#xff0c;关注 演示视频 https://www.bilibili.com/video/BV1iy4y1x7w6…

SSM框架-注解开发

11 注解开发 11.1 注解开发定义Bean 代码 接口BookDao public interface BookDao {void save(); }实现类BookDaoImpl【更改之处】 在最上面Component(“bookDao”) Component("bookDao") //或者Repository("bookDao") public class BookDaoImpl impl…