分享之前使用HarmonyOS NEXT Canvas做的动态GIF视频的一个案例,没有感情,全是技术。

陈杨
• 阅读 19

theme: fancy

hello,大家好,我是莓创-陈杨。最近忙着改图表组件的BUG,还有定制化开发一些图表。没啥时间写新东西,草稿里面放了十几个要实现的案例分享,欠的实在太多了,后面再慢慢还吧。这次分享一下之前使用HarmonyOS NEXT Canvas做的动态视频的一个案例,没有感情,全是技术。

什么!你还不知道我封装了什么图表组件,我不允许你不知道,还不快去看看:莓创开源图表快速地址

效果

先给大家看一下整体效果

分享之前使用HarmonyOS NEXT Canvas做的动态GIF视频的一个案例,没有感情,全是技术。

开发准备

开发流程与进度

这次整体开发流程主要如下:

  1. 获取图片素材列表数据,初始化视频的帧数以及canvas画布
  2. 绘画视频控制器,编写视频按帧数播放的功能
  3. 动态切换帧数进行播放
  4. 支持播放词条进行控制播放
  5. 添加音乐
  6. 导出视频

目前已经开发完第三步了,后面会继续开发,而且也会继续分享出来。感兴趣的开发可以关注一下。

代码讲解

接下来我简单讲解一下代码,也是需要注意点

1、获取图片素材列表,大家想要生成什么GIF或者视频就去找什么素材。可以用第三方的链接,可以用base64,可以用本地项目图片。最后都通过ImageBitmap方法将图片存储为canvas渲染的像素数据。非常方面,在H5还要担心跨域之类的问题。

let playTimer: number = 0
@Entry
@Component
export struct VideoEditing {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private scroller: Scroller = new Scroller()
  @State w: number = 400; // 编程画板的宽度
  @State h: number = 760; // 编程画板的尺寸
  @State levelList: any[] = [
    new ImageBitmap('common/images/icon0.png'),
    new ImageBitmap('common/images/icon1.png'),
    new ImageBitmap('common/images/icon2.png'),
    new ImageBitmap('common/images/icon3.png'),
    new ImageBitmap('common/images/icon4.png'),
    new ImageBitmap('common/images/icon5.png'),
    new ImageBitmap('common/images/icon6.png'),
    new ImageBitmap('common/images/icon7.png')
    ....
  ];

  build() {
    Flex({direction: FlexDirection.Column, alignItems: ItemAlign.Start}) {
      Column() {
        Canvas(this.context)
          .onReady(() => {
          })
          .width(this.w)
          .height(this.h)
          .backgroundColor('#fff')
      }.justifyContent(FlexAlign.Center).clip(true).width('100%').flexGrow(1).backgroundColor('#f8f8f8')
    }
  }
}

2、绘画视频控制器主体,这里主要控制变量就是视频的帧数fps,再结合循环计时器形成一个小型播放器,循环器的时间规则就是1000 / fps,代表着每秒几帧,想快就放大fps,想慢就缩小fps,是不是很简单。

let playTimer: number = 0
@Entry
@Component
export struct VideoEditing {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private scroller: Scroller = new Scroller()
  @State w: number = 400; // 编程画板的宽度
  @State h: number = 760; // 编程画板的尺寸
  @State levelList: any[] = [
    ....
  ]; // 图层list
  @State ispause: boolean = true // 是否是暂停状态
  @State plusNum: number = 0 // 帧总量
  @State plusCount: number = 0 // 帧总量计数器(判断循环次数)
  @State count: number = 0 // 当前帧
  @State fps: number = 25 // 25帧/秒
  @State fpsNumber: number = 2 // 25帧/秒
  @State recordFrom: number = 0 // 记录起始帧
  @State recordTo: number = 0 // 记录结束帧
  @State imgsLen: number = 0 // 记录帧长度

