# canvasGitCodeGitHub

组件类型:UniCanvasElement

画布

# 兼容性

Web 微信小程序 Android iOS HarmonyOS
4.21 4.41 4.25 4.25 4.61

App平台4.25之前没有完整的canvas组件,但提供了DrawableContext

  • 截图或海报需求,无需像webview那样通过canvas中转,app平台view直接提供截图API,takesnapshot

在绘制形状、文字、图片方面,uni-app x有2种解决方案:canvas组件DOM的DrawableContext API

它们的区别是:

  1. canvas组件的语法是W3C标准语法;DrawableContext是对原生view的绘制API的封装,语法尽可能靠齐W3C规范,但不相同。
  2. canvas组件全平台支持,而DrawableContext仅app支持
  3. canvas组件是一个独立组件,而DrawableContext是对现有的view组件进行绘制
  4. DrawableContext在iOS上绘制文字的性能略低,其原生系统如此
  5. 对于复杂绘制场景,比如游戏,canvas组件的绘制速度优于DrawableContext;对于简单场景,canvas组件的内存占用高于普通view。
  6. canvas是一个独立模块,在Android和iOS平台占用几百K体积,鸿蒙平台封装自鸿蒙自身的canvas。canvas模块不使用时会被摇树摇掉

# 属性

名称 类型 默认值 兼容性 描述
type string -
(string)
指定 canvas 类型,支持 2d (2.9.0) 和 webgl (2.7.0)
canvas-id string -
(string)
canvas 组件的唯一标识符,若指定了 type 则无需再指定该属性
disable-scroll boolean -
(boolean)
当在 canvas 中移动时且有绑定手势事件时,禁止屏幕滚动以及下拉刷新
@touchstart eventhandle -
(eventhandle)
手指触摸动作开始
@touchmove eventhandle -
(eventhandle)
手指触摸后移动
@touchend eventhandle -
(eventhandle)
手指触摸动作结束
@touchcancel eventhandle -
(eventhandle)
手指触摸动作被打断,如来电提醒,弹窗
@longtap eventhandle -
(eventhandle)
手指长按 500ms 之后触发,触发了长按事件后进行移动不会触发屏幕的滚动
@error eventhandle -
(eventhandle)
当发生错误时触发 error 事件,detail = {errMsg}

注意:Android平台默认会开启硬件加速无需额外设置

# 子组件

不可以嵌套组件

# API

老版 uni-app 的 canvas 使用了微信小程序的的旧版规范,和 W3C 规范有差异。微信小程序新版的 canvas 规范已经与 W3C 规范拉齐。

uni-app x 中废弃了老版方案,使用了 W3C 规范和微信小程序的新版规范。

注意:在uni-app x 4.21版以前,Web平台开发者写的老版canvas是可以运行的。但从 4.21+ 支持新版规范起,不再支持老版规范。开发者需调整代码。

注意:新版规范需要开发者根据自己的场景手动处理高清屏问题。

canvas相关的API较多,参考如下:

# 获取组件上下文对象CanvasContext

  1. 异步方式获取CanvasContext

这种方式跨平台,一般推荐这种写法。需HBuilderX 4.25+支持。

组合式

<template>
  <canvas id="canvas"></canvas>
</template>
<script setup>
  onReady(() => {
    // HBuilderX 4.25+
    // 异步调用方式, 跨平台写法
    uni.createCanvasContextAsync({
      id: 'canvas',
      component: getCurrentInstance().proxy,
      success: (context : CanvasContext) => {
        const canvasContext = context.getContext('2d')!;
        const canvas = canvasContext.canvas;

        // 处理高清屏逻辑
        const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
        canvas.width = canvas.offsetWidth * dpr;
        canvas.height = canvas.offsetHeight * dpr;
        canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
      }
    })
  })
</script>

选项式

<template>
  <view>
    <canvas id="canvas"></canvas>
  </view>
</template>

<script>
  export default {
    name: 'canvas',
    data() {
      return {
      }
    },
    onReady() {
      uni.createCanvasContextAsync({
        id: 'canvas',
        component: this,
        success: (context : CanvasContext) => {
          const canvasContext = context.getContext('2d')!;
          const canvas = canvasContext.canvas;

          // 处理高清屏逻辑
          const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
          canvas.width = canvas.offsetWidth * dpr;
          canvas.height = canvas.offsetHeight * dpr;
          canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
        }
      })
    }
  }
</script>

文档详见

  1. 同步方式CanvasContext

需HBuilderX 4.21+支持。

同步方式不支持小程序。仅App和web可以使用。

