(目錄)
主題
本帖使用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開發大部分工作。
生鮮app頁面源(https://ost.51cto.com/resource/2149)