制作大风车动画

发布于:2025-05-18 ⋅ 阅读:(22) ⋅ 点赞:(0)

这个案例的风车旋转应用了图形变换来实现,速度和缩放比例应用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。

视频:《大风车吱扭扭的转》。


网站公告

今日签到

点亮在社区的每一天
去签到