  // 跳到某一帧
  goto (n: number) {
    this.count = n
    this.drawImg(this.levelList[n])
  }
  drawImg(img) {
    // const image = offCanvas.transferToImageBitmap()
    // this.context.transferFromImageBitmap(image)
    this.context.drawImage(img,0,0,this.w, this.h)
  }
  fromTo(from: number, to: number) {
    const self = this
    const fps = this.fps
    // 先清除上次未执行完的动画
    clearInterval(playTimer)
    const timeFn = (): undefined => {
      if (self.ispause) {
        return
      }
      // 当总量计数器达到帧总量的时候退出
      if (self.plusNum <= self.plusCount) {
        self.resetData()
        // clearInterval(playTimer)
        return
      } else {
        // 未达到,继续循环
        // 帧计数器
        self.count++
        // 一次循环结束,重置keyCount为from
        if (self.count > to) {
          self.count = from
        }
        this.scroller.scrollTo({ xOffset: self.count * 150, yOffset: 0 })
        self.goto(self.count)
        // 总量计数器
        self.plusCount++
        return
      }
    }
    // 总量计数器
    this.plusCount = 0

    // 帧总量 帧数*循环次数first
    this.plusNum = to - from + 1
    this.ispause = false

    this.recordFrom = from
    this.recordTo = to

    timeFn()
    playTimer = setInterval(timeFn, 1000 / fps)
  }
  // 重置数据 停止并回到第一帧或cover帧
  resetData() {
    this.ispause = true
    clearInterval(playTimer)
    this.plusNum = 0
    this.plusCount = 0
    this.scroller.scrollTo({ xOffset: 0, yOffset: 0 })
    // 重置记录
    this.recordFrom = 0
    this.recordTo = this.imgsLen - 1
    this.count = 0
  }
  build() {
    Flex({direction: FlexDirection.Column, alignItems: ItemAlign.Start}) {
      Column() {
        Canvas(this.context)
          .onReady(() => {
            this.goto(0)
          })
          .width(this.w)
          .height(this.h)
          .backgroundColor('#fff')
      }.justifyContent(FlexAlign.Center).clip(true).width('100%').flexGrow(1).backgroundColor('#f8f8f8')
      Column() {
        Flex({justifyContent: FlexAlign.SpaceBetween}) {
          Row() {
            Text(String(this.fps)).fontSize(15).margin({right: 4})
            Text('帧/秒').fontSize(12)
            Image($r('app.media.ic_public_spinner_small')).width(20)
          }.onClick(() => {
            // this.showSex = true
            TextPickerDialog.show({
              range:  ['1', '12', '25', '30', '50', '60'],
              selected: this.fpsNumber,
              // selectedTextStyle: {color:'rgba(255, 80, 121, 1)'},
              onAccept: (value: TextPickerResult) => {
                this.fpsNumber = Number(value.index)
                this.fps = Number(value.value)
              }
            })
          })
          Row() {
            Image($r('app.media.ic_public_play')).width(20)
              .onClick(() => {
                this.imgsLen = this.levelList.length
                this.recordFrom = 0
                this.recordTo = this.imgsLen - 1
                this.fromTo(this.recordFrom, this.recordTo)
              })
            Image($r('app.media.ic_public_pause')).width(20)
          }
          Row() {
            Image($r('app.media.ic_public_music_filled')).width(20)
          }
        }.padding({top: 20, bottom: 20, left: 10, right: 10})
      }
    }.position({x: 0, y: 0}).width('100%').height('100%').backgroundColor('#fff').transition({ type: TransitionType.Insert, translate: { x: 0, y: '100%' } }).transition({ type: TransitionType.Delete, translate: { x: 0, y: '100%' } })
  }
}

以上就是前三步的实现代码,这三个步骤整体并不难。在页面能够实现简单的播放之后,后面就是生成视频或者GIF了。其实还有一个功能也很重要,就是导入视频,解析视频,然后就可以做视频编辑器了,这个也是一个大工程,想玩的可以去尝试尝试

