查看原文
其他

什么?Compose可以开发PC应用了?

路很长OoO 郭霖 2022-12-14


/   今日科技快讯   /

近日,2021上海国际车展媒体日首日,特斯拉展台出现疑似车主维权事件。在特斯拉展台,一位身穿“刹车失灵”字样T恤的女士站在一辆特斯拉展车车顶,高声呼喊:“刹车失灵。”因行为过激,这位女士被保安带走。

/   作者简介   /

本篇文章来自路很长OoO同学的投稿,和大家分享了如何用Compose开发PC应用的相关内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

路很长OoO的博客地址:
https://juejin.cn/user/4019470242152616

/   前言   /
     
Compose是由Kotlin语言快速编辑界面的框架,基于谷歌的现代工具箱,由JetBrains为您带来。Compose for Desktop简化并加速了桌面应用程序的UI开发,并允许Android和桌面应用程序之间大量的UI代码共享,这是来自官方的一些阐述解释。

Compose初忠是声明式UI,当然了跨平台的纷争乱战时代,它也有着跨平台的梦想。在桌面端还未流行和普及之时为何不用划水的时间来尝试一下Compose for Desktop!

/   环境   /

IntelliJ IDEA 2020.3以后的版本根据新建的类型compose desktop、compose app、compose web都可以自动依赖相关的gradle。我下载的最新版本,pojie有点辣手,之前的注册码和.jar方式都可以,如果由大佬知道破解方式可以楼下指点,先试用30天。截图看文字,相信大佬们都能看懂?左边选择Kotlin,右边会出现各种kotlin能干的事:Desktop、Web、Mobile....kotlin牛逼!



/   Desktop   /


Name、Location、Project Template->Desktopo即可、Build System 随便、Project JDK->11以上即可,继续finish完成。

你可以点击一下下图的main函数前面的绿色运行箭头.....等待奇迹出现。


运行的效果,当然了这是我没事干用贝塞尔曲线绘制的那个男人,时间问题没绘制完。大家如果看过我的自定义相信绘制不是难题!!


上面我们开发环境已经完毕,接下来是不是有点小激动,我们开始代码编写。

/   Desktop UI分析   /

微信的桌面端说不上花里胡哨,但是很优雅简约不缺美观。我们这篇文字主要模仿这个UI进行尝试Compose for Desktop。


素材准备

为了达到比较一致的效果,我们通过PS进行素材获取。

1.打开微信截图需要图标。

2.PS截图用魔术棒进行选区删除不需要部分。


3.通过选区缩放来进行调试边界。


保存图片即可,逐步操作需要图片。

布局分析

布局我们经常用,也知道可分为这三块从左到右都有联动。所以我们先进行一级布局UI。


  1. 左侧Colum又上到下配合Spacer完美
  2. 中间的Box内部ListView加搜索框
  3. 右侧ListView

Left

Compose for Desktop简化并加速了桌面应用程序的UI开发,并允许Android和桌面应用程序之间大量的UI代码共享既然官方如此说了和Android端的UI大量共享,我们接下来体验一下。当然了我感受了一波的却大量的组件都基本一致。就自定义方面缺少一些API,阴影的设置,如果你发现了可以告诉一下我,感激不尽。既然和Android一致那么接下来大量的代码,接住了..


上面分析:

左侧Colum又上到下配合Spacer完美。

实体类封装点击图片路径

/**
 * @param defaultPath 默认图片路径
 * @param selectedPath 选择路径
 * @param path 实际路径
 * @param selected 是否选中
 */
data class WxSelectedBean(val defaultPath:String,var selectedPath:String,var path:String,var selected:Boolean)

负值图片路径

object WxViewModel : RememberObserver {
    val isAppReady = mutableStateOf(false)
    val position = ArrayList<WxSelectedBean>()
    fun initData() {
        var selectedDatas = arrayListOf<WxSelectedBean>()
        selectedDatas.add(
            WxSelectedBean(
                "images/head_lhc.png",
                "images/head_lhc.png",
                "images/head_lhc.png",
                 false
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                "images/message_unselected.png",
                "images/message_selected.png",
                "images/message_selected.png",
                 true
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                "images/person_unselected.png",
                "images/person_selected.png",
                "images/person_unselected.png",
                false
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                 "images/connected_unselecte.png",
                "images/connected_selected.png",
                "images/connected_unselecte.png",
                 false
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                "images/file_default.png",
                "images/file_default.png",
                "images/file_default.png",
                false
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                "images/frends.png",
                "images/frends.png",
                "images/frends.png",
                false
            )
        )

        selectedDatas.add(
            WxSelectedBean(
                "images/phone.png",
                "images/phone.png",
                "images/phone.png",
                false
            )
        )
        selectedDatas.add(
            WxSelectedBean(
                "images/mulu.png",
                "images/mulu.png",
                "images/mulu.png",
                false
            )
        )
        position.addAll(selectedDatas)
    }


