前言
之前写过一篇关于元服务项目的上架流程,为了更好的了解及开发元服务,准备从0到1简单开发一个小项目,也希望能够帮助到刚刚介入到鸿蒙开发的同学,具体项目呢,也是十分的简单,就是一个小巧的备忘录项目,可以编辑内容,可以展示已经编辑好的内容列表,开发上很快,一般半天到一天就可以搞定。
之所以选择这样的一个项目,最大的原因是不需要联网操作,数据都是本地的存储,方便个人开发者进行开发和后续的上架。
对了,目前随心记元服务项目已经上架,大家可以在应用商店搜索随心记即可体验。
我们先看下最终要实现的效果:
主页,无数据状态
主页,有数据状态
编辑页面
由于是项目开发前的开篇第一篇,本篇文章会带着大家把基本的UI进行绘制了。
首页UI
首页UI非常的简单,从上到下的结构依次是,标题栏,搜索框,备忘录列表;页面UI排版中,我们可以选择Column组件作为根布局,然后从上到下依次排开,因为有编辑按钮,还有空数据时的缺省页面,这里建议直接使用RelativeContainer组件,当然了,这并不是唯一的布局方式。
标题栏,大家可以使用Text组件设置,搜索使用Search组件,列表的话,直接使用List组件即可,至于缺省组件,需要根据是否有备忘录数据来动态的展示。
完整的代码如下:
RelativeContainer() {
Column() {
ActionBar({ title: "随心记" })
Search({ placeholder: "搜索……" })
.margin({ left: 10, right: 10, top: 10 })
List({ space: 10 }) {
ForEach(this.mListContentBean, (item: ListContentBean, index: number) => {
ListItem() {
Column() {
Text(item.time)
.width("100%")
.textAlign(TextAlign.End)
.margin({ top: 5, right: 5 })
Text(item.title)
.fontWeight(FontWeight.Bold)
.width("100%")
Text(item.desc)
.width("100%")
.margin({ top: 10, bottom: 10 })
}
.width("100%")
.height(100)
.padding({
top: 5,
bottom: 5,
left: 10,
right: 10
})
.backgroundColor(item.bgColor == undefined ? "#e8e8e8" : item.bgColor)
.borderRadius(10)
}.swipeAction({
end: {
builder: () => {
this.swipeDelete(this, item.bgColor == undefined ? "#e8e8e8" : item.bgColor,
item.id?.toString(), index)
},
actionAreaDistance: 80,
}
})
.onClick(() => {
//点击条目,跳转
})
})
}
.width("100%")
.layoutWeight(1)
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
}
Text("+")
.width(60)
.height(60)
.backgroundColor("#FB553C")
.borderRadius(50)
.fontColor(Color.White)
.fontSize(45)
.textAlign(TextAlign.Center)
.margin({ right: 20, bottom: 20 })
.alignRules({
bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
right: { anchor: "__container__", align: HorizontalAlign.End }
})
.onClick(() => {
//点击跳转编辑页面
})
//缺省提示
Text("暂时没有笔记,赶紧添加一条吧~")
.fontWeight(FontWeight.Bold)
.visibility(this.isContentEmpty ? Visibility.Visible : Visibility.None)
.alignRules({
center: { anchor: "__container__", align: VerticalAlign.Center },
middle: { anchor: "__container__", align: HorizontalAlign.Center }
})
}.height('100%')
.width('100%')
编辑页UI
编辑页面相对来说,稍稍复杂一些,除了内容编辑组件之外,增加了顶部的换肤,还有底部的一排样式设置,同样根布局也是使用的RelativeContainer组件。
绘制编辑页面UI,有两个需要注意的,一个是底部的一排样式按钮,需要根据软键盘的高度动态的设置位置,当然了本篇仅仅是UI绘制,我们后续的篇章在说。另一个就是样式列表,比如文本颜色,文本大小,皮肤列表等等,需要做动态的显示盒隐藏。
如何进行样式效果的动态显示呢?很简单,你可以把样式效果先写好,然后先隐藏,当点击的时候,进行显示即可,比如顶部的换肤,你可以先把换肤的UI写好。
Column() {
List() {
ForEach(this.skinColors, (item: string, index: number) => {
ListItem() {
Column() {
Image($r("app.media.complete"))
.width(20)
.height(20)
.visibility(this.clickSkinPosition == index ? Visibility.Visible : Visibility.None)
.border({ width: 1, color: "#666666", radius: 20 })
.margin({ bottom: 20 })
}
.width(100)
.height(160)
.backgroundColor(item)
.justifyContent(FlexAlign.End)
.borderRadius(5)
.margin({ left: 10 })
.onClick(() => {
this.clickSkinPosition = index
this.clickSkinColorValue = item
this.isClickSkin = !this.isClickSkin
})
}
})
}
.width("100%")
.height(180)
.backgroundColor("#e8e8e8")
.listDirection(Axis.Horizontal)
.padding({ top: 10 })
.scrollBar(BarState.Off)
}
.backgroundColor(Color.Transparent)
.width("100%")
.height("100%")
.onClick(() => {
this.isClickSkin = !this.isClickSkin
})
.visibility(this.isClickSkin ? Visibility.Visible : Visibility.None)
.alignRules({
top: { anchor: "bar", align: VerticalAlign.Bottom },
})
点击换肤按钮的时候,进行显示换肤列表。
//点击换肤 显示背景
this.isClickSkin = !this.isClickSkin
基本效果
编辑页面所有UI代码
RelativeContainer() {
ActionBar({
title: "编辑笔记",
leftIcon: $r("app.media.complete"),
left2Icon: $r("app.media.skin"),
leftMenuAttribute: {
imageWidth: 22,
imageHeight: 22
},
leftMenu2Attribute: {
imageWidth: 22,
imageHeight: 22,
imageMargin: { left: 20 }
},
onLeftImageClick: (position) => {
if (position == 0) {
//点击返回
router.back()
} else {
//点击换肤 显示背景
this.isClickSkin = !this.isClickSkin
}
}
}).id("bar")
Column() {
TextInput({ placeholder: "请输入笔记标题……", text: $$this.title })
.backgroundColor(Color.Transparent)
.placeholderFont({ weight: FontWeight.Bold, size: 15 })
.placeholderColor("#666666")
.fontSize(18)
.maxLength(50)
.fontColor("#222222")
.fontWeight(FontWeight.Bold)
.caretColor(Color.Red)//光标的颜色
.padding(10)
.borderRadius(0)
.margin({ top: 10 })
Text() {
Span(this.nowTime)
}
.width("100%")
.fontSize(13)
.fontColor("#666666")
.padding({ left: 10 })
.margin({ top: 10 })
RichEditor(this.options)
.onReady(() => {
//获取当前的时间
this.nowTime = this.getDateTime()
this.nowInterval = setInterval(() => {
this.nowTime = this.getDateTime()
}, 1000)
})
.placeholder("随心记,记录点点滴滴……", {
fontColor: "#666666"
})
.caretColor(Color.Red)
.padding(10)
.margin({ top: 10 })
.onSelect((value: RichEditorSelection) => {
this.start = value.selection[0];
this.end = value.selection[1];
})
}
.alignRules({
top: { anchor: "bar", align: VerticalAlign.Bottom },
bottom: { anchor: "bottom_bar", align: VerticalAlign.Top }
}).margin({ bottom: 80 })
Column() {
List({ space: 10 }) {
ForEach(this.fontColors, (item: ResourceColor) => {
ListItem() {
Text()
.width(20)
.height(20)
.backgroundColor(item)
.borderRadius(20)
.border({ width: 1, color: "#e8e8e8" })
.onClick(() => {
this.clickStyleColorValue = item
this.changeStyle()
this.setFontColor()
})
}
})
}
.width("100%")
.height(30)
.listDirection(Axis.Horizontal)
.padding({ left: 10, right: 10 })
.scrollBar(BarState.Off)
.visibility(this.isClickStyleColor ? Visibility.Visible : Visibility.None)
List({ space: 10 }) {
ForEach(this.fontSizes, (item: string, index: number) => {
ListItem() {
Text(item)
.height(20)
.borderRadius(20)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.onClick(() => {
let fontSize = 15
if (index == 0) {
fontSize = 21
} else if (index == 1) {
fontSize = 20
} else if (index == 2) {
fontSize = 19
} else if (index == 3) {
fontSize = 18
} else if (index == 4) {
fontSize = 17
} else if (index == 5) {
fontSize = 16
} else if (index == 6) {
fontSize = 15
} else {
fontSize = Number(item)
}
this.clickStyleSizeValue = fontSize
this.changeStyle()
//设置文字大小
this.setFontSize()
})
}
})
}
.width("100%")
.height(30)
.listDirection(Axis.Horizontal)
.padding({ left: 10, right: 10 })
.scrollBar(BarState.Off)
.visibility(this.isClickStyleSize ? Visibility.Visible : Visibility.None)
Row() {
Image($r("app.media.font_size"))
.onClick(() => {
this.isClickStyleSize = !this.isClickStyleSize
this.setBold()
})
.backgroundColor(this.isClickStyleSize ? "#e8e8e8" : Color.Transparent)
.width(20)
.height(20)
Text("B")
.onClick(() => {
this.isClickStyleB = !this.isClickStyleB
this.changeStyle()
this.setBold()
})
.fontWeight(FontWeight.Bold)
.fontSize(20)
.backgroundColor(this.isClickStyleB ? "#e8e8e8" : Color.Transparent)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.margin({ left: 20 })
Text("I")
.onClick(() => {
this.isClickStyleI = !this.isClickStyleI
this.changeStyle()
this.setStyle()
})
.fontWeight(FontWeight.Bold)
.fontStyle(FontStyle.Italic)
.backgroundColor(this.isClickStyleI ? "#e8e8e8" : Color.Transparent)
.fontSize(20)
.margin({ left: 20 })
.width(30)
.height(30)
.textAlign(TextAlign.Center)
Image($r("app.media.color_bg"))
.onClick(() => {
this.isClickStyleColor = !this.isClickStyleColor
})
.backgroundColor(this.isClickStyleColor ? "#e8e8e8" : Color.Transparent)
.margin({ left: 20 })
.width(20)
.height(20)
}
.width("100%")
.height(50)
.backgroundColor(this.clickSkinColorValue)
.border({ width: { top: 1 }, color: "#e8e8e8" })
.padding({ left: 20, right: 20 })
}.id("bottom_bar")
.alignRules({
bottom: { anchor: "__container__", align: VerticalAlign.Bottom }
})
//背景颜色
Column() {
List() {
ForEach(this.skinColors, (item: string, index: number) => {
ListItem() {
Column() {
Image($r("app.media.complete"))
.width(20)
.height(20)
.visibility(this.clickSkinPosition == index ? Visibility.Visible : Visibility.None)
.border({ width: 1, color: "#666666", radius: 20 })
.margin({ bottom: 20 })
}
.width(100)
.height(160)
.backgroundColor(item)
.justifyContent(FlexAlign.End)
.borderRadius(5)
.margin({ left: 10 })
.onClick(() => {
this.clickSkinPosition = index
this.clickSkinColorValue = item
this.isClickSkin = !this.isClickSkin
})
}
})
}
.width("100%")
.height(180)
.backgroundColor("#e8e8e8")
.listDirection(Axis.Horizontal)
.padding({ top: 10 })
.scrollBar(BarState.Off)
}
.backgroundColor(Color.Transparent)
.width("100%")
.height("100%")
.onClick(() => {
this.isClickSkin = !this.isClickSkin
})
.visibility(this.isClickSkin ? Visibility.Visible : Visibility.None)
.alignRules({
top: { anchor: "bar", align: VerticalAlign.Bottom },
})
}
.height('100%')
.width('100%')
.height(this.screenHeight) // 动态设置可视区域高度
.backgroundColor(this.clickSkinColorValue)
.expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
相关总结
UI页面绘制没什么好说的,就是组件的位置摆放,和组件的显示逻辑,有很多的属性并没有文章记录,大家可以去仓库中查看即可,文章中用到了我的一个标题栏组件,如果大家不想用,可以使用自己写的即可。
标题栏组件:
https://ohpm.openharmony.cn/#/cn/detail/@abner%2Fbar
文章系列
鸿蒙开发:如何上架一个元服务应用
项目地址:https://gitee.com/abnercode/harmony-easy-recording
备注:目前随心记元服务项目已经上架,大家可以在应用商店搜索随心记即可体验。