#Open Harmony DAYU200体验官# 世界巡游团App 原创 精华
@toc
主题
本帖使用Dayu200为开发板,展示一个在线旅游App - 世界巡游团。
注意:本文不涉及App上的用户互动,仅为页面设计效果的实现。
设计效果图
Dayu200的预览配置
为了大幅提高UI的开发效率,降低Dayu200的使用门槛,在开发过程中,强烈建议使用DevEco Studio 3.0 Beta3(OpenHarmony)的MatePadPro作为预览配置,并调整到竖屏模式,最终与Dayu200上的效果近似一致。
资源导入
本案例为了简单起见,文字与颜色直接写在代码中,仅图片资源需要导入,将所需图片拖到资源文件夹的media子目录中:
启动页
使用默认的index.ets入口页作为启动页,分析页面的结构,所有的元素都可以放入一个Column容器中,即组件依次放入一列之中布局。
封面组件
封面由4张度假酒店的门面图片组成,整体上呈现一种瀑布流的布局的Grid组件。不过当前Grid组件尚未支持瀑布流布局,作为迂回的实现,可以改为两列式的左右拼接,两侧各占50%的水平空间,即内部横向布局(Row)。
要创建一个新组件,依照惯例,在pages目录下新建源码文件bannerLeft.ets,用于容纳左侧图片,左侧最下方的图片有一个暗色效果,可以调低图片的亮度。
Image()
.brightness(0.3)
图片本身的尺寸固定大小,另有圆角和内边距:
Column({space:20}) {
Image($r("app.media.cover1"))
.width(149)
.height(222)
.objectFit(ImageFit.Contain)
.borderRadius(14)
Image($r("app.media.cover2"))
.width(149)
.height(222)
.objectFit(ImageFit.Contain)
.borderRadius(14)
.brightness(0.3)
.width('50%')
}
.padding(20)
再创建一个新组件,依照惯例,在pages目录下新建源码文件bannerRight.ets,布局与左
侧的相似,右侧最下方图片同样调低亮度,完整代码如下:
Column({space:20}) {
Image($r("app.media.cover3"))
.width(149)
.height(222)
.objectFit(ImageFit.Contain)
.borderRadius(14)
Image($r("app.media.cover4"))
.width(149)
.height(222)
.objectFit(ImageFit.Contain)
.borderRadius(14)
.brightness(0.3)
.width('50%')
}
.padding(20)
不过看起来图片产生了尺寸不对称问题,原因是图片本身可能存在大小不同,如果使用
相同的保持比例的填充方式,就会在容器大小固定的情况下产生变化。
需要把原先图片的ObjectFit从Contain:
Image(){
}
.objectFit(ImageFit.Contain)
改为:
Image(){
}
.objectFit(ImageFit.Fill)
更新组件代码:
@Entry
@Component
struct BannerRight {
build() {
Column({space:20}) {
Image($r("app.media.cover3"))
.width(149)
.height(222)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Image($r("app.media.cover4"))
.width(149)
.height(222)
.objectFit(ImageFit.Fill)
.borderRadius(14)
.brightness(0.3)
}
.padding(20)
.width('50%')
}
}
最后创建封面组件,依照惯例,在pages目录下新建源码文件banner.ets,将左右两侧容器组合起来,完整代码如下:
import bannerLeft from './bannerLeft.ets'
import bannerRight from './bannerRight.ets'
@Entry
@Component
export default struct Banner {
build() {
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
bannerLeft()
bannerRight()
}
}
}
因为通常创建新组件默认可能是生成Flex组件作为根容器,为了便利起见,在很多情况下,可以Flex组件代替Row组件,唯一需要是把direction属性设为Row即可。
启动按钮组件
启动页最下方是开始按钮,左侧一个右箭头图标略微离圆角有一点距离,右侧是文字,针对这种布局,可以使用Row容器,插入Blank组件来布局,相对比使用Flex容器更直观也容易理解。
Row() {
Image($r("app.media.start"))
.width(42)
.height(42)
.margin({
left:5}
)
Blank()
Text('开始巡游')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
}
.borderRadius(27)
.width(247)
.height(52)
.margin(10)
比较有挑战的是按钮的渐变色背景,观察设计稿的效果,是从上到下的淡绿色到绿色的过渡,可以使用线性渐变属性:
Row(){
}
.linearGradient()
线性渐变有3个参数:angle: 线性渐变的角度,direction: 线性渐变的方向,colors: 为渐变的颜色描述,repeating: 为渐变的颜色重复着色。
加上从上至下,即水平180度从淡绿色到绿色的参数:
Row(){
}
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]}
)
完整按钮代码如下:
@Entry
@Component
export default struct StartButton {
build() {
Row() {
Image($r("app.media.start"))
.width(42)
.height(42)
.margin({
left:5
})
Blank()
Text('开始巡游')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
}
.borderRadius(27)
.width(247)
.height(52)
.margin(10)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
}
背景色
在index.ets中使用Column容器包含所有子组件,给予背景色:
Column() {
banner()
StartButton()
}
.width('100%')
.height('100%')
.backgroundColor('#252A39')
右侧封面修正
到了这一步才发现右侧封面是相同大小,不过不用担心,因为是组件式的组合,可以直接修改右侧封面组件上方图片的高度为184,下方图片高度为273。bannerRight.ets的代码更新如下:
@Entry
@Component
struct BannerRight {
build() {
Column({space:20}) {
Image($r("app.media.cover3"))
.width(149)
.height(184)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Image($r("app.media.cover4"))
.width(149)
.height(273)
.objectFit(ImageFit.Fill)
.borderRadius(14)
.brightness(0.3)
}
.padding(20)
.width('50%')
}
}
修正之后,再次刷新首页预览:
完整代码和效果
把启动页的两段文字都加入,形成完整的index.ets代码如下:
import banner from './banner.ets'
import StartButton from './StartButton.ets'
@Entry
@Component
struct Index {
build() {
Column() {
banner()
Text("世界巡游团,启航")
.fontSize(32)
.fontColor(Color.White)
Blank()
Text("极尽所能,让您搜索优美的度假圣地")
.fontSize(16)
.fontColor('#767D92')
Blank()
StartButton()
Blank()
}
.width('100%')
.height('100%')
.backgroundColor('#252A39')
}
}
首页
分析页面的结构,与启动页非常相似,只不过组件较多。所有的元素都可以放入一个Column容器中,即组件依次放入一列之中布局。要创建组件,依照惯例,在pages目录下新建源码文件home.ets。
导航组件
导航由位于一行以内的文字块与头像图片组成,即内部横向布局(Row)。文字块的2块文字按列布局。
要创建一个新组件,依照惯例,在pages目录下新建源码文件nav.ets。
文字块:
Column(){
Text('小雅')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('探索世界之美')
.fontSize(18)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
头像图片
Image($r("app.media.avatar"))
.height(44)
.width(44)
.objectFit(ImageFit.Fill)
组合起来
@Entry
@Component
struct Nav {
build() {
Flex(
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceBetween
) {
Column(){
Text('小雅')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('探索世界之美')
.fontSize(18)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
Image($r("app.media.avatar"))
.height(44)
.width(44)
.objectFit(ImageFit.Fill)
}
.padding(20)
.width('100%')
}
}
口号文字组件
此组件非常简单,两段文字放入一列中。要创建组件,依照惯例,在pages目录下新建源码文件nav.ets。
完整代码如下:
@Entry
@Component
struct Slogon {
build() {
Column() {
Text('发现\n度假新世界')
.fontSize(35)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('度假圣地搜索、一触即达')
.fontSize(18)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding(20)
.width('100%')
}
}
搜索条组件
按照惯例要新建一个组件,在pages目录下新建一个searchbar.ets。搜索条右侧有一个渐变带图标的按钮,可以使用线性渐变属性修饰一个Row容器即可:
Row(){
Image($r("app.media.filter"))
.width(42)
.height(42)
.margin({
left:5}
)
}
.borderRadius(12)
.height(52)
.width(52)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]}
)
搜索输入框可以使用TextInput组件:
TextInput(
placeholder: '搜索'
)
.placeholderColor('#767D92')
.width('75%')
.height(52)
至于搜索前面的搜索小图标,TextInput暂不支持使用图标。这里留给读者去思考如何实现。
把两个组件组合起来,完整代码如下:
@Entry
@Component
export default struct Searchbar {
build() {
Row() {
TextInput({
placeholder: '搜索'
})
.placeholderColor('#767D92')
.width('75%')
.height(52)
Blank()
Row(){
Image($r("app.media.filter"))
.width(42)
.height(42)
.margin({
left:5
})
}
.borderRadius(12)
.height(52)
.width(52)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
.padding(20)
.width('100%')
}
}
筛选按钮栏
按照惯例要新建一个组件,在pages目录下新建一个filter.ets。因为筛选可能有很多个按钮,未能显示的筛选按钮可以向右滑动后展示,那么这里毫无疑问要选择List组件。滑动方向listDirection设置为Horizontal水平,另加上边距20:
List() {
ListItem() {
}
}
.listDirection(Axis.Horizontal)
.padding(20)
List中的每一项由ListItem进行包裹。先来实现第一个带渐变色的圆角“全部”按钮:
Row(){
Text('全部')
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.fontSize(18)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
嵌入ListItem中:
List() {
ListItem() {
Row(){
Text('全部')
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.fontSize(18)
.padding(top:8,bottom:8,left:20, right:20)
}
.borderRadius(8)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
}
.listDirection(Axis.Horizontal)
.padding(20)
接着是未选中的第二个“探险之旅”按钮,与选中的相比,未选中的按钮背景为深色无渐变,字体呈灰白色:
ListItem() {
Row(){
Text('探险之旅')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(18)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
好像两个按钮之间需要空隙,给List整体配置一个间距:
List({space:10}) {
ListItem() {
Row(){
Text('全部')
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.fontSize(18)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]}
)
}
ListItem() {
Row(){
Text('探险之旅')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(18)
.padding(top:8,bottom:8,left:20, right:20)
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
}
.listDirection(Axis.Horizontal)
.padding(20)
下一个按钮布局与上一个相同,文字做更改:
ListItem() {
Row(){
Text('洞穴')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(18)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
第四个按钮也是相同的布局:
ListItem() {
Row(){
Text('沙漠')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(18)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
可以看到默认因为按钮总体长度超出了List宽度,会被屏幕截断,尝试在向右滑动一下列表:
筛选按钮栏源文件filter.ets完整的代码如下:
@Entry
@Component
export default struct Filter {
build() {
List({space:10}) {
ListItem() {
Row(){
Text('全部')
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
ListItem() {
Row(){
Text('探险之旅')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
ListItem() {
Row(){
Text('洞穴')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
ListItem() {
Row(){
Text('沙漠')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
}
.listDirection(Axis.Horizontal)
.padding(20)
.height(80)
}
}
推荐卡片列表组件
推荐卡片列表组件分为标题栏和推荐卡片列表。在pages下新建card.ets。
标题栏非常简单,文字在一行之内,两侧加上间距:
Row() {
Text('推荐')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Text('看全部')
.fontSize(16)
.fontColor('#A7A6A7')
}
.padding(20)
.width('100%')
推荐卡片列表也是一个可以水平左右滚动的组件,选择List及ListItem组件,给予标准间距20:
List({space: 20}) {
ListItem(){
}
}
.listDirection(Axis.Horizontal)
先来实现单独的卡片,卡片由背景图、右上角收藏按钮、左下角两段文字描述叠加组成。首先是圆角背景图:
Stack() {
Image($r("app.media.item1"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
}
然后右上角加入收藏按钮,收藏按钮居右,可以包含在一个Row中:
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
可以看到预览中的位置并不在右上角,因为下方的文字区域还没生成,稍候再来解决。文字区域由两端按列布局的左对齐文字,加上左侧的标准边距20,以及右侧的行距构成:
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
再将爱心图标和文字区域整体放入一个Column中,Column尺寸与背景图尺寸一致:
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
整个叠加出来的Stack代码,作为第一张卡片的ListItem代码如下:
ListItem() {
Stack() {
Image($r("app.media.item1"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
第二张卡片,替换其中的背景图和文字区域:
ListItem() {
Stack() {
Image($r("app.media.item2"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('海边公寓')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('圭亚那 科里亚')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
第三张卡片,如上替换其中的背景图和文字区域:
ListItem() {
Stack() {
Image($r("app.media.item3"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('蒙特维多')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
发现拖动到最右端没有边距,增加List的外边距,完整List代码如下:
List({space: 20}) {
ListItem() {
Stack() {
Image($r("app.media.item1"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item2"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('海边公寓')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('圭亚那 科里亚')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item3"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('蒙特维多')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
}
.margin(20)
.listDirection(Axis.Horizontal)
可以看到预览中标题不可见,是因为暂时还未切换到整个页面带背景色的情况下,此时可以切换成暗黑模式查看标题栏:
完整的推荐卡片列表组件代码如下:
@Entry
@Component
export default struct Card {
build() {
Column(){
Row() {
Text('推荐')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Text('看全部')
.fontSize(16)
.fontColor('#A7A6A7')
}
.padding({left:20, right: 30, top: 5, bottom:5})
.width('100%')
List({space: 20}) {
ListItem() {
Stack() {
Image($r("app.media.item1"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item2"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('海边公寓')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('圭亚那 科里亚')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item3"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('蒙特维多')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
}
.margin({left:20,right:20})
.listDirection(Axis.Horizontal)
.height(290)
}
}
}
选项卡组件
在pages下新建tabbar.ets,作为底部选项卡的组件源文件。选项卡的所有组件都在一个Row中,其中第一个是熟悉的渐变色按钮,左侧也配了一个图标:
Row() {
Blank()
Image($r("app.media.home"))
.width(28)
.height(28)
.margin({
left:5
})
Text('主页')
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.padding(left:10)
Blank()
}
.borderRadius(27)
.width(126)
.height(48)
.margin(10)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
第二个收藏按钮:
Image($r("app.media.heart"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
第三个购物车按钮:
Image($r("app.media.cart"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
第四个是个人按钮:
Image($r("app.media.user"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
选项卡整体效果如下:
需要在各图标间插入Blank以便均匀分布在一行以内,tabbar.ets的完整源码如下:
@Entry
@Component
export default struct Tabbar {
build() {
Row() {
Row() {
Blank()
Image($r("app.media.home"))
.width(28)
.height(28)
.margin({
left:5
})
Text('主页')
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.padding({left:10})
Blank()
}
.borderRadius(27)
.width(126)
.height(48)
.margin(10)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
Blank()
Image($r("app.media.heart"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
Blank()
Image($r("app.media.cart"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
Blank()
Image($r("app.media.user"))
.width(28)
.height(28)
.objectFit(ImageFit.Fill)
Blank()
}
.width('100%')
.padding({left: 20,right:20,bottom: 10})
// .height('100%')
}
}
首页的调整优化
把上述的组件依次导入到index.ets的Home组件中,完整代码如下:
import Nav from './nav.ets'
import Slogon from './slogon.ets'
import Searchbar from './searchbar.ets'
import Filter from './filter.ets'
import Card from './card.ets'
import Tabbar from './tabbar.ets'
@Entry
@Component
struct Home {
build() {
Column() {
Nav()
Slogon()
Searchbar()
Filter()
Card()
Tabbar()
}
.width('100%')
.height('100%')
.backgroundColor('#252A39')
}
}
此时预览不一定能完美适配整个页面,依然需要对每个子组件对针对性的字体、容器、边距等调整优化。
nav.ets优化后:
@Entry
@Component
struct Nav {
build() {
Flex(
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceBetween
) {
Column(){
Text('小雅')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('探索世界之美')
.fontSize(18)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
Image($r("app.media.avatar"))
.height(44)
.width(44)
.objectFit(ImageFit.Fill)
}
.padding({top:10,bottom: 5, left:20,right:20})
.width('100%')
}
}
slogon.ets优化后:
@Entry
@Component
export default struct Slogon {
build() {
Column() {
Text('发现\n度假新世界')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('度假圣地搜索、一触即达')
.fontSize(15)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding(20)
.width('100%')
}
}
searchbar.ets优化后:
@Entry
@Component
export default struct Searchbar {
build() {
Row() {
TextInput({
placeholder: '搜索'
})
.placeholderColor('#767D92')
.width('75%')
.height(52)
Blank()
Row(){
Image($r("app.media.filter"))
.width(42)
.height(42)
.margin({
left:5
})
}
.borderRadius(12)
.height(52)
.width(52)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
.padding(20)
.width('100%')
}
}
filter.ets优化后:
@Entry
@Component
export default struct Filter {
build() {
List({space:10}) {
ListItem() {
Row(){
Text('全部')
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
})
}
ListItem() {
Row(){
Text('探险之旅')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
ListItem() {
Row(){
Text('洞穴')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
ListItem() {
Row(){
Text('沙漠')
.fontWeight(FontWeight.Lighter)
.fontColor('#767D92')
.fontSize(12)
.padding({top:8,bottom:8,left:20, right:20})
}
.borderRadius(8)
.backgroundColor('#2D3344')
}
}
.listDirection(Axis.Horizontal)
.padding(20)
.height(80)
}
}
card.ets优化后:
@Entry
@Component
export default struct Card {
build() {
Column(){
Row() {
Text('推荐')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Text('看全部')
.fontSize(16)
.fontColor('#A7A6A7')
}
.padding({left:20, right: 30, top: 5, bottom:5})
.width('100%')
List({space: 20}) {
ListItem() {
Stack() {
Image($r("app.media.item1"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('圣胡安')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('美国 阿拉巴马')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item2"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('海边公寓')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('圭亚那 科里亚')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
ListItem() {
Stack() {
Image($r("app.media.item3"))
.width(220)
.height(290)
.objectFit(ImageFit.Fill)
.borderRadius(14)
Column() {
Row() {
Blank()
Row() {
Image($r("app.media.heart_white"))
.width(32)
.height(32)
.objectFit(ImageFit.Fill)
}
.backgroundColor(Color.Gray)
.backdropBlur(0.9)
.borderRadius(8)
}
.width('100%')
.padding(20)
Blank()
Row() {
Column() {
Text('蒙特维多')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(18)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
}
.width('100%')
}
.width(220)
.height(290)
}
}
}
.margin({left:20,right:20})
.listDirection(Axis.Horizontal)
.height(290)
}
}
}
全部子组件优化后的首页预览如下:
详情页
详情页由全屏的背景图、导航栏、缩略图列表和详情卡片堆叠而成。要创建组件,依照惯例,在pages目录下新建源码文件detail.ets。
导航栏
在pages目录下新建源码文件nav2.ets。导航栏由返回按钮和用户头像在一列内组成,首先是返回按钮:
Row() {
Blank()
Image($r("app.media.back"))
.width(32)
.height(32)
.objectFit(ImageFit.Contain)
Blank()
}
.width(52)
.height(52)
.backgroundColor(Color.Gray)
.backdropBlur(0.8)
.borderRadius(8)
加上右侧头像已经中间的间距,以及导航栏容器Row整体的内边距:
@Entry
@Component
struct Nav2 {
build() {
Row() {
Row() {
Blank()
Image($r("app.media.back"))
.width(32)
.height(32)
.objectFit(ImageFit.Contain)
Blank()
}
.width(52)
.height(52)
.backgroundColor(Color.Gray)
.backdropBlur(0.8)
.borderRadius(8)
Blank()
Image($r("app.media.avatar"))
.width(44)
.height(44)
.objectFit(ImageFit.Contain)
}
.width('100%')
.padding(20)
}
}
缩略图列表
缩略图列表由4张带边框的小图排成一行组成。在pages下新建thumb.ets文件,先实现单个的缩略图:
Image($r("app.media.cover1"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
把图片装入一个Row,再设置一个内边距即可便捷创建一个描边的效果:
Row() {
Image($r("app.media.cover1"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
}
.borderRadius(8)
.backgroundColor(Color.Gray)
.padding(5)
把其他缩略图就加上,完整代码如下:
@Entry
@Component
export default struct Thumb {
build() {
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceAround
}) {
Row() {
Image($r("app.media.cover1"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
}
.borderRadius(8)
.backgroundColor(Color.Gray)
.padding(5)
Row() {
Image($r("app.media.cover2"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
}
.borderRadius(8)
.backgroundColor(Color.Gray)
.padding(5)
Row() {
Image($r("app.media.cover3"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
}
.borderRadius(8)
.backgroundColor(Color.Gray)
.padding(5)
Row() {
Image($r("app.media.cover4"))
.width(50)
.height(50)
.objectFit(ImageFit.Fill)
.borderRadius(8)
}
.borderRadius(8)
.backgroundColor(Color.Gray)
.padding(5)
}
.padding({left: 30, right:30, bottom: 30})
.width('100%')
}
}
详情卡片
在pages下新建infoCard.ets源文件。首先是卡片容器,高度是屏幕高度的一半,有主题背景色:
Column() {
}
.backgroundColor('#262A39')
.borderRadius(44)
.width('100%')
.height('50%')
卡片标题由左侧文字区域和右侧收藏按钮组成:
Row() {
Column() {
Text('蒙特维多庄园')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(15)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
Column() {
Image($r("app.media.bookmark"))
.height(32)
.width(32)
.objectFit(ImageFit.Contain)
}
.padding(10)
.borderRadius(8)
.backgroundColor('#2D3344')
}
.width('100%')
.padding(right:20)
标题下方是参观人的头像和人数,注意头像是一个叠加在另一个之上,视觉效果像是一串圆形硬币向右展开,先放置一个Row容器和左侧的Stack容器和一个头像:
Row() {
Stack() {
Image($r("app.media.avatar"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Contain)
}
}
.width('100%')
.padding(20)
然后依次挨个叠加其他头像,简单起见可以使用封面的图来作为其他头像的后续,要注意挨个相对于左侧的边距偏移,即translate属性中的x值:
Row() {
Stack() {
Image($r("app.media.item3"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
Image($r("app.media.cover1"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({x: 25})
Image($r("app.media.cover2"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({x: 50})
Image($r("app.media.cover3"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({x: 75})
Image($r("app.media.item1"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({x: 100})
Image($r("app.media.avatar"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({x: 125})
}
.height(40)
Blank()
Text('50人参团')
.fontColor('#00ADB5')
.fontSize(14)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(20)
接下来是详情描述区,布局与标题类似:
Column(){
Row() {
Text('描述')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Text('看更多')
.fontSize(16)
.fontColor('#00ADB5')
}
.width('100%')
.padding({bottom: 20})
Text('蒙特维多乡村庄园是一个宁静的,位于生态保护区附近舒适度假酒店,此处的赛若普兰诺地区以蒙特维度蝴蝶花园而闻名于世。')
.fontSize(15)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding({left:20, right: 20})
卡片的最后一个部分是价格和预定按钮,其中按钮保持整个设计中统一的渐变色:
Row() {
Column() {
Text('定价')
.fontSize(18)
.fontColor('#767D92')
Text('$20/晚')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
Blank()
}
.width('100%')
.padding({left:20, right: 20, bottom:30})
渐变按钮完整代码:
Column() {
Blank()
Text('现在预定')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
}
.borderRadius(18)
.width(188)
.height(52)
.linearGradient({
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]}
)
信息卡片完整代码:
@Entry
@Component
struct InfoCard {
build() {
Column() {
Row() {
Column() {
Text('蒙特维多庄园')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('澳大利亚 悉尼')
.fontSize(15)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding(20)
Blank()
Column() {
Image($r("app.media.bookmark"))
.height(32)
.width(32)
.objectFit(ImageFit.Contain)
}
.padding(10)
.borderRadius(8)
.backgroundColor('#2D3344')
}
.width('100%')
.padding({ right: 20 })
Row() {
Stack() {
Image($r("app.media.item3"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
Image($r("app.media.cover1"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({ x: 25 })
Image($r("app.media.cover2"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({ x: 50 })
Image($r("app.media.cover3"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({ x: 75 })
Image($r("app.media.item1"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({ x: 100 })
Image($r("app.media.avatar"))
.height(40)
.width(40)
.borderRadius(20)
.objectFit(ImageFit.Fill)
.translate({ x: 125 })
}
.height(40)
Blank()
Text('50人参团')
.fontColor('#00ADB5')
.fontSize(14)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(20)
Column() {
Row() {
Text('描述')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Text('看更多')
.fontSize(16)
.fontColor('#00ADB5')
}
.width('100%')
.padding({ bottom: 20 })
Text('蒙特维多乡村庄园是一个宁静的,位于生态保护区附近舒适度假酒店,此处的赛若普兰诺地区以蒙特维度蝴蝶花园而闻名于世。')
.fontSize(15)
.fontColor('#767D92')
}
.alignItems(HorizontalAlign.Start)
.padding( {left: 20, right: 20} )
Blank()
Row() {
Column() {
Text('定价')
.fontSize(18)
.fontColor('#767D92')
Text('$20/晚')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.alignItems(HorizontalAlign.Start)
Blank()
Column() {
Blank()
Text('现在预定')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Blank()
}
.borderRadius(18)
.width(188)
.height(52)
.linearGradient(
angle: 180,
direction: GradientDirection.Bottom,
colors: [[0x00F4FF,0],[0x00ADB5,1]]
)
}
.width('100%')
.padding({left:20, right: 20, bottom:30})
}
.backgroundColor('#262A39')
.borderRadius(44)
.width('100%')
.height('50%')
}
}
详情页的调整优化
把上述的组件依次导入到detail.ets的Detail组件中,完整代码如下:
import Nav2 from './nav2.ets'
import Thumb from './thumb.ets'
import InfoCard from './infocard.ets'
@Entry
@Component
struct Detail {
build() {
Stack() {
Image($r("app.media.cover4"))
.objectFit(ImageFit.Fill)
Column() {
Nav2()
Blank()
Thumb()
InfoCard()
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
总结
Dayu200不仅适合设备开发,更适合App开发,配合最新的DevEco Studio 3.0,即使您手头没有设备,也可以进行相对完善的UI开发大部分工作。
老师界面设计的总是那么好看
必须的老铁