这个案例的风车旋转应用了图形变换来实现,速度和缩放比例应用slider来实现,其中图片的速度,图片大小的信息通过@State来定义变量管理,速度和和缩放比例的即时的值通过@Prop来管理。
1. 案例效果截图
2. 案例运用到的知识点
2.1. 核心知识点
- Text组件:文本组件,用于呈现一段信息。
- Image组件:图片组件,用来渲染展示图片。
- Slider组件:滑动条组件,用来快速调节设置值,如音量、亮度等。
2.2. 其他知识点
- ArkTS语言基础
- 自定义组件和组件生命周期
- V1状态管理:@State/@Prop
- 内置组件:Column/Image/Text/Row/Stack/Blank/Button
- 常量与资源分类的访问
3. 代码结构
├──entry/src/main/ets // 代码区
│ ├──common
│ │ └──Constants.ets // 常量
│ ├──entryability
│ │ └──EntryAbility.ts // 应用的入口
│ ├──pages
│ │ └──SliderPage.ets // 入口页面
│ └──view
│ └──PanelComponent.ets // 自定义组件
└──entry/src/main/resources // 资源文件目录
4. 公共文件与资源
本案例涉及到的常量类和工具类代码如下:
4.1. 通用常量类
// entry/src/main/ets/common/Constant.ets
export enum RotatePosition {
X = 0,
Y = 0,
Z = 1,
}
export enum SliderSpeed {
MIN = 1,
MAX = 10,
STEP = 1,
}
export enum SliderMode {
SPEED = 1,
SCALE = 2,
}
export class Constants {
static readonly FONT_SIZE = 14
static readonly LAYOUT_WEIGHT = 1
static readonly PERCENTAGE_100 = '100%'
static readonly DELAY_TIME = 15
static readonly SLIDER_SKIN = $r('app.color.slider_color')
static readonly INTERVAL = 0
static readonly SPEED = 5
static readonly WEIGHT_BLANK_IMAGE = '25%'
static readonly PANEL_MARGIN_TOP = '4%'
static readonly PANEL_MARGIN_BOTTOM = '5%'
static readonly IMAGE_SIZE = 150
static readonly ANGLE = 0
static readonly IMAGE_SIZE_INITIAL = 1
static readonly FRACTION_DIGITS = 1
static readonly TITLE_PADDING = 5
static readonly TITLE_MARGIN_HORIZONTAL = 10
static readonly SPEED_MARGIN_BOTTOM = 6
static readonly SLIDER_MARGIN_HORIZONTAL = 11
static readonly PANEL_RADIUS = 24
static readonly PANEL_IMAGE_WIDTH = 19
static readonly PANEL_IMAGE_HEIGHT = 16
static readonly PANEL_IMAGE_BIG_HEIGHT = 18
static readonly PANEL_IMAGE_BIG_WIDTH = 22
static readonly PANEL_WIDTH = '98%'
static readonly PANEL_FONT_SIZE = 20
static readonly PANEL_END_FONT_SIZE = 24
static readonly PANEL_HOLDER = 'A'
static readonly PANEL_HEIGHT = 100
static readonly PANEL_PADDING = 10
static readonly PANEL_MARGIN = 10
static readonly MIN: number = 0.5
static readonly MAX: number = 2.5
static readonly STEP: number = 0.1
}
本案例涉及到的资源文件如下:
4.2. string.json
// entry/src/main/resources/base/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "scale_text",
"value": "缩放比例"
},
{
"name": "speed_text",
"value": "速度"
}
]
}
4.3. color.json
// entry/src/main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "white",
"value": "#FFFFFF"
},
{
"name": "slider_color",
"value": "#007dff"
},
{
"name": "background_color",
"value": "#F1F3F5"
},
{
"name": "font_color",
"value": "#182431"
}
]
}
其他资源请到源码中获取。
5. 单个页面扁平实现
// entry/src/main/ets/pages/Index.ets
import {
Constants, RotatePosition, SliderMode, SliderSpeed
} from '../common/Constants'
@Entry
@Component
struct Index {
@State private speed: number = Constants.SPEED
@State private imageSize: number = Constants.IMAGE_SIZE_INITIAL
@State private angle: number = Constants.ANGLE
private interval: number = Constants.INTERVAL
build() {
Column() {
Image($rawfile('windmill.png'))
.objectFit(ImageFit.Contain)
.height(Constants.IMAGE_SIZE)
.width(Constants.IMAGE_SIZE)
.rotate({
x: RotatePosition.X,
y: RotatePosition.Y,
z: RotatePosition.Z,
angle: this.angle
})
.scale({ x: this.imageSize, y: this.imageSize })
.margin({ bottom: Constants.WEIGHT_BLANK_IMAGE })
Column() {
Text($r('app.string.speed_text'))
.width(Constants.PANEL_WIDTH)
.padding({ left: Constants.TITLE_PADDING })
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({
left: Constants.TITLE_MARGIN_HORIZONTAL,
right: Constants.TITLE_MARGIN_HORIZONTAL
})
Column() {
Text(this.speed.toFixed(Constants.FRACTION_DIGITS))
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
Row() {
Image($rawfile('speedLow.png'))
.objectFit(ImageFit.Contain)
.height(Constants.PANEL_IMAGE_HEIGHT)
.width(Constants.PANEL_IMAGE_WIDTH)
Slider({
value: this.speed,
min: SliderSpeed.MIN,
max: SliderSpeed.MAX,
step: SliderSpeed.STEP,
style: SliderStyle.InSet
})
.layoutWeight(Constants.LAYOUT_WEIGHT)
.selectedColor(Constants.SLIDER_SKIN)
.onChange((value: number) => {
this.speed = value
clearInterval(this.interval)
this.speedChange()
})
.margin({
left: Constants.SLIDER_MARGIN_HORIZONTAL,
right: Constants.SLIDER_MARGIN_HORIZONTAL
})
Image($rawfile('speed.png'))
.objectFit(ImageFit.Contain)
.height(Constants.PANEL_IMAGE_BIG_HEIGHT)
.width(Constants.PANEL_IMAGE_BIG_WIDTH)
.height(Constants.PANEL_IMAGE_BIG_HEIGHT)
.width(Constants.PANEL_IMAGE_BIG_WIDTH)
}
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(Constants.PANEL_RADIUS)
.height(Constants.PANEL_HEIGHT)
.width(Constants.PANEL_WIDTH)
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.margin({
top: Constants.PANEL_MARGIN,
bottom: Constants.PANEL_MARGIN
})
}
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.width(Constants.PERCENTAGE_100)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
Column() {
Text($r('app.string.scale_text'))
.width(Constants.PANEL_WIDTH)
.padding({ left: Constants.TITLE_PADDING })
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({
left: Constants.TITLE_MARGIN_HORIZONTAL,
right: Constants.TITLE_MARGIN_HORIZONTAL
})
Column() {
Text(this.imageSize.toFixed(Constants.FRACTION_DIGITS))
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
Row() {
Text(Constants.PANEL_HOLDER)
.fontSize(Constants.PANEL_FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
Slider({
value: this.imageSize,
min: Constants.MIN,
max: Constants.MAX,
step: Constants.STEP,
style: SliderStyle.InSet
})
.layoutWeight(Constants.LAYOUT_WEIGHT)
.selectedColor(Constants.SLIDER_SKIN)
.onChange((value: number) => {
this.imageSize = value
})
.margin({
left: Constants.SLIDER_MARGIN_HORIZONTAL,
right: Constants.SLIDER_MARGIN_HORIZONTAL
})
Text(Constants.PANEL_HOLDER)
.fontSize(Constants.PANEL_END_FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
}
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(Constants.PANEL_RADIUS)
.height(Constants.PANEL_HEIGHT)
.width(Constants.PANEL_WIDTH)
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.margin({
top: Constants.PANEL_MARGIN,
bottom: Constants.PANEL_MARGIN
})
}
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.width(Constants.PERCENTAGE_100)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
.justifyContent(FlexAlign.End)
.height(Constants.PERCENTAGE_100)
.width(Constants.PERCENTAGE_100)
.backgroundColor($r('app.color.background_color'))
}
speedChange(): void {
let that = this
this.angle = Constants.ANGLE
this.interval = setInterval(() => {
that.angle += that.speed
}, Constants.DELAY_TIME)
}
onPageShow() {
clearInterval(this.interval)
this.speedChange()
}
}
6. 组件抽离实现
6.1. 面板组件
// entry/src/main/ets/views/PanelComponent.ets
import { Constants, SliderMode } from '../common/Constants'
@Component
export struct PanelComponent {
@Prop text: string = ''
title?: Resource
mode?: SliderMode
options?: SliderOptions
callback: (value: number, mode: SliderChangeMode) => void = () => {}
build() {
Column() {
Text(this.title)
.width(Constants.PANEL_WIDTH)
.padding({ left: Constants.TITLE_PADDING })
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({
left: Constants.TITLE_MARGIN_HORIZONTAL,
right: Constants.TITLE_MARGIN_HORIZONTAL
})
Column() {
Text(this.text)
.fontSize(Constants.FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
Row() {
if (this.mode === SliderMode.SPEED) {
Image($rawfile('speedLow.png'))
.objectFit(ImageFit.Contain)
.height(Constants.PANEL_IMAGE_HEIGHT)
.width(Constants.PANEL_IMAGE_WIDTH)
} else {
Text(Constants.PANEL_HOLDER)
.fontSize(Constants.PANEL_FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
}
Slider(this.options)
.layoutWeight(Constants.LAYOUT_WEIGHT)
.selectedColor(Constants.SLIDER_SKIN)
.onChange((value: number, mode: SliderChangeMode) => {
this.callback(value, mode);
})
.margin({
left: Constants.SLIDER_MARGIN_HORIZONTAL,
right: Constants.SLIDER_MARGIN_HORIZONTAL
})
if (this.mode === SliderMode.SPEED) {
Image($rawfile('speed.png'))
.objectFit(ImageFit.Contain)
.height(Constants.PANEL_IMAGE_BIG_HEIGHT)
.width(Constants.PANEL_IMAGE_BIG_WIDTH)
.height(Constants.PANEL_IMAGE_BIG_HEIGHT)
.width(Constants.PANEL_IMAGE_BIG_WIDTH)
} else {
Text(Constants.PANEL_HOLDER)
.fontSize(Constants.PANEL_END_FONT_SIZE)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.font_color'))
.margin({ bottom: Constants.SPEED_MARGIN_BOTTOM })
}
}
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(Constants.PANEL_RADIUS)
.height(Constants.PANEL_HEIGHT)
.width(Constants.PANEL_WIDTH)
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.margin({
top: Constants.PANEL_MARGIN,
bottom: Constants.PANEL_MARGIN
})
}
.padding({
left: Constants.PANEL_PADDING,
right: Constants.PANEL_PADDING
})
.width(Constants.PERCENTAGE_100)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
6.2. 页面改造
// entry/src/main/ets/pages/Index.ets
import {
Constants, RotatePosition, SliderMode, SliderSpeed
} from '../common/Constants'
import { PanelComponent } from '../views/PanelComponent'
@Entry
@Component
struct SliderPage {
@State private speed: number = Constants.SPEED
@State private imageSize: number = Constants.IMAGE_SIZE_INITIAL
@State private angle: number = Constants.ANGLE
private interval: number = Constants.INTERVAL
build() {
Column() {
Image($rawfile('windmill.png'))
.objectFit(ImageFit.Contain)
.height(Constants.IMAGE_SIZE)
.width(Constants.IMAGE_SIZE)
.rotate({
x: RotatePosition.X,
y: RotatePosition.Y,
z: RotatePosition.Z,
angle: this.angle
})
.scale({ x: this.imageSize, y: this.imageSize })
.margin({ bottom: Constants.WEIGHT_BLANK_IMAGE })
PanelComponent({
mode: SliderMode.SPEED,
title: $r('app.string.speed_text'),
text: this.speed.toFixed(Constants.FRACTION_DIGITS),
callback: ((value: number) => {
this.speed = value
clearInterval(this.interval)
this.speedChange()
}),
options: {
value: this.speed,
min: SliderSpeed.MIN,
max: SliderSpeed.MAX,
step: SliderSpeed.STEP,
style: SliderStyle.InSet
}
})
PanelComponent({
mode: SliderMode.SCALE,
title: $r('app.string.scale_text'),
text: this.imageSize.toFixed(Constants.FRACTION_DIGITS),
callback: ((value: number) => {
this.imageSize = value
}),
options: {
value: this.imageSize,
min: Constants.MIN,
max: Constants.MAX,
step: Constants.STEP,
style: SliderStyle.InSet
}
})
.margin({
bottom: Constants.PANEL_MARGIN_BOTTOM,
top: Constants.PANEL_MARGIN_TOP
})
}
.justifyContent(FlexAlign.End)
.height(Constants.PERCENTAGE_100)
.backgroundColor($r('app.color.background_color'))
}
speedChange(): void {
let that = this
this.angle = Constants.ANGLE
this.interval = setInterval(() => {
that.angle += that.speed
}, Constants.DELAY_TIME)
}
onPageShow() {
clearInterval(this.interval)
this.speedChange()
}
}
7. 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-05-02.zip。
视频:《大风车吱扭扭的转》。