    override fun onAbandoned() {

    }

    override fun onForgotten() {
    }

    override fun onRemembered() {
    }
}

界面

import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import module_view.WxSelectedBean
import module_view.WxViewModel

fun main() = Window {
    WxViewModel.initData()
    var wxData by remember { mutableStateOf(WxViewModel.position) }
    //选中的索引
    var selectedIndex by remember { mutableStateOf(1) }
    //图片选中动画执行与否
    var imageAnimal by remember { mutableStateOf(true) }
    //图片旋转动画
    val imageAngle: Float by animateFloatAsState(
        if (imageAnimal) {
            0f
        } else {
            360f
        }, animationSpec = TweenSpec(durationMillis = 1001)
    )
    MaterialTheme {
        Scaffold {
            Row {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier.fillMaxHeight().width(66.dp)
                        .background(Color(247, 242, 243))
                ) {
                    ImageRes(
                        getPath(wxData, selectedIndex, 0),
                        modifier = Modifier.padding(top = 30.dp).size(48.dp)
                            .clickable(role = Role.Image) {
                            imageAnimal = !imageAnimal
                        }.rotate(imageAngle)
                    )

                    ImageRes(
                        getPath(wxData, selectedIndex, 1),
                        modifier = Modifier.padding(vertical = 20.dp).size(42.dp).clickable {
                            selectedIndex = 1
                        })
                    ImageRes(getPath(wxData, selectedIndex, 2),
                        modifier = Modifier.size(32.dp).clickable {
                            selectedIndex = 2
                        })
                    ImageRes(
                        getPath(wxData, selectedIndex, 3),
                        modifier = Modifier.padding(vertical = 20.dp).size(30.dp).clickable {
                            selectedIndex = 3
                        }
                    )
                    ImageRes(getPath(wxData, selectedIndex, 4), modifier = Modifier.size(30.dp))
                    ImageRes(
                        getPath(wxData, selectedIndex, 5),
                        modifier = Modifier.padding(vertical = 20.dp).size(30.dp)
                    )
                    Spacer(modifier = Modifier.weight(1f))
                    ImageRes(
                        getPath(wxData, selectedIndex, 6),
                        modifier = Modifier.padding(vertical = 20.dp).size(35.dp)
                    )
                    ImageRes(
                        getPath(wxData, selectedIndex, 7),
                        modifier = Modifier.padding(vertical = 20.dp).size(30.dp)
                    )
                }
            }
        }

    }

}


/**
 * @param wxData 数据集合
 * @param selectedIndex 选中的索引
 * @param currenIndex 当前Image对应的索引
 * return 返回各个按钮选中和未选中图片路径
 */
private fun getPath(
    wxData: ArrayList<WxSelectedBean>,
    selectedIndex: Int,
    currenIndex: Int
): String {
    return if (selectedIndex == currenIndex) {
        wxData[currenIndex].selectedPath
    } else {
        wxData[currenIndex].defaultPath
    }

}

看看效果?


Center

中间部分如下图分析可见Box里面一个Row一个列表搞定?对于UI代码编写之前,大概的代码框架构思还是比较重要的。代码结构大概的有所构思对于后面的思路很有帮助。

Box(){
  LazyColunm()
  Row{
     TextFile()
     Box{
        Image() 
     }
  }
}


/**
 * 分钟微信中间界面
 */
