制作大风车动画
这个案例的风车旋转应用了图形变换来实现,速度和缩放比例应用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 = 14static readonly LAYOUT_WEIGHT = 1static readonly PERCENTAGE_100 = '100%'static readonly DELAY_TIME = 15static readonly SLIDER_SKIN = $r('app.color.slider_color')static readonly INTERVAL = 0static readonly SPEED = 5static readonly WEIGHT_BLANK_IMAGE = '25%'static readonly PANEL_MARGIN_TOP = '4%'static readonly PANEL_MARGIN_BOTTOM = '5%'static readonly IMAGE_SIZE = 150static readonly ANGLE = 0static readonly IMAGE_SIZE_INITIAL = 1static readonly FRACTION_DIGITS = 1static readonly TITLE_PADDING = 5static readonly TITLE_MARGIN_HORIZONTAL = 10static readonly SPEED_MARGIN_BOTTOM = 6static readonly SLIDER_MARGIN_HORIZONTAL = 11static readonly PANEL_RADIUS = 24static readonly PANEL_IMAGE_WIDTH = 19static readonly PANEL_IMAGE_HEIGHT = 16static readonly PANEL_IMAGE_BIG_HEIGHT = 18static readonly PANEL_IMAGE_BIG_WIDTH = 22static readonly PANEL_WIDTH = '98%'static readonly PANEL_FONT_SIZE = 20static readonly PANEL_END_FONT_SIZE = 24static readonly PANEL_HOLDER = 'A'static readonly PANEL_HEIGHT = 100static readonly PANEL_PADDING = 10static readonly PANEL_MARGIN = 10static readonly MIN: number = 0.5static readonly MAX: number = 2.5static 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.ANGLEprivate interval: number = Constants.INTERVALbuild() {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 = valueclearInterval(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 = thisthis.angle = Constants.ANGLEthis.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?: Resourcemode?: SliderModeoptions?: SliderOptionscallback: (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.ANGLEprivate interval: number = Constants.INTERVALbuild() {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 = valueclearInterval(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 = thisthis.angle = Constants.ANGLEthis.interval = setInterval(() => {that.angle += that.speed}, Constants.DELAY_TIME)}onPageShow() {clearInterval(this.interval)this.speedChange()}
}
7. 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-05-02.zip。
视频:《大风车吱扭扭的转》。