未验证 提交 2a127cb9 编写于 作者: O openharmony_ci 提交者: Gitee

!10896 UI指南-声明式开发实例

Merge pull request !10896 from 田雨/master
......@@ -159,5 +159,7 @@
- [时间选择弹窗](ts-methods-timepicker-dialog.md)
- [文本选择弹窗](ts-methods-textpicker-dialog.md)
- [菜单](ts-methods-menu.md)
- [应用级变量的状态管理](ts-state-management.md)
- [像素单位](ts-pixel-units.md)
- [枚举说明](ts-appendix-enums.md)
- [类型说明](ts-types.md)
......@@ -3,12 +3,12 @@
为开发者提供4种像素单位,框架采用vp为基准数据单位。
| 名称 | 描述 |
| ---- | ---------------------------------------- |
| px | 屏幕物理像素单位。 |
| vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。 |
| fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
| lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过[designWidth](../quick-start/package-structure.md)配置)的比值,designWidth默认值为720。当designWidth为720时,在实际宽度为1440物理像素的屏幕上,1lpx为2px大小。 |
| 名称 | 描述 |
| ---- | ------------------------------------------------------------ |
| px | 屏幕物理像素单位。 |
| vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。 |
| fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
| lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过[designWidth](../../quick-start/package-structure.md)配置)的比值,designWidth默认值为720。当designWidth为720时,在实际宽度为1440物理像素的屏幕上,1lpx为2px大小。 |
## 像素单位转换
......
......@@ -7,25 +7,27 @@
- 文件组织
- [目录结构](ts-framework-directory.md)
- [应用代码文件访问规则](ts-framework-file-access-rules.md)
- [像素单位](ts-pixel-units.md)
- 深入理解组件化
- [自定义组件初始化](ts-custom-component-initialization.md)
- [自定义组件生命周期回调函数](ts-custom-component-lifecycle-callbacks.md)
- [组件创建和重新初始化示例](ts-component-creation-re-initialization.md)
- 声明式开发实例
- [创建简单视图](ui-ts-creating-simple-page.md)
- 构建实例
- [构建食物数据模型](ui-ts-building-data-model.md)
- [构建食物列表List布局](ui-ts-building-category-list-layout.md)
- [构建食物分类Grid布局](ui-ts-building-category-grid-layout.md)
- [页面跳转与数据传递](ui-ts-page-redirection-data-transmission.md)
- 添加闪屏动画
- [绘制图像](ui-ts-drawing-feature)
- [添加动画效果](ui-ts-animation-feature)
- 常见布局开发指导
- [弹性布局](ui-ts-layout-flex.md)
- [栅格布局](ui-ts-layout-grid-container.md)
- [媒体查询](ui-ts-layout-mediaquery.md)
- [Web组件开发](ui-ts-components-web.md)
- 体验声明式UI
- [创建声明式UI工程](ui-ts-creating-project.md)
- [初识Component](ui-ts-components.md)
- [创建简单视图](ui-ts-creating-simple-page.md)
- 页面布局与连接
- [构建食物数据模型](ui-ts-building-data-model.md)
- [构建食物列表List布局](ui-ts-building-category-list-layout.md)
- [构建食物分类Grid布局](ui-ts-building-category-grid-layout.md)
- [页面跳转与数据传递](ui-ts-page-redirection-data-transmission.md)
- [性能提升的推荐方法](ts-performance-improvement-recommendation.md)
- UI开发(兼容JS的类Web开发范式)
- [概述](ui-js-overview.md)
......
......@@ -14,7 +14,7 @@
```
具体允许哪种方式取决于状态变量的装饰器:
具体允许哪种方式取决于[状态变量](../quick-start/arkts-state-mgmt-concepts.md)的装饰器:
| 装饰器类型 | 本地初始化 | 通过构造函数参数初始化 |
......
# 添加动画效果
动画主要包含了组件动画和页面间动画,并提供了[插值计算](../reference/apis/js-apis-curve.md)[矩阵变换](../reference/apis/js-apis-matrix4.md)的动画能力接口,让开发者极大程度的自主设计动画效果。
在本节主要完成两个动画效果:
1. 启动页的闪屏动画,即Logo图标的渐出和放大效果;
2. 食物列表页和食物详情页的共享元素转场动画效果。
## animateTo实现闪屏动画
组件动画包括属性动画和animateTo显式动画:
1. 属性动画:设置组件通用属性变化的动画效果。
2. 显式动画:设置组件从状态A到状态B的变化动画效果,包括样式、位置信息和节点的增加删除等,开发者无需关注变化过程,只需指定起点和终点的状态。animateTo还提供播放状态的回调接口,是对属性动画的增强与封装。
闪屏页面的动画效果是Logo图标的渐出和放大,动画结束后跳转到食物分类列表页面。接下来,我们就使用animateTo来实现启动页动画的闪屏效果。
1. 动画效果自动播放。闪屏动画的预期效果是,进入Logo页面后,animateTo动画效果自动开始播放,可以借助于组件显隐事件的回调接口来实现。调用Shape的onAppear方法,设置其显式动画。
```ts
Shape() {
...
}
.onAppear(() => {
animateTo()
})
```
2. 创建opacity和scale数值的成员变量,用装饰器@State修饰。表示其为有状态的数据,即改变会触发页面的刷新。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo()
})
}
}
```
3. 设置animateTo的动画曲线curve。Logo的加速曲线为先慢后快,使用贝塞尔曲线cubicBezier,cubicBezier(0.4, 0, 1, 1)。
需要使用动画能力接口中的插值计算,首先要导入curves模块。
```ts
import Curves from '@ohos.curves'
```
@ohos.curves模块提供了线性Curve. Linear、阶梯step、三阶贝塞尔(cubicBezier)和弹簧(spring)插值曲线的初始化函数,可以根据入参创建一个插值曲线对象。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
curve: this.curve1
})
})
}
}
```
4. 设置动画时长为1s,延时0.1s开始播放,设置显示动效event的闭包函数,即起点状态到终点状态为透明度opacityValue和大小scaleValue从0到1,实现Logo的渐出和放大效果。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 2000,
curve: this.curve1,
delay: 100,
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
5. 闪屏动画播放结束后定格1s,进入FoodCategoryList页面。设置animateTo的onFinish回调接口,调用定时器Timer的setTimeout接口延时1s后,调用router.replace,显示FoodCategoryList页面。
```ts
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 2000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replace({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
整体代码如下。
```ts
import Curves from '@ohos.curves'
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
Path()
.commands(this.pathCommands1)
.fill('none')
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 2000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replace({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top: 300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![animation-feature](figures/animation-feature.gif)
## 页面转场动画
食物分类列表页和食物详情页之间的共享元素转场,即点击FoodListItem/FoodGridItem后,食物缩略图会放大,随着页面跳转,到食物详情页的大图。
1. 设置FoodListItem和FoodGridItem的Image组件的共享元素转场方法(sharedTransition)。转场id为foodItem.id,转场动画时长为1s,延时0.1s播放,变化曲线为贝塞尔曲线Curves.cubicBezier(0.2, 0.2, 0.1, 1.0) ,需引入curves模块。
共享转场时会携带当前元素的被设置的属性,所以创建Row组件,使其作为Image的父组件,设置背景颜色在Row上。
在FoodListItem的Image组件上设置autoResize为false,因为image组件默认会根据最终展示的区域,去调整图源的大小,以优化图片渲染性能。在转场动画中,图片在放大的过程中会被重新加载,所以为了转场动画的流畅,autoResize设置为false。
```ts
// FoodList.ets
import Curves from '@ohos.curves'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(40)
.width(40)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
}
.params({ foodData: this.foodItem })
.margin({ right: 24, left:32 })
}
}
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(152)
.width('100%')
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
.padding({ left: 8 })
Text(this.foodItem.calories + 'kcal')
.fontSize(14)
.margin({ right: 6 })
}
.height(32)
.width('100%')
.backgroundColor('#FFe5e5e5')
}
.height(184)
.width('100%')
.onClick(() => {
router.push({ url: 'pages/FoodDetail', params: { foodId: this.foodItem } })
})
}
}
```
2. 设置FoodDetail页面的FoodImageDisplay的Image组件的共享元素转场方法(sharedTransition)。设置方法同上。
```ts
import Curves from '@ohos.curves'
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
Text(this.foodItem.name)
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
}
}
```
![animation-feature1](figures/animation-feature1.gif)
通过对绘制组件和动画的学习,我们已完成了启动Logo的绘制、启动页动画和页面间的转场动画,声明式UI框架提供了丰富的动效接口,合理地应用和组合可以让应用更具有设计感。
# 构建食物分类Grid布局
健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。
1. 将Category枚举类型引入FoodCategoryList页面。
```ts
import { Category, FoodData } from '../model/FoodData'
```
2. 创建FoodCategoryList和FoodCategory组件,其中FoodCategoryList作为新的页面入口组件,在入口组件调用initializeOnStartup方法。
```ts
@Component
struct FoodList {
......@@ -19,7 +19,7 @@
......
}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
......@@ -27,7 +27,7 @@
......
}
}
@Entry
@Component
struct FoodCategoryList {
......@@ -39,13 +39,14 @@
```
3. 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if...else...。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack() {
if (this.showList) {
......@@ -59,13 +60,14 @@
```
4. 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
......@@ -85,14 +87,15 @@
}
```
5. 添加\@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加\@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。
5. 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加\@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
@State private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
......@@ -110,7 +113,7 @@
}.height('100%')
}
}
```
点击切换图标,FoodList组件出现,再次点击,FoodList组件消失。
......@@ -118,6 +121,7 @@
![zh-cn_image_0000001170411978](figures/zh-cn_image_0000001170411978.gif)
6. 创建显示所有食物的页签(All)。在FoodCategory组件内创建Tabs组件和其子组件TabContent,设置tabBar为All。设置TabBars的宽度为280,布局模式为Scrollable,即超过总长度后可以滑动。Tabs是一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图TabContent。
```ts
@Component
struct FoodCategory {
......@@ -137,13 +141,14 @@
![zh-cn_image_0000001204538065](figures/zh-cn_image_0000001204538065.png)
7. 创建FoodGrid组件,作为TabContent的子组件。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
......@@ -162,6 +167,7 @@
```
8. 实现2 \* 6的网格布局(一共12个食物数据资源)。创建Grid组件,设置列数columnsTemplate('1fr 1fr'),行数rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr'),行间距和列间距rowsGap和columnsGap为8。创建Scroll组件,使其可以滑动。
```ts
@Component
struct FoodGrid {
......@@ -185,6 +191,7 @@
```
9. 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) \* 6 - 8 = 1144。
```ts
@Component
struct FoodGridItem {
......@@ -196,7 +203,7 @@
.objectFit(ImageFit.Contain)
.height(152)
.width('100%')
}.backgroundColor('#FFf1f3f5')
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
......@@ -214,7 +221,7 @@
.width('100%')
}
}
@Component
struct FoodGrid {
private foodItems: FoodData[]
......@@ -239,123 +246,126 @@
}
```
![zh-cn_image_0000001170167520](figures/zh-cn_image_0000001170167520.gif)
![zh-cn_image_0000001170167520](figures/zh-cn_image_0000001170167520.png)
10. 创建展示蔬菜(Category.Vegetable)、水果(Category.Fruit)、坚果(Category.Nut)、海鲜(Category.SeaFood)和甜品(Category.Dessert)分类的页签。
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })
}.tabBar('Vegetable')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })
}.tabBar('Fruit')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })
}.tabBar('Nut')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })
}.tabBar('Seafood')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })
}.tabBar('Dessert')
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })
}.tabBar('Vegetable')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })
}.tabBar('Fruit')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })
}.tabBar('Nut')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })
}.tabBar('Seafood')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })
}.tabBar('Dessert')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
```
11. 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用'1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。
创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate : string = ''
private heightValue: number
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({foodItem: item})
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({left: 16, right: 16})
}
}
```
调用aboutToAppear接口计算行数(gridRowTemplate )和高度(heightValue)。
```ts
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
```
自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件销毁之前的时机执行。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![zh-cn_image_0000001215113569](figures/zh-cn_image_0000001215113569.png)
调用aboutToAppear接口计算行数(gridRowTemplate )和高度(heightValue)。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate : string = ''
private heightValue: number
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({foodItem: item})
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({left: 16, right: 16})
}
}
```
```ts
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
```
自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件销毁之前的时机执行。
![zh-cn_image_0000001215113569](figures/zh-cn_image_0000001215113569.png)
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![zh-cn_image_0000001170008198](figures/zh-cn_image_0000001170008198.gif)
![zh-cn_image_0000001170008198](figures/zh-cn_image_0000001170008198.gif)
\ No newline at end of file
# 构建食物列表List布局
使用List组件和ForEach循环渲染,构建食物列表布局。
1. 在pages目录新建页面FoodCategoryList.ets,将index.ets改名为FoodDetail.ets,并将其添加到config.json文件下的pages标签,位于第一序位的页面为首页。
```json
"js": [
{
"pages": [
"pages/FoodCategoryList",
"pages/FoodDetail"
],
]
```
1. 在pages目录新建页面FoodCategoryList.ets,将index.ets改名为FoodDetail.ets。
2. 新建FoodList组件作为页面入口组件,FoodListItem为其子组件。List组件是列表组件,适用于重复同类数据的展示,其子组件为ListItem,适用于展示列表中的单元。
```
```ts
@Component
struct FoodListItem {
build() {}
......@@ -43,7 +32,7 @@
```
4. FoodList和FoodListItem组件数值传递。在FoodList组件内创建类型为FoodData[]成员变量foodItems,调用initializeOnStartup方法为其赋值。在FoodListItem组件内创建类型为FoodData的成员变量foodItem。将父组件foodItems数组的第一个元素的foodItems[0]作为参数传递给FoodListItem。
```
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
......@@ -68,20 +57,64 @@
```
5. 声明子组件FoodListItem 的UI布局。创建Flex组件,包含食物图片缩略图,食物名称,和食物对应的卡路里。
```
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
![zh-cn_image_0000001204776353](figures/zh-cn_image_0000001204776353.png)
6. 创建两个FoodListItem。在List组件创建两个FoodListItem,分别给FoodListItem传递foodItems数组的第一个元素this.foodItems[0]和第二个元素foodItem: this.foodItems[1]。
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
......@@ -90,62 +123,21 @@
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
.margin({ right: 24, left: 32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
![zh-cn_image_0000001204776353](figures/zh-cn_image_0000001204776353.png)
6. 创建两个FoodListItem。在List组件创建两个FoodListItem,分别给FoodListItem传递foodItems数组的第一个元素this.foodItems[0]和第二个元素foodItem: this.foodItems[1]。
```
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
ListItem() {
FoodListItem({ foodItem: this.foodItems[1] })
}
......@@ -153,36 +145,16 @@
}
}
```
![zh-cn_image1_0000001204776353](figures/zh-cn_image1_0000001204776353.png)
7. 单独创建每一个FoodListItem肯定是不合理的,这就需要引入[ForEach循环渲染](../quick-start/arkts-rendering-control.md#循环渲染)
![zh-cn_image_0000001204537865](figures/zh-cn_image_0000001204537865.png)
7. 单独创建每一个FoodListItem肯定是不合理的。这就需要引入ForEach循环渲染,ForEach语法如下。
```
ForEach(
arr: any[], // Array to be iterated
itemGenerator: (item: any) => void, // child component generator
keyGenerator?: (item: any) => string // (optional) Unique key generator, which is recommended.
)
```
ForEach组有三个参数,第一个参数是需要被遍历的数组,第二个参数为生成子组件的lambda函数,第三个参数是键值生成器。出于性能原因,即使第三个参数是可选的,强烈建议开发者提供。keyGenerator使开发框架能够更好地识别数组更改,而不必因为item的更改重建全部节点。
遍历foodItems数组循环创建ListItem组件,foodItems中每一个item都作为参数传递给FoodListItem组件。
```
ForEach(this.foodItems, item => {
ListItem() {
FoodListItem({ foodItem: item })
}
}, item => item.id.toString())
```
整体的代码如下。
```
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
......@@ -191,8 +163,7 @@
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
......@@ -204,7 +175,7 @@
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
......@@ -220,22 +191,25 @@
}
}
```
8. 添加FoodList标题。
```
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
Column() {
Flex({justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center}) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text('Food List')
.fontSize(20)
.margin({ left:20 })
.margin({ left: 20 })
}
.height('7%')
.backgroundColor('#FFf1f3f5')
List() {
ForEach(this.foodItems, item => {
ListItem() {
......@@ -249,4 +223,4 @@
}
```
![zh-cn_image_0000001169678922](figures/zh-cn_image_0000001169678922.png)
![zh-cn_image_0000001169678922](figures/zh-cn_image_0000001169678922.png)
\ No newline at end of file
......@@ -2,12 +2,16 @@
在这一小节中,我们将开始食物详情页的开发,学习如何通过容器组件Stack、Flex和基础组件Image、Text,构建用户自定义组件,完成图文并茂的食物介绍。
在创建页面前,请先创建eTS工程,FA模型请参考[创建FA模型的eTS工程](../quick-start/start-with-ets-stage.md#创建ets工程),Stage模型请参考[创建Stage模型的eTS工程](..//quick-start/start-with-ets-fa.md#创建ets工程)
## 构建Stack布局
1. 创建食物名称。
删掉工程模板的build方法的代码,创建Stack组件,将Text组件放进Stack组件的花括号中,使其成为Stack组件的子组件。Stack组件为堆叠组件,可以包含一个或多个子组件,其特点是后一个子组件覆盖前一个子组件。
```
在index.ets文件中,创建Stack组件,将Text组件放进Stack组件的花括号中,使其成为Stack组件的子组件。Stack组件为堆叠组件,可以包含一个或多个子组件,其特点是后一个子组件覆盖前一个子组件。
```ts
@Entry
@Component
struct MyComponent {
......@@ -18,15 +22,15 @@
.fontWeight(500)
}
}
}
}
```
![zh-cn_image_0000001214128687](figures/zh-cn_image_0000001214128687.png)
2. 食物图片展示。
创建Image组件,指定Image组件的url,Image组件是必选构造参数组件。为了让Text组件在Image组件上方显示,所以要先声明Image组件。图片资源放在resources下的rawfile文件夹内,引用rawfile下资源时使用`$rawfile('filename')`的形式,filename为rawfile目录下的文件相对路径。当前`$rawfile`仅支持Image控件引用图片资源。
```
```ts
@Entry
@Component
struct MyComponent {
......@@ -42,30 +46,29 @@
```
![zh-cn_image_0000001168410342](figures/zh-cn_image_0000001168410342.png)
![zh-cn_image_0000001168410342](figures/zh-cn_image_0000001168410342.png)
3. 通过资源访问图片。
除指定图片路径外,也可以使用引用媒体资源符$r引用资源,需要遵循resources文件夹的资源限定词的规则。右键resources文件夹,点击New>Resource Directory,选择Resource Type为Media(图片资源)。
将Tomato.png放入media文件夹内。就可以通过`$r('app.type.name')`的形式引用应用资源,即`$r('app.media.Tomato')`
```
@Entry
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
```
4. 设置Image宽高,并且将image的objectFit属性设置为ImageFit.Contain,即保持图片长宽比的情况下,使得图片完整地显示在边界内。
如果Image填满了整个屏幕,原因如下:
......@@ -73,83 +76,53 @@
2. Image的objectFit默认属性是ImageFit.Cover,即在保持长宽比的情况下放大或缩小,使其填满整个显示边界。
```
@Entry
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
```
![zh-cn_image_0000001214210217](figures/zh-cn_image_0000001214210217.png)
![zh-cn_image_0000001214210217](figures/zh-cn_image_0000001214210217.png)
5. 设置食物图片和名称布局。设置Stack的对齐方式为底部起始端对齐,Stack默认为居中对齐。设置Stack构造参数alignContent为Alignment.BottomStart。其中Alignment和FontWeight一样,都是框架提供的内置枚举类型。
```
@Entry
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![zh-cn_image_0000001168728872](figures/zh-cn_image_0000001168728872.png)
6. 通过设置Stack的背景颜色来改变食物图片的背景颜色,设置颜色有四种方式:
1. 通过框架提供的Color内置枚举值来设置,比如backgroundColor(Color.Red),即设置背景颜色为红色。
2. string类型参数,支持的颜色格式有:rgb、rgba和HEX颜色码。比如backgroundColor('\#0000FF'),即设置背景颜色为蓝色,backgroundColor('rgb(255, 255, 255)'),即设置背景颜色为白色。
3. number类型参数,支持十六进制颜色值。比如backgroundColor(0xFF0000),即设置背景颜色为红色。
4. Resource类型参数请参考[资源访问](ts-resource-access.md)
```
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
.backgroundColor('#FFedf2f5')
}
}
```
```
![zh-cn_image_0000001168728872](figures/zh-cn_image_0000001168728872.png)
![zh-cn_image_0000001168888822](figures/zh-cn_image_0000001168888822.png)
6. 调整Text组件的外边距margin,使其距离左侧和底部有一定的距离。margin是简写属性,可以统一指定四个边的外边距,也可以分别指定。具体设置方式如下:
7. 调整Text组件的外边距margin,使其距离左侧和底部有一定的距离。margin是简写属性,可以统一指定四个边的外边距,也可以分别指定。具体设置方式如下:
1. 参数为Length时,即统一指定四个边的外边距,比如margin(20),即上、右、下、左四个边的外边距都是20。
2. 参数为{top?: Length, right?: Length, bottom?: Length, left?:Length},即分别指定四个边的边距,比如margin({ left: 26, bottom: 17.4 }),即左边距为26,下边距为17.4。
```
@Entry
```ts
@Entry
@Component
struct MyComponent {
build() {
......@@ -161,20 +134,19 @@
.fontSize(26)
.fontWeight(500)
.margin({left: 26, bottom: 17.4})
}
.backgroundColor('#FFedf2f5')
}
}
}
```
```
![zh-cn_image_0000001213968747](figures/zh-cn_image_0000001213968747.png)
![zh-cn_image_0000001213968747](figures/zh-cn_image_0000001213968747.png)
7. 调整组件间的结构,语义化组件名称。创建页面入口组件为FoodDetail,在FoodDetail中创建Column,设置水平方向上居中对齐 alignItems(HorizontalAlign.Center)。MyComponent组件名改为FoodImageDisplay,为FoodDetail的子组件。
8. 调整组件间的结构,语义化组件名称。创建页面入口组件为FoodDetail,在FoodDetail中创建Column,设置水平方向上居中对齐 alignItems(HorizontalAlign.Center)。MyComponent组件名改为FoodImageDisplay,为FoodDetail的子组件。
Column是子组件竖直排列的容器组件,本质为线性布局,所以只能设置交叉轴方向的对齐。
```
@Component
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
......@@ -185,11 +157,10 @@
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
.backgroundColor('#FFedf2f5')
.height(357)
}
}
@Entry
@Component
struct FoodDetail {
......@@ -200,9 +171,7 @@
.alignItems(HorizontalAlign.Center)
}
}
```
```
## 构建Flex布局
......@@ -210,8 +179,8 @@
1. 创建ContentTable组件,使其成为页面入口组件FoodDetail的子组件。
```
@Component
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
......@@ -223,15 +192,15 @@
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.backgroundColor('#FFedf2f5')
}
}
@Component
struct ContentTable {
build() {}
build() {
}
}
@Entry
@Component
struct FoodDetail {
......@@ -243,8 +212,7 @@
.alignItems(HorizontalAlign.Center)
}
}
```
```
2. 创建Flex组件展示Tomato两类成分。
一类是热量Calories,包含卡路里(Calories);一类是营养成分Nutrition,包含蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)。
......@@ -253,8 +221,8 @@
已省略FoodImageDisplay代码,只针对ContentTable进行扩展。
```
@Component
```ts
@Component
struct ContentTable {
build() {
Flex() {
......@@ -270,7 +238,7 @@
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
......@@ -282,15 +250,15 @@
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001169759552](figures/zh-cn_image_0000001169759552.png)
```
![zh-cn_image_0000001169759552](figures/zh-cn_image_0000001169759552.png)
3. 调整布局,设置各部分占比。分类名占比(layoutWeight)为1,成分名和成分含量一共占比(layoutWeight)2。成分名和成分含量位于同一个Flex中,成分名占据所有剩余空间flexGrow(1)。
```
@Component
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
......@@ -301,11 +269,10 @@
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.backgroundColor('#FFedf2f5')
}
}
}
@Component
struct ContentTable {
build() {
......@@ -327,7 +294,7 @@
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
......@@ -339,16 +306,15 @@
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001215079443](figures/zh-cn_image_0000001215079443.png)
```
![zh-cn_image_0000001215079443](figures/zh-cn_image_0000001215079443.png)
4. 仿照热量分类创建营养成分分类。营养成分部分(Nutrition)包含:蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)四个成分,后三个成分在表格中省略分类名,用空格代替。
设置外层Flex为竖直排列FlexDirection.Column, 在主轴方向(竖直方向)上等距排列FlexAlign.SpaceBetween,在交叉轴方向(水平轴方向)上首部对齐排列ItemAlign.Start。
```
@Component
```ts
@Component
struct ContentTable {
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
......@@ -427,7 +393,7 @@
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
......@@ -439,8 +405,7 @@
.alignItems(HorizontalAlign.Center)
}
}
```
```
5. 使用自定义构造函数\@Builder简化代码。可以发现,每个成分表中的成分单元其实都是一样的UI结构。
![zh-cn_image_0000001169599582](figures/zh-cn_image_0000001169599582.png)
......@@ -449,8 +414,8 @@
在ContentTable内声明\@Builder修饰的IngredientItem方法,用于声明分类名、成分名称和成分含量UI描述。
```
@Component
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
......@@ -469,13 +434,12 @@
}
}
}
```
```
在ContentTable的build方法内调用IngredientItem接口,需要用this去调用该Component作用域内的方法,以此来区分全局的方法调用。
在ContentTable的build方法内调用IngredientItem接口,需要用this去调用该Component作用域内的方法,以此来区分全局的方法调用。
```
@Component
```ts
@Component
struct ContentTable {
......
build() {
......@@ -490,13 +454,12 @@
.padding({ top: 30, right: 30, left: 30 })
}
}
```
```
ContentTable组件整体代码如下。
ContentTable组件整体代码如下。
```
@Component
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
......@@ -514,21 +477,20 @@
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
......@@ -540,10 +502,9 @@
.alignItems(HorizontalAlign.Center)
}
}
```
```
![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png)
![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png)
通过学习Stack布局和Flex布局已完成食物的图文展示和营养成分表,构建出第一个普通视图的食物详情页。在下一个章节中,将开发食物分类列表页,并完成食物分类列表页面和食物详情页面的跳转和数据传递,现在我们就进入下一个章节的学习吧。
......
# 绘制图形
绘制能力主要是通过框架提供的绘制组件来支撑,支持svg标准绘制命令。
本节主要学习如何使用绘制组件,绘制详情页食物成分标签(基本几何图形)和应用Logo(自定义图形)。
## 绘制基本几何图形
绘制组件封装了一些常见的基本几何图形,比如矩形Rect、圆形Circle、椭圆形Ellipse等,为开发者省去了路线计算的过程。
FoodDetail页面的食物成分表里,给每一项成分名称前都加上一个圆形的图标作为成分标签。
1. 创建Circle组件,在每一项含量成分前增加一个圆形图标作为标签。设置Circle的直径为 6vp。修改FoodDetail页面的ContentTable组件里的IngredientItem方法,在成分名称前添加Circle。
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
......
}
}
```
2. 每个成分的标签颜色不一样,所以我们在build方法中,调用IngredientItem,给每个Circle填充不一样的颜色。
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal')
this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g')
this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g')
this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
```
![drawing-feature](figures/drawing-feature.png)
## 绘制自定义几何图形
除绘制基础几何图形,开发者还可以使用Path组件来绘制自定义的路线,下面进行绘制应用的Logo图案。
1. 在pages文件夹下创建新的eTS页面Logo.ets。
![drawing-feature1](figures/drawing-feature1.png)
2. Logo.ets中删掉模板代码,创建Logo Component。
```ts
@Entry
@Component
struct Logo {
build() {
}
}
```
3. 创建Flex组件为根节点,宽高设置为100%,设置其在主轴方向和交叉轴方向的对齐方式都为Center,创建Shape组件为Flex子组件。
Shape组件是所有绘制组件的父组件。如果需要组合多个绘制组件成为一个整体,需要创建Shape作为其父组件。
我们要绘制的Logo的大小630px * 630px。声明式UI范式支持多种长度单位的设置,在前面的章节中,我们直接使用number作为参数,即采用了默认长度单位vp,虚拟像素单位。vp和设备分辨率以及屏幕密度有关。比如设备分辨率为1176 * 2400,屏幕基准密度(resolution)为3,vp = px / resolution,则该设备屏幕宽度是392vp。
但是绘制组件采用svg标准,默认采取px为单位的,为方便统一,在这绘制Logo这一部分,统一采取px为单位。声明式UI框架同样也支持px单位,入参类型为string,设置宽度为630px,即210vp,设置方式为width('630px')或者width(210)。
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
}
}
```
4. 给页面填充渐变色。设置为线性渐变,偏移角度为180deg,三段渐变 #BDE895 -->95DE7F --> #7AB967,其区间分别为[0, 0.1], (0.1, 0.6], (0.6, 1]。
```ts
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
```
![drawing-feature2](figures/drawing-feature2.png)
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature3](figures/drawing-feature3.png)
5. 绘制第一条路线Path,设置其绘制命令。
```ts
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
```
Path的绘制命令采用svg标准,上述命令可分解为:
```ts
M162 128.7
```
将笔触移动到(Moveto)坐标点(162, 128.7)。
```ts
a222 222 0 0 1 100.8 374.4
```
画圆弧线(elliptical arc)半径rx,ry为222,x轴旋转角度x-axis-rotation为0,角度大小large-arc-flag为0,即小弧度角,弧线方向(sweep-flag)为1,即逆时针画弧线,小写a为相对位置,即终点坐标为(162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1)。
```ts
H198
```
画水平线(horizontal lineto)到198,即画(262.8, 503.1)到(198, 503.1)的水平线。
```ts
a36 36 0 0 3 -36 -36
```
画圆弧线(elliptical arc),含义同上,结束点为(198 - 36 = 162, 503.1 - 36 = 467.1)。
```ts
V128.7
```
画垂直线(vertical lineto)到128.7,即画(162, 467.1)到(162, 128.7)的垂直线。
```ts
z
```
关闭路径(closepath)。
![drawing-feature4](figures/drawing-feature4.png)
填充颜色为白色。
```ts
.fill(Color.White)
```
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature5](figures/drawing-feature5.png)
6. 在Shape组件内绘制第二个Path。第二条Path的背景色为渐变色,但是渐变色的填充是其整体的box,所以需要clip将其裁剪,入参为Shape,即按照Shape的形状进行裁剪。
```ts
Path()
.commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')
.fill('none')
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'))
```
Path的绘制命令比较长,可以将其作为组件的成员变量,通过this调用。
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
build() {
......
Path()
.commands(this.pathCommands1)
.fill('none')
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
......
}
}
```
![drawing-feature6](figures/drawing-feature6.png)
7. 在Shape组件内绘制第二个Path。
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
Path()
.commands(this.pathCommands1)
.fill('none')
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature7](figures/drawing-feature7.png)
完成应用Logo的绘制。Shape组合了三个Path组件,通过svg命令绘制出一个艺术的叶子,寓意绿色健康饮食方式。
8. 添加应用的标题和slogan。
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
Path()
.commands(this.pathCommands1)
.fill('none')
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top:300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top:4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature8](figures/drawing-feature8.png)
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册