春光不自留,莫怪东风恶
Compose for Desktop
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。我下载的最新版本,先试用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
三、Desktop UI编写 - 微信-Left
Compose for Desktop简化并加速了桌面应用程序的UI开发,并允许Android和桌面应用程序之间大量的UI代码共享既然官方如此说了和Android端的UI大量共享,我们接下来体验一下。当然了我感受了一波的却大量的组件都基本一致。就自定义方面缺少一些API,阴影的设置,如果你发现了可以告诉一下我,感激不尽。既然和Android一致那么接下来大量的代码,接住了..
上面分析:1.左侧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
}
}
看看效果?
四、Desktop UI编写 - 微信-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))
五、Desktop UI编写 - 微信-Right
最后我们来完成Right UI、这部分如下图:
1、顶部Row
2、聊天列表部分
3、输入框部分
1、顶部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,
)
)
}
}
2、聊天列表部分
聊天部分其实也很简单,只要咋们分析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搞定这里由于时间问题设置圆角即可。
前四章
Jetpack-Compose基本布局
JetPack-Compose - 自定义绘制
JetPack-Compose - Flutter 动态UI?
JetPack-Compose UI终结篇
JetPack-Compose 水墨画效果
@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,
)
)
}
}
3、输入框部分
这部分最简单来有没有?上代码
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 = {
}
)
)
}
六、总结
构建更好的桌面应用程序 composefordesktop提供了一种用Kotlin创建用户界面的声明式UI。结合可组合的功能来构建用户界面,并享受IDE和构建系统提供的完整工具支持—不需要XML或模板语言,写了几个小时的博客着实体会到了Compose在UI方面的能力和方便,几个小时基本搞定UI以及部分交互逻辑,我仔细算来其中所有的图标都是我经过PS处理、博客排版、微信滑水等时间除去,写代码部分时间比实际要少得多,所以Compose值得期待,我也坚信声明式UI才是未来。