<template>
  <canvas id="canvas"></canvas>
</template>
<script setup>
  onReady(() => {
    // 同步调用方式,仅支持 app/web
    const canvas = uni.getElementById("canvas") as UniCanvasElement
    const context = canvas.getContext("2d")!;

    // 处理高清屏逻辑
    const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
    canvas.width = canvas.offsetWidth * dpr;
    canvas.height = canvas.offsetHeight * dpr;
    context.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
    // 省略绘制代码,和 w3c 规范保持一致
  })
</script>

# 示例

示例为hello uni-app x alpha分支,与最新HBuilderX Alpha版同步。与最新正式版同步的master分支示例另见

扫码体验(手机浏览器跳转到App直达页)

示例

<template>
  <view class="page" id="page-canvas">
    <canvas id="canvas" class="canvas-element"></canvas>
    <scroll-view class="scroll-view">
      <!-- #ifdef WEB -->
      <button class="canvas-drawing-button" @click="canvasToBlob">canvasToBlob</button>
      <view>
        <text>testToBlobResult: {{testToBlobResult}}</text>
      </view>
      <!-- #endif -->
      <button class="canvas-drawing-button" id="toDataURL" @click="canvasToDataURL">canvasToDataURL</button>
      <view class="text-group" v-if="dataBase64.length>0">
        <text>canvasToDataURL:</text>
        <text>{{dataBase64.slice(0,22)}}...</text>
      </view>
      <button @click="onCreateImage">createImage</button>
      <button @click="onCreatePath2D">createPath2D</button>
      <button @click="startAnimationFrame">requestAnimationFrame</button>
      <button @click="stopAnimationFrame">cancelAnimationFrame</button>
      <view style="padding: 8px 10px;">CanvasContext API 演示</view>
      <navigator url="./canvas-context">
        <button>CanvasContext API</button>
      </navigator>

      <view class="text-group">
        <text>获取 CanvasContext 结果:</text>
        <text id="testCanvasContext">{{testCanvasContext}}</text>
      </view>
      <view class="text-group">
        <text>测试 ToDataURL 结果:</text>
        <text id="testToDataURLResult">{{testToDataURLResult}}</text>
      </view>

      <view class="text-group">
        <text>测试 createImage 结果:</text>
        <text id="testCreateImage">{{testCreateImage}}</text>
      </view>

      <view class="text-group">
        <text>测试 createPath2D 结果:</text>
        <text id="testCreatePath2D">{{testCreatePath2D}}</text>
      </view>

      <view class="text-group">
        <text>测试 createCanvasContextAsync 结果:</text>
        <view @click="testCreateContextAsync" id="createCanvasContextAsync">{{testCanvasCtx}}</view>
      </view>
      <canvas-child ref="canvas-child"></canvas-child>
    </scroll-view>
  </view>
</template>

