180******22ArkTS
ArkUI 的 Canvas 绘制
橙动创意用 Canvas 实际可用尺寸重新投影数据。折叠事件适合记时序,但分屏和窗口拖动也会改尺寸,所以别直接用折叠姿态选一套固定坐标。
Canvas(this.ctx)
.onReady(() => { this.repaint(); })
.onAreaChange((_old, area) => {
this.resizeAndRepaint(area.width, area.height);
})resizeAndRepaint 在尺寸有效且变化时才清画布,并从业务点位按新坐标重绘。每次重绘把变换恢复到基线,避免叠乘旧缩放。
中华神刃折叠屏内外屏切换会直接改变画布实际像素尺寸,旧绘图上下文的变换矩阵、画布宽高不会自动同步,所以绘制内容错位,优先用 Canvas 的 onReady 做尺寸变更重绘兜底,再搭配 display 的 foldStatusChange 监听折叠切换事件,收到变更后清空旧上下文、重置画布坐标系、重新执行整套绘制逻辑,同时保存绘制指令而不是绘制结果,不要缓存旧画布像素,切换时直接重放绘制指令,另外注册监听后一定要在 aboutToDisappear 取消事件订阅,避免内存泄漏,下面是可直接使用的完整示例。
import { display } from '@kit.ArkUI'
@Component
struct FoldCanvasDemo {
private canvasCtx: CanvasRenderingContext2D | null = null
private canvasWidth: number = 0
private canvasHeight: number = 0
private foldListenerId: number | null = null
aboutToAppear() {
this.foldListenerId = display.on('foldStatusChange', () => {
this.resetAndRedraw()
})
}
aboutToDisappear() {
if (this.foldListenerId !== null) {
display.off('foldStatusChange', this.foldListenerId)
this.foldListenerId = null
}
this.canvasCtx = null
}
resetAndRedraw() {
if (!this.canvasCtx) return
const ctx = this.canvasCtx
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
ctx.resetTransform()
this.doDraw(ctx)
}
doDraw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = '#007DFF'
ctx.fillRect(this.canvasWidth / 4, this.canvasHeight / 4, this.canvasWidth / 2, this.canvasHeight / 2)
}
build() {
Canvas(this.canvasCtx)
.width('100%')
.height('400vp')
.onReady((ctx: CanvasRenderingContext2D) => {
this.canvasCtx = ctx
this.canvasWidth = ctx.width
this.canvasHeight = ctx.height
this.resetAndRedraw()
})
.onAreaChange((_, newArea) => {
this.canvasWidth = newArea.width
this.canvasHeight = newArea.height
this.resetAndRedraw()
})
}
}
这里补充一点,onReady 在画布尺寸改变时本身就会触发,属于最可靠的兜底回调,foldStatusChange 是用来提前感知折叠切换事件,适合需要在切换前做状态保存的场景,如果只是单纯修复错位,只依靠 onReady + resetTransform 也能解决,onAreaChange 用来同步布局区域,防止 vp 和像素换算偏差,绘制业务尽量全部封装到 doDraw,每次重建画布统一重放,不要依赖上一轮的绘图状态。
飞b6结论:折叠屏内外屏切换时,建议采用 foldDisplayModeChange + onAreaChange + Canvas.onReady 三者配合,而不是只监听折叠状态后重新创建 CanvasRenderingContext2D。
ArkUI 的 Canvas 在尺寸变化时会重新初始化绘制区域,但不会自动重新执行你的业务绘制逻辑。如果仍然使用外屏的坐标、缩放比例或离屏缓存绘制内屏内容,就容易出现错位、拉伸、裁剪或空白。
一、三个事件分别负责什么?
| API | 作用 | 推荐处理 |
|---|---|---|
|
| 监听物理折叠状态 | 判断折叠、展开、悬停 |
|
| 监听内外屏显示模式变化 | 标记布局需要重新适配 |
|
| Canvas 初始化完成或尺寸变化 | 根据当前画布尺寸重新绘制 |
|
| 监听组件实际布局尺寸变化 | 更新绘制区域大小,必要时重绘 |
官方文档特别指出:物理折叠状态变化先于屏幕显示模式变化。因此,在 foldStatusChange 回调里立即读取窗口尺寸并重建画布,可能拿到旧尺寸。
二、推荐的 ArkTS 实现
下面的示例使用 CanvasRenderingContext2D,在折叠切换时重新测量和绘制,不依赖销毁整个组件。
import { display } from '@kit.ArkUI';
@Entry
@Component
struct FoldCanvasPage {
private settings: RenderingContextSettings =
new RenderingContextSettings(true);
private ctx: CanvasRenderingContext2D =
new CanvasRenderingContext2D(this.settings);
private canvasWidth: number = 0;
private canvasHeight: number = 0;
private canvasReady: boolean = false;
private onDisplayModeChange =
(mode: display.FoldDisplayMode): void => {
console.info(`显示模式变化: ${mode}`);
// 不立即按旧尺寸绘制。
// 后续 onReady / onAreaChange 使用实际布局尺寸。
};
aboutToAppear(): void {
if (display.isFoldable()) {
display.on(
'foldDisplayModeChange',
this.onDisplayModeChange
);
}
}
aboutToDisappear(): void {
if (display.isFoldable()) {
display.off(
'foldDisplayModeChange',
this.onDisplayModeChange
);
}
}
private redraw(): void {
if (!this.canvasReady ||
this.canvasWidth <= 0 ||
this.canvasHeight <= 0) {
return;
}
// 清除旧画面
this.ctx.clearRect(
0, 0,
this.ctx.width,
this.ctx.height
);
// 根据实际尺寸计算比例
const scaleX = this.canvasWidth / 360;
const scaleY = this.canvasHeight / 640;
this.ctx.save();
this.ctx.resetTransform();
this.ctx.scale(scaleX, scaleY);
this.ctx.fillStyle = '#317AF7';
this.ctx.fillRect(30, 30, 120, 80);
this.ctx.restore();
}
build() {
Column() {
Canvas(this.ctx)
.width('100%')
.height('100%')
.onReady(() => {
this.canvasReady = true;
this.canvasWidth = this.ctx.width;
this.canvasHeight = this.ctx.height;
this.redraw();
})
.onAreaChange((oldValue, newValue) => {
const w = Number(newValue.width);
const h = Number(newValue.height);
if (w > 0 && h > 0 &&
(w !== this.canvasWidth ||
h !== this.canvasHeight)) {
this.canvasWidth = w;
this.canvasHeight = h;
this.redraw();
}
})
}
.width('100%')
.height('100%')
}
}
该示例展示的是重绘流程。实际项目中需要确保 onAreaChange 获取的尺寸与 Canvas 绘制坐标采用一致的单位,并以 onReady 后的上下文尺寸为准。对于新版本的 Canvas({ unit: ... }) / DrawingRenderingContext,应按相应绘制单位进行适配。
这里并不要求每次折叠都 new CanvasRenderingContext2D()。 官方说明,onReady 在 Canvas 尺寸变化时也会触发,而且触发时画布会被清空,重新绘制即可。
三、为什么重绘后仍然可能错位?
重点检查这四个问题。
1. 坐标变换没有重置
如果每次重绘都执行:
ctx.scale(scaleX, scaleY);却没有恢复变换矩阵,多次绘制可能造成缩放叠加。
更稳妥的绘制顺序是:
ctx.save();
ctx.resetTransform();
ctx.clearRect(0, 0, ctx.width, ctx.height);
// 使用新尺寸重新设置绘图变换
ctx.scale(scaleX, scaleY);
// 绘制业务内容
drawContent();
ctx.restore();其中 drawContent() 是业务绘图函数。实际项目应统一管理变换状态,避免历史缩放和位移继续影响新画面。
2. 画布的逻辑尺寸和像素尺寸混用
HarmonyOS Canvas 的默认绘制坐标通常采用 vp,而 windowSizeChange 获取的窗口尺寸为 px。
如果直接用窗口的 px 宽度作为 Canvas 的 vp 宽度,就容易出现位置偏移。需要统一单位,并根据实际显示区域计算。
3. 没有重建离屏缓存
如果使用 OffscreenCanvas 缓存波形图、图表或手写轨迹,折叠后还使用旧尺寸缓存,就可能出现图像拉伸或裁剪。
此时应根据新的目标像素尺寸重建或调整离屏画布,并重新生成缓存内容。
4. 强制按宽高分别缩放
示例中的 scaleX、scaleY 适合演示尺寸变化,但复杂图形通常不建议直接使用非等比缩放,否则圆形可能变椭圆、字体可能变形。
更合理的方法是:
const scale = Math.min(
canvasWidth / designWidth,
canvasHeight / designHeight
);
const offsetX = (canvasWidth - designWidth * scale) / 2;
const offsetY = (canvasHeight - designHeight * scale) / 2;再使用统一缩放和居中偏移,或者直接按照新尺寸重新计算图形坐标。
四、什么时候需要真正重建 Canvas?
一般情况下,尺寸变化后重新绘制即可,不必销毁 Canvas 组件。
只有在以下情况才考虑重建:
-
上下文关联的绘图资源确实失效。
-
Canvas 底层绘制模式需要切换。
-
旧离屏缓存尺寸无法适配。
-
出现可稳定复现的系统级上下文异常。
如果通过条件渲染强制销毁、重建 Canvas,应确保保存绘图业务数据,而不是依赖旧画布里的像素内容。
另外,在组件 aboutToDisappear() 中记得取消 display 监听,避免旧页面仍然接收折叠事件。
五、推荐排查顺序
-
监听
foldDisplayModeChange,确认显示模式发生变化。 -
记录
Canvas.onReady()是否重新执行。 -
记录
onAreaChange()返回的宽高,以及ctx.width、ctx.height。 -
排查
scale()、translate()是否叠加。 -
检查离屏 Canvas、PixelMap 等缓存是否仍使用旧尺寸。
-
如果尺寸正确却仍显示错位,再用最简单的矩形绘制进行对照,排除业务坐标计算问题。
官方参考:
最终建议:将折叠事件作为适配信号,将 Canvas 实际尺寸变化作为重绘依据。 不要在物理折叠事件中直接按旧窗口尺寸重建画布,也不要在每次切屏时无条件重新创建上下文。这样通常可以解决内外屏切换后的错位、残影和图形缩放异常。
Z折叠屏切换时 Canvas 绘制错位,根因是 Canvas 尺寸变化后画布会被清空并触发 onReady,但你的绘制逻辑没有跟着重新执行,或者 context 的样式属性被重置后没有重新设置。
监听折叠状态
两种方式,选一种即可:
方式一:display.on('foldStatusChange') 直接监听折叠状态,回调拿到 FoldStatus 枚举(0=未知,1=展开,2=折叠,3=悬停)
方式二:windowClass.on('windowSizeChange') 监听窗口尺寸变化,不关心折叠态只关心尺寸的话用这个更简单
重建画布的正确姿势
核心要点:Canvas 尺寸变化时画布会被清空,onReady 会被再次触发。所以你的 onReady 回调里必须包含完整的重绘逻辑,不能只在初始化时画一次就不管了。
如果你需要保留用户手绘等内容(不能简单重绘),在尺寸变化前用 getImageData 保存,onReady 时用 putImageData 恢复。
完整 demo
import { display } from '@kit.ArkUI';
import { Callback } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct FoldableCanvasDemo {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
@State canvasWidth: number = 0;
@State canvasHeight: number = 0;
private imageData: ImageData | undefined = undefined;
aboutToAppear(): void {
// 方式一:监听折叠状态变化
let foldCallback: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
this.redrawCanvas();
};
display.on('foldStatusChange', foldCallback);
// 方式二:监听窗口尺寸变化(二选一)
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let win = context.windowStage.getMainWindowSync();
win.on('windowSizeChange', (data: window.Size) => {
this.canvasWidth = this.getUIContext().px2vp(data.width);
this.canvasHeight = this.getUIContext().px2vp(data.height);
});
// 初始化尺寸
let rect = win.getWindowProperties().windowRect;
this.canvasWidth = this.getUIContext().px2vp(rect.width);
this.canvasHeight = this.getUIContext().px2vp(rect.height);
}
private redrawCanvas(): void {
// 保存当前画布内容(如果需要保留手绘等不可重建内容)
if (this.context.width > 0 && this.context.height > 0) {
this.imageData = this.context.getImageData(0, 0, this.context.width, this.context.height);
}
}
private drawContent(): void {
// 重置画笔样式(onReady 后 context 样式会重置,必须重新设置)
this.context.fillStyle = '#0097D4';
this.context.font = 'bold 18fp sans-serif';
this.context.fillRect(20, 20, this.canvasWidth * 0.5, this.canvasHeight * 0.3);
this.context.fillStyle = '#333333';
this.context.fillText('折叠屏 Canvas 适配', 20, this.canvasHeight * 0.35 + 30);
}
build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
.onReady(() => {
// Canvas 尺寸变化后画布被清空,onReady 再次触发
// 恢复画布内容(如果有保存的)
if (this.imageData) {
this.context.putImageData(this.imageData, 0, 0);
}
// 重新设置画笔样式并绘制
this.drawContent();
})
}
.width('100%')
.height('100%')
}
}
两个容易踩的坑:一是 onReady 里只做了首次初始化的绘制,折叠切换后再触发 onReady 时不会重绘;二是 context 的 fillStyle、font 等样式属性在 onReady 后会被重置为默认值,每次 onReady 都要重新设置。如果你的内容是可以根据数据重建的(比如图表),在 onReady 里完整重绘即可;如果是用户手绘这种不可重建内容,提前 getImageData 保存。
AI助手折叠屏内外屏切换时出现绘制内容错位,根本原因在于 Canvas 组件在尺寸发生变化时会重新触发 onReady 回调,此时画布会被系统清空并按新尺寸重建,而之前在 onReady 之外绘制的所有内容都会丢失;如果绘制逻辑中使用了固定的宽高常量或旧的坐标数据,就会导致内容相对新画布发生错位。解决思路分两步:一是通过 display.on('foldStatusChange') 监听设备折叠状态变化;二是把所有绘制逻辑收敛到一个可重复调用的重绘函数中,在 onReady 和 onAreaChange 里基于最新画布尺寸重新执行,而不是依赖旧的上下文状态。
一、关键机制说明
Canvas 组件的事件触发规则需要注意以下几点:
onReady:在 Canvas 初始化完成或发生大小变化时触发,触发时画布被清空,之后宽高确定且可获取。折叠屏展开/折叠、横竖屏切换都会触发该事件。onAreaChange:Canvas 区域(含位置和大小)变化时触发,且在onReady之后触发。当 Canvas 仅发生位置变化时只触发onAreaChange,不触发onReady。display.on('foldStatusChange'):监听设备物理折叠状态变化,回调返回display.FoldStatus枚举值(0 未知、1 展开、2 折叠、3 半折叠,三折叠设备还有 11、12、13 等扩展值)。
因此,折叠适配的推荐做法是:在 onReady 中恢复绘制(保证画布就绪后至少有一帧完整画面),在 onAreaChange 中根据新旧区域尺寸差异触发重绘,同时用 foldStatusChange 获知折叠状态以便做布局层面的差异化处理(如调整画布比例、切换布局)。
二、完整示例代码
以下示例基于 API 12+(display 相关接口从 @kit.ArkUI 导入),实现折叠状态监听与画布重建:
import { display } from '@kit.ArkUI';
import { Callback } from '@kit.BasicServicesKit';
// 绘制操作记录模型:以归一化坐标(0~1)存储,保证不同画布尺寸下可等比还原
class DrawRecord {
xRatio: number = 0;
yRatio: number = 0;
widthRatio: number = 0;
heightRatio: number = 0;
constructor(xRatio: number, yRatio: number, widthRatio: number, heightRatio: number) {
this.xRatio = xRatio;
this.yRatio = yRatio;
this.widthRatio = widthRatio;
this.heightRatio = heightRatio;
}
}
@Entry
@Component
struct FoldableCanvasPage {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
// 折叠状态:0 未知、1 展开、2 折叠、3 半折叠
@State foldStatus: number = 0;
// 画布当前宽高(vp),由 onAreaChange 实时更新
@State canvasWidth: number = 0;
@State canvasHeight: number = 0;
// 归一化的绘制记录,尺寸变化后据此重绘
private records: DrawRecord[] = [];
aboutToAppear(): void {
// 注册折叠状态监听,内外屏切换、半折叠等状态变化都会回调
let callback: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
this.foldStatus = data;
// 折叠状态变化通常伴随窗口尺寸变化,此处可做布局级差异化处理;
// 画布内容恢复统一交给 onReady / onAreaChange 完成
};
display.on('foldStatusChange', callback);
}
aboutToDisappear(): void {
// 页面销毁时取消监听,避免内存泄漏
display.off('foldStatusChange');
}
// 统一的重绘入口:先清空画布,再按当前尺寸重绘全部内容
private redrawAll(): void {
if (this.canvasWidth <= 0 || this.canvasHeight <= 0) {
return;
}
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 按归一化坐标将记录还原到新尺寸的画布上,避免错位
for (let i = 0; i < this.records.length; i++) {
let record: DrawRecord = this.records[i];
this.context.fillRect(
record.xRatio * this.canvasWidth,
record.yRatio * this.canvasHeight,
record.widthRatio * this.canvasWidth,
record.heightRatio * this.canvasHeight
);
}
}
build() {
Column() {
Text(`当前折叠状态:${this.foldStatus}(1 展开 / 2 折叠 / 3 半折叠)`)
.fontSize(16)
.margin({ top: 20 })
Canvas(this.context)
.width('100%')
.layoutWeight(1)
.backgroundColor('#f1f3f5')
.onReady(() => {
// 画布初始化或尺寸变化时触发,此时画布已被清空且宽高确定
this.redrawAll();
})
.onAreaChange((oldValue: Area, newValue: Area) => {
// 折叠/展开导致画布区域变化时触发,取新尺寸并重绘
this.canvasWidth = Number(newValue.width);
this.canvasHeight = Number(newValue.height);
if (oldValue.width !== newValue.width || oldValue.height !== newValue.height) {
this.redrawAll();
}
})
.onClick((event: ClickEvent) => {
// 点击落笔:记录归一化坐标,保证折叠切换后位置可等比还原
let x: number = event.x;
let y: number = event.y;
this.records.push(new DrawRecord(x / this.canvasWidth, y / this.canvasHeight, 0.1, 0.1));
this.redrawAll();
})
}
.width('100%')
.height('100%')
}
}
三、补充说明
关于绘制内容错位的核心规避手段,建议将所有绘制数据以归一化坐标(相对画布宽高的比例值)或独立的业务模型存储,而不是直接保存像素坐标。这样无论画布如何变化,重绘时都能按新尺寸等比还原,这是华为官方围棋棋子绘制、儿童绘画板等示例中采用的一致做法。
关于 onReady 与 onAreaChange 的分工:onReady 负责画布就绪后的首次绘制与尺寸变化后的兜底恢复;onAreaChange 负责精确感知区域变化并触发重绘。两者都应调用同一个重绘函数,避免逻辑分散导致漏画。若 onReady 中绘制内容较多,尺寸变化时可能引发闪烁,可只在 onReady 中设置画布属性,把实际绘制放到外部方法中执行。
如果折叠切换时窗口尺寸变化存在多段过渡(如半折叠中间态),onAreaChange 可能被高频触发,可在回调中加入简单的尺寸比较判断(如示例中仅在宽高真正变化时重绘),必要时增加防抖逻辑,避免重复重绘带来的性能开销。
匿名用户不用重制吧4天前 来自浙江
- 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
- 如您发布的内容为转载内容,请注明内容来源。
我要提问题
HarmonyOS 如何实现播放Canvas绘制过程的动画?(API12+)
HarmonyOS Canvas绘制大量图片时,设备性能严重劣化(API12+)
HarmonyOS 使用 canvas fillText 绘制文字时,如何使用自定义字体(API12+)
HarmonyOS 为何设置文字阴影后,同层级Canvas内容失效?(API12+)
HarmonyOS折叠屏折叠与展开状态如何监听?(API12+)
HarmonyOS 使用Canvas的textAlign时,偶尔出现画布内容丢失的情况,要如何修改 (API12+)
HarmonyOS 如何实现Canvas绘制的文本与系统保持一致 (API12+)
HarmonyOS 折叠屏折叠后app被强制横屏 (API12+)




































苏公安网备 32011402010933号