点赞
收藏
评论区
推荐文章
徐小夕 徐小夕
3年前
从零搭建一款PC页面编辑器PC-Dooring
之前一直忙着调研lowcode平台和开发以下两个项目:H5编辑器,可视化大屏编辑器没有太多时间做PC端搭建化项目,好在搭建平台很多原理都是通用的,所以早在去年我就开发好了面向PC端的编辑器,虽然在设计上还有些不足(在后面的内容中会提到),但是基本模型已经实现,接下来就和大家一起分享一下具体的实现.为了保证文章整体的逻辑性和条理性,我将可
陈杨 陈杨
4天前
开源啦!!!基于鸿蒙ArkTS封装的图表组件《McCharts》,大家快来一起共创
Hello;大家好,我是陈杨。好久没更新了,首先是自己本职工作比较忙,基本没时间写作。其次就是学习技术,自学鸿蒙ArkTS语言已经接近半年了,也算半路出师了,这次将分享我封装的组件库,所以有啥讲错的地方请大家高抬贵手,宽容一下,谢谢。这次主要是给大家带来一
陈杨 陈杨
4天前
【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图
简介大家好,我是陈杨。今天主要是分享一下McCharts组件库中的折线图实现过程,记录并分享自己的一些开发经验,感兴趣的可以互相学习。McCharts组件库是基于鸿蒙ArkTS语法开发,支持API9以上的版本。图表组件已经开源了,大家可以一起参与进来,不管
陈杨 陈杨
4天前
McCharts 2.0来了,完美适配HarmonyOS NEXT最新版本,可轻松迁移Echarts图表项目
大家好,我是陈杨。终于有时间来分享一些技术文章了,自从McCharts组件上线第一期之后,就开始忙碌鸿蒙创新赛与极客马拉松比赛。在比赛的过程一直收到很多Issues,但是由于腾不出时间来维护,导致大家以为我们不维护了。在这里给大家说一声对不起。现在两个比赛
陈杨 陈杨
3天前
McCharts 2.0来了,完美适配HarmonyOS NEXT最新版本,可轻松迁移Echarts图表项目
大家好,我是陈杨。终于有时间来分享一些技术文章了,自从McCharts组件上线第一期之后,就开始忙碌鸿蒙创新赛与极客马拉松比赛。在比赛的过程一直收到很多Issues,但是由于腾不出时间来维护,导致大家以为我们不维护了。在这里给大家说一声对不起。现在两个比赛
陈杨 陈杨
4天前
实战分享!!HarmonyOS NEXT开发一款智能会议小助手应用
大家好,我是陈杨;一只会打代码的羊。最近在忙着全面升级我们的莓创图表组件,一直没有更新与分享相关的技术;等全面升级完成之后会给大家介绍一下做了什么升级,敬请期待!!这次抽点时间出来给大家分享一下之前使用HarmonyOSNEXT开发了一款智能小助手APP整
陈杨 陈杨
4天前
莓创图表:从零到一打造鸿蒙NEXT原生组件,跟我一起探索原生组件库的无限可能
大家好,我是陈杨。又隔了好久没写文章了,一直都在忙(其实是借口),没有及时跟大家去分享一些技术相关的东西,今天来是跟大家分享两件事。发布会总结第一件就是:前段时间我去参加了"",这次我首次参加产品发布会,在发布会上我看到了鸿蒙5.0NEXT正式落地,也了解
陈杨 陈杨
4天前
在原生鸿蒙上开发一款绘画动画软件,然后制作动画短视频,发到B站会火?
大家好,欢迎来到莓创IT技术频道;我是陈杨。今天给大家介绍一款我开发的动画制作软件:IF画。这款软件一开始是准备自己用来学习绘画跟学习制作动画视频的,如果学会成功了,自己就画画插画或者动画,然后投稿到各大短视频平台,做一位自媒体达人,就不再打代码了。但是当
陈杨 陈杨
4天前
鸿蒙原生绘图API:从基础到高阶的绘制之旅(基础版)
theme:hydrogen大家好,欢迎来到莓创IT技术分享频道,我是陈杨。由于经常有小伙伴一直给我反馈说莓创图表(mccharts)数据多的时候经常卡顿,很无奈之前做动画的时候没考虑ArkTs的性能瓶颈,导致现在又要重构开发。于是我重新翻阅文档,看看有没