#DAYU200体验官# 生鲜配送App 原创 精华
@toc
主题
本帖使用Dayu200为开发板,展示一个在线生鲜配送的App首页。
注意:本文不涉及App上的用户互动,仅为页面设计效果的实现。
设计效果图
首页上半部分:
首页下半部分:
Dayu200的预览配置
为了大幅提高UI的开发效率,降低Dayu200的使用门槛,在开发过程中,强烈建议使用DevEco Studio 3.0 Beta3(OpenHarmony)的MatePadPro作为预览配置,并调整到横屏模式,最终与Dayu200上的效果近似一致。
资源导入
本案例为了简单起见,文字与颜色直接写在代码中,仅图片资源需要导入,将全部所需图片拖到资源文件夹的media子目录中:
首页结构
在新建工程时,选中“Tablet(平板)”作为默认显示设备,虽然手机页面与大屏网页区别比较大,其实布局结构思路上并不需要实质性的改变。
使用既有的index.ets入口页作为首页,分析页面的层次结构,可按上中下3个部分依次入列,即Column布局。
网页上部分有导航,直觉上属于上部分,不过由于网页比较长用户可以向下滑动,大部分实际应用场景中导航能随着用户滑动到下半部分而保持在顶部,灵活起见可将导航放在整个页面层次结构之上。
依此思路布局的话,代码上整体是一个Stack,内容部分是Column结构,结构骨架代码如下:
Stack { //堆叠结构
Column(){ //内容列
Top() //上
Mid() //中
Bottom() //下
}
Nav() //导航菜单
}
导航
导航本身内容在一行之内,不过由于有一个锐利的光源垂直照射阴影效果,依照设计,依旧要在其下放置一个略短的同类型结构,背景色略淡。
依此思路布局的话,代码上整体是一个Stack,内容部分是Row结构,结构骨架代码如下:
Stack { //堆叠结构
Shadow() //阴影
Row(){ //内容列
Left() //左
Center() //中
Right() //右
}
}
要创建组件,依照惯例,在pages目录下新建源码文件nav.ets。
阴影层
阴影层相对于菜单的背景层透明度为0.5,并有20的圆角。
Column() {
}
.width(750)
.height(150)
.backgroundColor('#1F242B')
.borderRadius(20)
.opacity(0.5)
菜单层
为了制造出阴影的效果,将菜单层的位置往垂直方向(y轴)上再上移20。
Row() {
}
.width(800)
.height(150)
.backgroundColor('#1F242B')
.borderRadius(20)
.offset({
y: -20
})
菜单阴影效果
将上述2个层合并置于Stack内,形成阴影效果,代码如下:
Stack() {
Column() {
}
.width(750)
.height(150)
.backgroundColor('#1F242B')
.borderRadius(20)
.opacity(0.5)
Row() {
}
.width(800)
.height(150)
.backgroundColor('#1F242B')
.borderRadius(20)
.offset({
y: -20
})
}
.width('100%')
菜单内容
菜单内容左侧是一个开关图标,距离菜单左侧边距为40:
Image($r("app.media.toggle"))
.width(64)
.height(39)
.margin({left:40})
中间的文字单独组成一行,间距20,宽度设为总宽度的60%:
Row({space: 20}) {
Blank()
Text('首页')
.fontSize(20)
.fontColor(Color.White)
Text('关于')
.fontSize(20)
.fontColor(Color.White)
Text('菜单')
.fontSize(20)
.fontColor(Color.White)
Text('主厨')
.fontSize(20)
.fontColor(Color.White)
Text('文化')
.fontSize(20)
.fontColor(Color.White)
Blank()
}
.width('60%')
菜单层最右侧是一个红色大按钮,距离菜单最右侧边距为40,这样与左侧图标的40左边距形成对称:
Button() {
Text('联系我们')
.fontColor(Color.White)
.fontSize(20)
}
.width(170)
.height(58)
.backgroundColor('#FF5146')
.borderRadius(7)
.type(ButtonType.Normal)
.margin(right:40)
完整代码
再给3个之间插入空白,导航栏的整体代码如下:
@Entry
@Component
export default struct Nav {
build() {
Column () {
Stack() {
Column() {
}
.width(900)
.height(90)
.backgroundColor('#1F242B')
.borderRadius(20)
.opacity(0.5)
Row() {
Image($r("app.media.toggle"))
.width(64)
.height(35)
.objectFit(ImageFit.Contain)
.margin({left:40})
Blank()
Row({space: 20}) {
Blank()
Text('首页')
.fontSize(15)
.fontColor(Color.White)
Text('关于')
.fontSize(15)
.fontColor(Color.White)
Text('菜单')
.fontSize(15)
.fontColor(Color.White)
Text('主厨')
.fontSize(15)
.fontColor(Color.White)
Text('文化')
.fontSize(15)
.fontColor(Color.White)
Blank()
}
.width('60%')
Blank()
Button() {
Text('联系我们')
.fontColor(Color.White)
.fontSize(14)
}
.width(170)
.height(45)
.backgroundColor('#FF5146')
.borderRadius(7)
.type(ButtonType.Normal)
.margin({right:40})
}
.width(950)
.height(100)
.backgroundColor('#1F242B')
.borderRadius(20)
.padding({top: 20})
.offset({
y: -20
})
}
.width('100%')
}
.width('100%')
.height('100%')
}
}
上半部分
上半部分页面看起来层次非常丰富,这对布局也提出了更高的要求。不过无论层次有多丰富,都可以通过行列和层互相交错堆叠来实现。
要创建组件,依照惯例,在pages目录下新建源码文件up.ets。
笔者选择的整体骨架结构,依旧沿用Stack分层,每一层使用Column或Row来继续分解:
Stack { //页面上半部分
Back() //背景图片层
Theme() //主题文字层
}
背景图片层
背景图片层有3个交错的图片,最底层的是左侧的曲线状填充:
Stack() {
Row() {
Image($r("app.media.leftTopMask"))
.width(703)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
}
.width('100%')
.height('100%')
但是对比设计图,观察到图片并不是完全显示,而是往左侧有一定的偏移:
Stack() {
Row() {
Image($r("app.media.leftTopMask"))
.width(703)
.objectFit(ImageFit.Fill)
.offset({x: -150})
Blank()
}
.width('100%')
}
.width('100%')
.height('100%')
添加偏移后的预览:
右侧的蔬菜水果图片左侧有一部分覆盖在最底层图片之上:
Row() {
Blank()
Image($r("app.media.cover"))
.width(649)
.objectFit(ImageFit.Fill)
}
.width('100%')
在这两个图层之上,还有一个认证徽章小图:
Column() {
Image($r("app.media.cert"))
.width(134)
.objectFit(ImageFit.Contain)
}
.width('100%')
对比设计图,图片需要左上有一定的偏移:
Column() {
Image($r("app.media.cert"))
.width(134)
.objectFit(ImageFit.Contain)
.offset({x: -150, y: -150})
}
.width('100%')
背景图片层整体还需要主题色背景,蔬菜图片高度需要有左侧曲线块一致:
Stack() {
Row() {
Image($r("app.media.leftTopMask"))
.width(703)
.objectFit(ImageFit.Fill)
.offset({x: -150})
Blank()
}
.width('100%')
Row() {
Blank()
Image($r("app.media.cover"))
.width(649)
.objectFit(ImageFit.Fill)
.margin(top:120)
}
.width('100%')
Column() {
Image($r("app.media.cert"))
.width(134)
.objectFit(ImageFit.Contain)
.offset({x: -150, y: -150})
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F9F4E5')
主题文字层
网页的主题文字列的布局非常简单,注意其中的“超快”和“配送”因为颜色不同,所以拆分成2段不同的Text组合在一行之内:
Column() {
Text('轻松追踪物流')
.fontSize(14)
.fontColor('#FF5146')
Row() {
Text('超快')
.fontSize(40)
Text('配送')
.fontSize(40)
.fontColor('#FF5146')
}
Text('&')
.fontSize(40)
Text('直送上门')
.fontSize(40)
Text("无论您身在任何一个城市,24小时内蔬菜瓜果、\n肉食禽蛋,我们都将风雨无阻,细致送达。\n100%新鲜,100%有机,100%源头")
.fontSize(14)
.fontWeight(FontWeight.Lighter)
.padding(top: 20)
}
.alignItems(HorizontalAlign.Start)
.padding(left: 60)
.width('100%')
文字列下方是一行内2个按钮,第一个搜索按钮的文字覆盖在按钮背景之上:
Row() {
Image($r("app.media.search"))
.height(17)
.width(17)
.objectFit(ImageFit.Contain)
.margin({right: 20})
Text('查找分店')
.fontColor(Color.White)
}
.borderRadius(27)
.height(40)
.backgroundColor('#FF5146')
.width(160)
.padding({left: 20})
.margin({top: 30})
与搜索按钮在同一行内右侧的是下单按钮,下单按钮左侧有一个播放图片:
Row() {
Image($r("app.media.play"))
.height(45)
.width(45)
.objectFit(ImageFit.Contain)
Text('如何下单?')
}
播放按钮正常布局,将其放在按钮组内,并且加上按钮阴影效果:
Row({space: 15}) {
Row() {
Image($r("app.media.search"))
.height(17)
.width(17)
.objectFit(ImageFit.Contain)
.margin({right: 20, left: 20})
Text('查找分店')
.fontColor(Color.White)
}
.borderRadius(27)
.height(40)
.backgroundColor('#FF5146')
.width(160)
.shadow({
radius: 20,
offsetX: 5,
offsetY: 5,
color: Color.Gray,
})
Row() {
Image($r("app.media.play"))
.height(45)
.width(45)
.objectFit(ImageFit.Contain)
Text('如何下单?')
}
}
.margin(top: 30)
右侧指示图片层
在蔬菜图片的上方有一个指示图片,想要指示图片到达指定的水果位置,需要同时调整它的容器,即列在水平和垂直方向的偏移量(offset):
Column() {
Image($r("app.media.target"))
.objectFit(ImageFit.Contain)
.width(260)
.height(150)
}
.alignItems(HorizontalAlign.End)
.offset({x: -125, y: -130})
.width('100%')
完整代码和效果
至此,上半部分所有组成子组件已经完成,把它们组合起来,up.ets完整代码如下:
import Nav from './nav.ets'
@Entry
@Component
struct Up {
build() {
Stack() {
Stack() {
Row() {
Image($r("app.media.leftTopMask"))
.width(703)
.objectFit(ImageFit.Fill)
.offset({x: -150})
Blank()
}
.width('100%')
Row() {
Blank()
Image($r("app.media.cover"))
.width(649)
.objectFit(ImageFit.Fill)
.margin({top:120})
}
.width('100%')
Column() {
Image($r("app.media.cert"))
.width(134)
.objectFit(ImageFit.Contain)
.offset({x: -150, y: -150})
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F9F4E5')
Column() {
Text('轻松追踪物流')
.fontSize(14)
.fontColor('#FF5146')
Row() {
Text('超快')
.fontSize(40)
Text('配送')
.fontSize(40)
.fontColor('#FF5146')
}
Text('&')
.fontSize(40)
Text('直送上门')
.fontSize(40)
Text("无论您身在任何一个城市,24小时内蔬菜瓜果、\n肉食禽蛋,我们都将风雨无阻,细致送达。\n100%新鲜,100%有机,100%源头")
.fontSize(14)
.fontWeight(FontWeight.Lighter)
.padding({top: 20})
Row({space: 15}) {
Row() {
Image($r("app.media.search"))
.height(17)
.width(17)
.objectFit(ImageFit.Contain)
.margin({right: 20, left: 20})
Text('查找分店')
.fontColor(Color.White)
}
.borderRadius(27)
.height(40)
.backgroundColor('#FF5146')
.width(160)
.shadow({
radius: 20,
offsetX: 5,
offsetY: 5,
color: Color.Gray,
})
Row() {
Image($r("app.media.play"))
.height(45)
.width(45)
.objectFit(ImageFit.Contain)
Text('如何下单?')
}
}
.margin({top: 30})
}
.alignItems(HorizontalAlign.Start)
.padding({left: 60})
.width('100%')
Column() {
Image($r("app.media.target"))
.objectFit(ImageFit.Contain)
.width(260)
.height(150)
}
.alignItems(HorizontalAlign.End)
.offset({x: -125, y: -130})
.width('100%')
Nav()
}
.width('100%')
.height('100%')
}
}
中间部分
中间部分比较简单,一系列友情链接网站的logo图片,均匀占据一行之内的空间。要创建新组件,在pages下新建mid.ets,代码如下:
@Entry
@Component
export default struct Mid {
build() {
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Start,
justifyContent: FlexAlign.Center
}) {
Image($r("app.media.partner1"))
.width(249)
.height(110)
.objectFit(ImageFit.Fill)
Image($r("app.media.partner2"))
.width(249)
.height(110)
.objectFit(ImageFit.Fill)
Image($r("app.media.partner3"))
.width(249)
.height(110)
.objectFit(ImageFit.Fill)
Image($r("app.media.partern4"))
.width(249)
.height(110)
.objectFit(ImageFit.Fill)
}
.width('100%')
.height('25%')
.padding(40)
}
}
下半部分
下半部分是一系列的甜甜圈卡片列表,均匀占据一行之内的空间。要创建新组件,在pages下新建bottom.ets。
卡片结构
单个卡片是由背景层和内容层叠加而成,其中内容层为3个组件组成的一列。
Stack {
Image()
Column() {
Image()
Text()
Button()
}
}
卡片背景
把卡片背景图至于卡片容器Stack的最底层:
Stack() {
Image($r("app.media.donutMask"))
.width(341)
.height(476)
.objectFit(ImageFit.Contain)
}
.borderRadius(20)
.backgroundColor('#DC7CFF')
.width(341)
.height(476)
发现图片左侧并未与容器最左侧对齐,遇到这种情况可以将图片包含在一个Row容器中,并缩短图片宽度,右侧加入Blank组件,以求与设计图一致,代码优化如下:
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(300)
.height(476)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
}
.borderRadius(20)
.backgroundColor('#DC7CFF')
.width(341)
.height(476)
卡片内容
卡片内容的布局在一列之中,注意按钮的阴影效果,以及适当调整间距:
Column() {
Image($r("app.media.donut"))
.width(229)
.height(182)
.objectFit(ImageFit.Contain)
Blank()
Text('蓝莓甜甜圈')
.fontSize(32)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(18)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15}
)
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(232)
.height(60)
.shadow({
radius: 25,
color: Color.Gray}
)
}
.height('80%')
参照第一张卡片来构建第二张卡片:
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(300)
.height(476)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(229)
.height(182)
.objectFit(ImageFit.Contain)
Blank()
Text('巧克力甜甜圈')
.fontSize(32)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(18)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15}
)
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(232)
.height(60)
.shadow({
radius: 25,
color: Color.Gray}
)
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#7CD8FF')
.width(341)
.height(476)
第三张卡片的代码:
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(300)
.height(476)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(229)
.height(182)
.objectFit(ImageFit.Contain)
Blank()
Text('草莓甜甜圈')
.fontSize(32)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('4折大酬宾')
.fontSize(18)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15}
)
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(232)
.height(60)
.shadow({
radius: 25,
color: Color.Gray}
)
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#BAFF7C')
.width(341)
.height(476)
卡片列表
把上面的3张卡片放入一行之中:
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('蓝莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#DC7CFF')
.width(280)
.height(400)
.margin(10)
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('蓝莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#7CD8FF')
.width(280)
.height(400)
.margin(10)
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('草莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('4折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#BAFF7C')
.width(280)
.height(400)
.margin(10)
}
.width('100%')
.height('100%')
卡片列表容器背景
需要加上整个卡片列表的背景色和背景图:
Stack() {
Row() {
Blank()
Image($r("app.media.rightBottomMask"))
.width(703)
.objectFit(ImageFit.Fill)
.offset(x: 150)
}
.width('100%')
}
.width('100%')
.height('80%')
.backgroundColor('#F9F4E5')
完整组件代码和预览效果
考虑到卡片实际预览之间没有空隙,需要缩小卡片大小。bottom.ets完整源文件:
@Entry
@Component
export default struct Bottom {
build() {
Stack() {
Row() {
Blank()
Image($r("app.media.rightBottomMask"))
.width(703)
.objectFit(ImageFit.Fill)
.offset({x: 150})
}
.width('100%')
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('蓝莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#DC7CFF')
.width(280)
.height(400)
.margin(10)
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('蓝莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('5折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#7CD8FF')
.width(280)
.height(400)
.margin(10)
Stack() {
Row() {
Image($r("app.media.donutMask"))
.width(280)
.height(400)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
Column() {
Image($r("app.media.donut"))
.width(200)
.height(160)
.objectFit(ImageFit.Contain)
Blank()
Text('草莓甜甜圈')
.fontSize(25)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
Button(){
Text('4折大酬宾')
.fontSize(15)
.margin({
left: 40,
right: 40,
top: 15,
bottom: 15
})
}
.type(ButtonType.Normal)
.backgroundColor(Color.White)
.borderRadius(10)
.width(220)
.height(50)
.shadow({
radius: 25,
color: Color.Gray
})
}
.height('80%')
}
.borderRadius(20)
.backgroundColor('#BAFF7C')
.width(280)
.height(400)
.margin(10)
}
.width('100%')
.height('100%')
}
.width('100%')
.height('80%')
.backgroundColor('#F9F4E5')
}
}
下半屏预览
设计中中间与下半部分加起来相当于上半部分的比例是1:1,即占据整个屏幕的宽和高,为了测试期间,讲中间部分与下半部分做一个组合,来查看实际的半屏效果。
在pages下新建一个secondhalf.ets,导入mid.ets和bottom.ets,将两者组合到一列中:
import Mid from './mid.ets'
import Bottom from './bottom.ets'
@Entry
@Component
struct Secondhalf {
build() {
Flex({
direction: FlexDirection.Column,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Mid()
Bottom()
}
.width('100%')
.height('100%')
}
}
总结
Dayu200不仅适合设备开发,更适合App开发,配合最新的DevEco Studio 3.0,即使您手头没有设备,也可以进行相对完善的UI开发大部分工作。
老师的布局还是很漂亮的!
还得是我波神啊
一起学习兄弟