@Composable
fun centerView() {
    var inputValue by remember { mutableStateOf("搜索") }
    Box() {
        Column(
            modifier = Modifier
                .width(320.dp)
                .background(Color.Red)
                .verticalScroll(rememberScrollState())
        ) {
            列表内容
        }
        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.width(320.dp).background(Color.White).padding(8.dp)) {
            TextField(
                value = inputValue,
                onValueChange = {
                    inputValue = it
                },
                colors = TextFieldDefaults.textFieldColors(
                    unfocusedIndicatorColor = Color.Transparent,
                    focusedIndicatorColor = Color.Transparent,
                    backgroundColor = Color.Transparent
                ),
                modifier = Modifier.padding(8.dp).background(
                    color = Color(247, 242, 243), shape = RoundedCornerShape(20),
                ).height(26.dp).width(250.dp),
                leadingIcon = {
                    Icon(
                        bitmap = getImageBitmap("images/sousuo.png"),
                        "",
                        modifier = Modifier.size(10.dp)
                    )
                },
            )
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier.size(26.dp).background(
                    color = Color(247, 242, 243),
                    shape = RoundedCornerShape(10)
                ).clip(shape = RoundedCornerShape(10))
            ) {
                ImageRes(
                    "images/jia.png",
                    modifier = Modifier.size(18.dp)
                )
            }
        }

    }
}


滑动列表Item的编写

下面是列表Item的样式,那我们来进行基本Item代码结构样式的明确。

Row{
  Image()
  Column{
     Row{
       Text("主管老婆大人")
       Text("06:42")
     }
     Text("[文件]20202323002030320302.png")
  }

}


 LazyColumn(
            state = scrollLazyState,
            modifier = Modifier
                .width(300.dp)
                .padding(top = 70.dp)
        ) {
            items(100) { index ->
                Row (Modifier.background(selectedColor(selectedIndex,index)).padding(top=10.dp,start = 15.dp,bottom = 10.dp,end = 15.dp).clickable {
                    selectedIndex = index
                }){
                    Image(bitmap = getImageBitmap("images/head_lhc.png"),"",modifier = Modifier.width(45.dp))
                    Column(verticalArrangement=Arrangement.SpaceBetween,horizontalAlignment = Alignment.Start,modifier = Modifier.width(300.dp).padding(start = 10.dp)){
                        Row(horizontalArrangement = Arrangement.SpaceBetween,modifier = Modifier.width(300.dp)) {
                            Text("主管老婆大人",fontSize = 14.sp)
                            Text("06:42",fontSize = 11.sp,color = Color(111,111,111))
                        }
                        Spacer(Modifier.height(6.dp))
                        Text("[文件]202023230ll.png",fontSize = 12.sp,color = Color(111,111,111))
                    }

                }
            }

        }

这里点击事件如果用clickable就会出现点击水波纹但是微信没有这个水波纹,所以我们不能用clickable来进行点击事件的扑捉,我们用手势检测器来代替。

.pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            selectedIndex = index
                        }
                    )

                }

完善数据,以假乱真

头像部分裁剪+PS-魔术棒+反选+delete+保存即可。

  //中间部分数据造假
        wxDatas.add(WxListBean("主管老婆大人","images/item_a.png","[文件]20211999lll.pdf","6:45",0))
        wxDatas.add(WxListBean("CSDN付费专栏作者交流群","images/item_b.png","杨修张:如果博客设置权限,是不是每个人都看不到了...","7:45",1))
        wxDatas.add(WxListBean("CSDN社区专家","images/item_c.png","不是每个人都可以坐吃享受天下美食...","7:45",2))
        wxDatas.add(WxListBean("郭比蓝","images/item_d.png","撸啊撸","7:45",3))
        wxDatas.add(WxListBean("公众号","images/item_e.png","郭霖:Compose UI 带来的精彩...","10:45",4))
        wxDatas.add(WxListBean("Flutter交流群","images/items_h.png","java Dart Kotlin js ...","10:35",5))
        wxDatas.add(WxListBean("小江","images/items_u.png","clickable点击水波纹能去掉不?","7:45",5))
        wxDatas.add(WxListBean("lemone","images/items_g.png","我来了带他过来,如果他来了就可以面试了","13:45",5))
        wxDatas.add(WxListBean("窒息","images/item_c.png","撸啊撸","7:45",3))
        wxDatas.add(WxListBean("公众健康","images/item_b.png","郭霖:Compose UI 带来的精彩...","17:45",4))

Right

最后我们来完成Right UI。这部分如下图:

  1. 顶部Row
  2. 聊天列表部分
  3. 输入框部分