<script>
  import CanvasChild from './canvas-child.uvue'

  function hidpi(canvas : UniCanvasElement) {
    const context = canvas.getContext("2d")!;
    const dpr = uni.getWindowInfo().pixelRatio;
    canvas.width = canvas.offsetWidth * dpr;
    canvas.height = canvas.offsetHeight * dpr;
    context.scale(dpr, dpr);
  }

  export default {
    components: {
      CanvasChild
    },
    data() {
      return {
        title: 'Context2D',
        canvas: null as UniCanvasElement | null,
        canvasContext: null as CanvasContext | null,
        renderingContext: null as CanvasRenderingContext2D | null,
        canvasWidth: 0,
        canvasHeight: 0,
        dataBase64: '',
        taskId: 0,
        lastTime: 0,
        frameCount: 0,
        // 仅测试
        testCanvasContext: false,
        testToBlobResult: false,
        testToDataURLResult: false,
        testCreateImage: false,
        testCreatePath2D: false,
        testFrameCount: 0,
        testCanvasCtx1: false,
        testCanvasCtx2: false,
        testCounter: 0
      }
    },
    computed: {
      testCanvasCtx() {
        return this.testCanvasCtx1 && this.testCanvasCtx2
      }
    },
    onLoad() {
      // HBuilderX 4.25+
      // 异步调用方式, 跨平台写法
      uni.createCanvasContextAsync({
        id: 'canvas',
        component: this,
        success: (context : CanvasContext) => {
          this.canvasContext = context;
          this.renderingContext = context.getContext('2d')!;
          this.canvas = this.renderingContext!.canvas;

          hidpi(this.canvas!);
          this.canvasWidth = this.canvas!.width;
          this.canvasHeight = this.canvas!.height;

          // #ifdef WEB
          context.toBlob((blob : Blob) => {
            this.testToBlobResult = (blob.size > 0 && blob.type == 'image/jpeg')
          }, 'image/jpeg', 0.95);
          // #endif
          // #ifdef APP || WEB || MP
          setTimeout(() => {
            this.testToDataURLResult = this.canvasContext!.toDataURL().startsWith('data:image/png;base64')
          }, 50)
          // #endif
          this.testCanvasContext = true
        }
      })

      uni.$on('canvasChildReady', this.onChildReady)
    },
    onReady() {
      // 同步调用方式,仅支持 app/web
      // let canvas = uni.getElementById("canvas") as UniCanvasElement
      // this.renderingContext = canvas.getContext("2d")
      // hidpi(canvas);
      // this.canvas = canvas;
      // this.canvasWidth = canvas.width;
      // this.canvasHeight = canvas.height;
    },
    onUnload() {
      uni.$off('canvasChildReady', this.onChildReady)
      if (this.taskId > 0) {
        this.stopAnimationFrame()
      }
    },
    methods: {
      // #ifdef WEB
      canvasToBlob() {
        this.canvasContext!.toBlob((blob : Blob) => {
          this.testToBlobResult = (blob.size > 0 && blob.type == 'image/jpeg')
        }, 'image/jpeg', 0.95)
      },
      // #endif
      canvasToDataURL() {
        this.dataBase64 = this.canvasContext!.toDataURL()
      },
      onCreateImage() {
        this.renderingContext!.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
        let image = this.canvasContext!.createImage();
        image.src = "/static/logo.png"
        image.onload = () => {
          this.testCreateImage = true
          this.renderingContext?.drawImage(image, 0, 0, 100, 100);
        }
      },
      onCreatePath2D() {
        this.renderingContext!.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
        const context = this.renderingContext!
        let path2D = this.canvasContext!.createPath2D()
        this.testCreatePath2D = true
        const amplitude = 64;
        const wavelength = 64;
        for (let i = 0; i < 5; i++) {
          const x1 = 0 + (i * wavelength);
          const y1 = 128;
          const x2 = x1 + wavelength / 4;
          const y2 = y1 - amplitude;
          const x3 = x1 + 3 * wavelength / 4;
          const y3 = y1 + amplitude;
          const x4 = x1 + wavelength;
          const y4 = y1;
          context.moveTo(x1, y1);
          path2D.bezierCurveTo(x2, y2, x3, y3, x4, y4);
        }
        context.stroke(path2D);
      },
      startAnimationFrame() {
        this.taskId = this.canvasContext!.requestAnimationFrame((timestamp : number) => {
          this.testFrameCount++
          this.updateFPS(timestamp)
          this.startAnimationFrame()
        })
      },
      stopAnimationFrame() {
        this.canvasContext!.cancelAnimationFrame(this.taskId)
        this.taskId = 0
      },
      updateFPS(timestamp : number) {
        this.frameCount++
        if (timestamp - this.lastTime >= 1000) {
          const timeOfFrame = (1000 / this.frameCount)
          this.renderingContext!.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
          this.renderingContext!.fillText(`${this.frameCount} / ${timeOfFrame.toFixed(3)}ms`, 10, 18)
          this.frameCount = 0
          this.lastTime = timestamp
        }
      },
      testCreateContextAsync() {
        uni.createCanvasContextAsync({
          id: 'canvas',
          component: this,
          success: () => {
            this.testCanvasCtx1 = true
          }
        })

        // no `component` param
        uni.createCanvasContextAsync({
          id: 'canvas',
          success: () => {
            this.testCanvasCtx2 = true
          }
        })
      },
      onChildReady() {
        const childInstance = (this.$refs['canvas-child'] as ComponentPublicInstance);
        this.testCounter = childInstance.$data['testCounter'] as number;
      }
    }
  }
</script>

<style>
  .page {
    flex: 1;
    height: 100%;
    overflow: hidden;
  }

  .scroll-view {
    flex: 1;
  }

  .canvas-element {
    width: 100%;
    height: 250px;
    background-color: #ffffff;
  }

  .btn-to-image {
    margin: 10px;
  }

  .text-group {
    display: flex;
    flex-flow: row nowrap;
    justify-content: space-between;
    align-items: center;
    padding: 8px 10px;
  }
</style>

# 场景

canvas有很多应用场景,插件市场有很多封装好的插件:

一些web平台的canvas插件,并没有适配uts。此时使用web-view中的canvas也是一种方案,uvue页面里的web-view组件可以和uvue页面里的uts代码双向通信。

# 参见