顶部Row


我们代码结构如下:

Row{
   Text("主管老婆大人")
   Image(bitmap)
}

代码部分:

@Composable
fun RightView() {
    Column {
        Row(
            modifier = Modifier.height(55.dp).fillMaxWidth().background(Color(243, 243, 243)).padding(15.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("主管老婆大人")
            Image(bitmap = getImageBitmap("images/gengduo.png"), "")
        }

        Spacer(Modifier.weight(1f))
        TextField(
            value = "hello", onValueChange = {

            }, modifier = Modifier.height(226.dp).fillMaxWidth(),
            colors = TextFieldDefaults.textFieldColors(
                cursorColor = Color.Gray,
                backgroundColor = Color(243, 243, 243),
                unfocusedIndicatorColor = Color.Transparent,
                focusedIndicatorColor = Color.Transparent,
            )
        )
    }

}


聊天列表部分

聊天部分其实也很简单,只要咋们分析UI结构和数据即可:如下图。

列表分为左右消息,有图片、有视频、有文字。所以我们定义数据时候需要消息数据类型和不同人的userId、以及头像即可。

/**
 * @param userID 用户ID
 * @param headPath 头像
 * @param message 消息
 * @param messageImg 消息图片
 * @param messageType 消息类型
 * 
 */
data class WxMessageBean(
    val userID: String,
    var headPath: String,
    var message: String,
    var messageImg: String,
    var messageType: MessageType
)

  //聊天详情内容
        wxMessages.add(WxMessageBean("002","images/item_a.png","有美女照片没有?","images/mn_1.png",MessageType.MESSAGE))
        wxMessages.add(WxMessageBean("001","images/item_d.png","","images/mn_1.png",MessageType.IMAGE))
        wxMessages.add(WxMessageBean("001","images/item_d.png","漂亮不?还有...","images/mn_1.png",MessageType.MESSAGE))
        wxMessages.add(WxMessageBean("001","images/item_d.png","","images/mn_2.png",MessageType.IMAGE))
        wxMessages.add(WxMessageBean("002","images/item_a.png","有没有健身的妹纸呀?这些美女照片太多了没意思...要刚柔并进。你的明白吧?","images/mn_1.png",MessageType.MESSAGE))
        wxMessages.add(WxMessageBean("001","images/item_d.png","安心学技术多好,看啥美女对不?","images/mn_2.png",MessageType.MESSAGE))
        wxMessages.add(WxMessageBean("001","images/item_d.png","Compose最近看了一眼,也能跨平台呢?","images/mn_2.png",MessageType.MESSAGE))
        wxMessages.add(WxMessageBean("002","images/item_a.png","是的没错! 但是我觉得Flutter目前更胜一筹在Web端方面","images/mn_1.png",MessageType.MESSAGE))

布局:

我相信对于大家都很简单吧。如果没想法可以看看我之前的四篇博客。整体的聊天可以分为左右信息。也就是需要两套信息根据是否本人来显示位置。第二无非头像和消息的位置、第三对于消息小尖头等简单的clip搞定这里由于时间问题设置圆角即可。

@Composable
fun RightView() {
    var inputText by remember { mutableStateOf("") }
    Column {
        Row(
            modifier = Modifier.height(55.dp).fillMaxWidth().background(Color(247, 242, 243, 100))
                .padding(15.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("郭b蓝")
            Image(bitmap = getImageBitmap("images/gengduo.png"), "")
        }
        Spacer(Modifier.height(1.dp).fillMaxWidth().background(Color(222, 222, 222)))
        LazyColumn(Modifier.weight(1f).fillMaxWidth().background(Color(247, 242, 243, 100))) {
            items(WxViewModel.wxMessages.size) { index ->
                val wxmessage = WxViewModel.wxMessages[index]
                if (wxmessage.userID == "001") {
                    Box {
                        Row(Modifier.padding(10.dp)) {
                            Image(
                                bitmap = getImageBitmap(wxmessage.headPath),
                                "",
                                modifier = Modifier.size(45.dp),
                                contentScale = ContentScale.FillWidth
                            )
                            if (wxmessage.messageType == MessageType.MESSAGE) {
                                Text(
                                    text = wxmessage.message,
                                    fontSize = 13.sp,
                                    modifier = Modifier.background(
                                        color = Color.White,
                                        shape = RoundedCornerShape(20)
                                    ).clip(shape = RoundedCornerShape(20)).padding(10.dp)
                                )
                            } else {
                                Image(
                                    bitmap = getImageBitmap(wxmessage.messageImg),
                                    "",
                                    modifier = Modifier.size(80.dp)
                                )
                            }
                        }
                    }

                } else {
                    Row(
                        modifier = Modifier.fillMaxWidth().padding(15.dp),
                        horizontalArrangement = Arrangement.End
                    ) {
                        Row {
                            if (wxmessage.messageType == MessageType.MESSAGE) {
                                Text(
                                    text = wxmessage.message,
                                    fontSize = 13.sp,
                                    modifier = Modifier.width(250.dp).background(
                                        color = Color.White,
                                        shape = RoundedCornerShape(20)
                                    ).clip(shape = RoundedCornerShape(20)).padding(10.dp)
                                )
                            } else {
                                Image(
                                    bitmap = getImageBitmap(wxmessage.messageImg),
                                    "",
                                    modifier = Modifier.size(80.dp)
                                )

                            }
                            Image(
                                bitmap = getImageBitmap(wxmessage.headPath),
                                "",
                                modifier = Modifier.padding(start= 10.dp).size(40.dp),
                                contentScale = ContentScale.FillBounds

                            )

                        }

                    }

                }
            }
        }
        Spacer(Modifier.height(1.dp).fillMaxWidth().background(Color(222, 222, 222)))
        TextField(
            value = inputText, onValueChange = {
                inputText = it
            }, modifier = Modifier.height(226.dp).fillMaxWidth(),
            colors = TextFieldDefaults.textFieldColors(
                cursorColor = Color.Gray,
                backgroundColor = Color(247, 242, 243, 100),
                unfocusedIndicatorColor = Color.Transparent,
                focusedIndicatorColor = Color.Transparent,
            )
        )
    }

}


输入框部分

这部分最简单来有没有?上代码:

Column {
            Row(
                modifier = Modifier.background(Color(247, 242, 243, 100)).padding(start = 15.dp,end = 15.dp,top=15.dp)
                    .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Image(
                        bitmap = getImageBitmap("images/wx_face.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp),
                    )
                    Image(
                        bitmap = getImageBitmap("images/wx_file.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp),
                    )
                    Image(
                        bitmap = getImageBitmap("images/wx_jd.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp),
                    )
                    Image(
                        bitmap = getImageBitmap("images/wx_msg.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp).clickable {
                            WxViewModel.wxMessages.add(WxMessageBean("002","images/item_a.png",inputText,"images/mn_2.png",MessageType.MESSAGE))
                            send=!send
                            GlobalScope.launch{
                                state.animateScrollTo(yPosition)
                            }

                        }
                    )
                }
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Image(
                        bitmap = getImageBitmap("images/wx_phone.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp)
                    )
                    Image(
                        bitmap = getImageBitmap("images/wx_sp.png"),
                        "",
                        modifier = Modifier.padding(horizontal = 5.dp)
                    )
                }


            }
            TextField(
                value = inputText, onValueChange = {
                    inputText = it
                }, modifier = Modifier.height(226.dp).fillMaxWidth(),
                colors = TextFieldDefaults.textFieldColors(
                    cursorColor = Color.Gray,
                    backgroundColor = Color(247, 242, 243, 100),
                    unfocusedIndicatorColor = Color.Transparent,
                    focusedIndicatorColor = Color.Transparent,
                ),
                keyboardActions = KeyboardActions(
                    onDone = {

                    }
                )

            )
        }

/   总结   /

构建更好的桌面应用程序compose for desktop提供了一种用Kotlin创建用户界面的声明式UI。结合可组合的功能来构建用户界面,并享受IDE和构建系统提供的完整工具支持—不需要XML或模板语言,写了几个小时的博客着实体会到了Compose在UI方面的能力和方便,几个小时基本搞定UI以及部分交互逻辑,我仔细算来其中所有的图标都是我经过PS处理、博客排版、微信滑水等时间除去,写代码部分时间比实际要少得多,所以Compose值得期待,我也坚信声明式UI才是未来。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
一起来看看Android官推Kotlin-First的图片加载库
Git常用指令,你还记得多少?

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存