HarmonyOS 6实战(源码教学篇)— PinchGesture 图像处理【仿证件照工具实现手势交互的canvas裁剪框】)

HarmonyOS 6实战(源码教学篇)--- PinchGesture 图像处理【仿证件照工具实现手势交互的Canvas裁剪框】

  • [HarmonyOS 6实战(源码教学篇)--- PinchGesture 图像处理【仿证件照工具实现手势交互的Canvas裁剪框】](#HarmonyOS 6实战(源码教学篇)— PinchGesture 图像处理【仿证件照工具实现手势交互的Canvas裁剪框】)
    • [🎯 写在前面:为什么要自己实现裁剪功能?](#🎯 写在前面:为什么要自己实现裁剪功能?)
    • [📱 移动端手势裁剪的三大核心难题](#📱 移动端手势裁剪的三大核心难题)
    • [🛠️ 我们的解决方案:从零实操一个裁剪组件,进行源码级学习。](#🛠️ 我们的解决方案:从零实操一个裁剪组件,进行源码级学习。)
      • [🔍 本教程将详细讲解:](#🔍 本教程将详细讲解:)
    • [🚀 技术栈概览](#🚀 技术栈概览)
    • [📖 教程目录](#📖 教程目录)
    • [1. Canvas绘制专业裁剪框](#1. Canvas绘制专业裁剪框)
    • [2. PinchGesture手势交互实现](#2. PinchGesture手势交互实现)
      • [🤲 移动端手势:从简单到复杂](#🤲 移动端手势:从简单到复杂)
      • [🎯 鸿蒙的手势系统](#🎯 鸿蒙的手势系统)
      • [🔧 实现组合手势](#🔧 实现组合手势)
      • [📐 手势处理的核心:旋转修正](#📐 手势处理的核心:旋转修正)
      • [🔄 缩放和旋转的处理](#🔄 缩放和旋转的处理)
    • [3. 矩阵变换与边界检测](#3. 矩阵变换与边界检测)
      • [🧮 理解矩阵变换](#🧮 理解矩阵变换)
      • [🚧 边界检测:确保图片填满裁剪框](#🚧 边界检测:确保图片填满裁剪框)
      • [📏 坐标转换:从屏幕到图像像素](#📏 坐标转换:从屏幕到图像像素)
    • [4. 性能优化技巧](#4. 性能优化技巧)
      • [⚡ 手势事件节流](#⚡ 手势事件节流)
      • [🎨 Canvas渲染优化](#🎨 Canvas渲染优化)
      • [💾 内存管理](#💾 内存管理)
    • [📚 技术要点总结](#📚 技术要点总结)
    • [🎉 收获与展望](#🎉 收获与展望)
    • 下一篇预告
    • [💡 实战建议](#💡 实战建议)

HarmonyOS 6实战(源码教学篇)--- PinchGesture 图像处理【仿证件照工具实现手势交互的Canvas裁剪框】

基于 HarmonyOS 6 的图像处理技术实战

开发环境:DevEco Studio 5.1.0+ | HarmonyOS SDK 6.0.0+


🎯 写在前面:为什么要自己实现裁剪功能?

作为一名前端开发者,你可能已经习惯了这样:

javascript 复制代码
// 从前在前端开发中...
<template>
  <div>
    <tiny-button text="图片裁剪" @click="visible = !visible"></tiny-button>
    <tiny-crop :cropvisible="visible" @update:cropvisible="visible = $event" :src="imgUrl"></tiny-crop>
  </div>
</template>

<script setup lang="jsx">
	import { ref } from 'vue'
	import { TinyButton, TinyCrop } from '@opentiny/vue'
	
	const visible = ref(false)
	const imgUrl = ref(`//res-static.opentiny.design/tiny-vue-web-doc/3.27.0/static/images/mountain.png`)
</script>

点几下,配置几个参数,功能就有了! 这确实很方便,但当我们来到鸿蒙开发时,情况发生了变化:鸿蒙生态相对前端生态,可学习、实操的组件要少很多,移动端的处理思路也和传统混合开发有一定的区别,这就引出了我们今天要解决的核心问题:

在鸿蒙开发中,当没有现成的"轮子"时,如何自己造一个专业级的图像裁剪工具?


📱 移动端手势裁剪的三大核心难题

在开始编码之前,我们先来思考一下实现这样一个功能需要解决哪些"坑":

难题一:手势交互的自然性

复制代码
用户期望:                    技术挑战:
├── 双指缩放要丝滑           → 捏合手势的实时响应
├── 拖动图片要跟手           → 触摸事件的精准捕获  
├── 旋转操作要流畅           → 角度变化的平滑过渡
└── 边界要有弹性反馈         → 越界后的自动回弹

难题二:坐标系统的复杂性

复制代码
你以为的坐标:              实际处理的坐标:
┌──────────────┐           ┌──────────────┐
│ (x,y)直接对应 │           │ 需要考虑:    │
│              │ →         │ 1. UI坐标 ↔ 图像坐标 │
│  这么简单?   │           │ 2. 旋转后的坐标系变换 │
└──────────────┘           │ 3. 不同设备的像素密度 │
                           └──────────────┘

难题三:视觉效果的精细度

复制代码
基础效果:                 进阶要求:
├── 显示裁剪框              → 半透明遮罩层
├── 图片能拖动              → 九宫格辅助线
├── 基本裁剪                → 实时预览效果
└──                        → 边框高亮提示

🛠️ 我们的解决方案:从零实操一个裁剪组件,进行源码级学习。

在今天的教程中,我将带你从头实现一个完整的证件照裁剪工具,重点解决以下核心问题:

🔍 本教程将详细讲解:

  1. Canvas绘制专业裁剪框 - 如何用Canvas实现带遮罩的裁剪区域
  2. PinchGesture捏合手势 - 双指缩放的精确控制
  3. 矩阵变换计算 - 处理旋转、缩放、平移的数学原理
  4. 边界自动适配 - 确保图片始终填满裁剪框
  5. 坐标系统转换 - UI坐标 ↔ 图像坐标的精准映射

🚀 技术栈概览

技术模块 鸿蒙API 作用
手势交互 PinchGesturePanGesture 处理捏合、拖动手势
图形绘制 CanvasCanvasRenderingContext2D 绘制裁剪框和遮罩
图像变换 Matrix4transform 实现旋转缩放效果
图像处理 PixelMapImageKit 实际裁剪操作

📖 教程目录

  1. Canvas绘制专业裁剪框
  2. PinchGesture手势交互实现
  3. 矩阵变换与坐标计算
  4. 边界检测与自动适配
  5. 完整源码解析
  6. 性能优化技巧

1. Canvas绘制专业裁剪框

🎨 为什么不用普通UI组件?

很多开发者第一次尝试实现裁剪框时,可能会这样想:

typescript 复制代码
// 直觉做法:用UI组件堆叠
Stack() {
  // 底层:半透明遮罩
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.7)');
  
  // 中层:裁剪区域(挖空)
  Column()
    .width(cropWidth)
    .height(cropHeight)
    .backgroundColor(Color.Transparent)
    .border(...);
}

但很快你就会发现这行不通! 因为:

  1. 无法"挖空":UI组件只能叠加,不能做"镂空"效果
  2. 性能问题:多层叠加影响渲染性能
  3. 灵活性差:难以实现复杂的视觉反馈

💡 正确的思路:使用Canvas

Canvas才是实现这类专业视觉效果的利器。它就像一张画布,你可以完全控制每一个像素的绘制:

typescript 复制代码
// Canvas的强大之处
Canvas(this.context)
  .onReady(() => {
    // 1. 绘制整块黑色遮罩
    this.context.fillRect(0, 0, width, height);
    
    // 2. 关键技巧:使用"挖空"混合模式
    this.context.globalCompositeOperation = 'destination-out';
    
    // 3. "挖出"裁剪区域
    this.context.fillRect(cropX, cropY, cropWidth, cropHeight);
  })

🛠️ 实现步骤详解

让我们一步步构建这个专业的裁剪框:

步骤1:初始化Canvas
typescript 复制代码
@Builder
CanvasCropOverlay() {
  Canvas(this.context)
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Transparent)
    .onReady(() => {
      if (!this.context) return;
      
      const width = this.context.width;
      const height = this.context.height;
      
      // 1. 绘制半透明遮罩层
      this.context.fillStyle = 'rgba(0, 0, 0, 0.7)';
      this.context.fillRect(0, 0, width, height);

这里的关键是rgba(0, 0, 0, 0.7),最后的0.7表示70%的透明度,让用户既能看清图片,又能聚焦于裁剪区域。

步骤2:使用"挖空"效果
typescript 复制代码
      // 2. 关键魔法:使用"挖空"混合模式
      this.context.globalCompositeOperation = 'destination-out';
      this.context.fillStyle = 'white';
      
      // 计算裁剪框位置(居中显示)
      const cropX = (width - this.cropWidth) / 2;
      const cropY = (height - this.cropHeight) / 2;
      
      // "挖出"裁剪区域
      this.context.fillRect(cropX, cropY, this.cropWidth, this.cropHeight);

globalCompositeOperation = 'destination-out' 是关键!这个属性告诉Canvas:新绘制的图形应该"挖掉"已有的内容。就像在一张纸上挖个洞一样。

步骤3:绘制边框和辅助线
typescript 复制代码
      // 3. 切换回正常绘制模式,绘制边框
      this.context.globalCompositeOperation = 'source-over';
      this.context.strokeStyle = '#FFFFFF';
      this.context.lineWidth = 2;
      this.context.strokeRect(cropX, cropY, this.cropWidth, this.cropHeight);
      
      // 4. 绘制九宫格辅助线(提升专业感)
      this.drawGrid(cropX, cropY, this.cropWidth, this.cropHeight);
    })
}

九宫格辅助线是专业图像处理软件的标配,它能帮助用户更好地构图:

typescript 复制代码
private drawGrid(x: number, y: number, width: number, height: number): void {
  this.context.strokeStyle = 'rgba(255, 255, 255, 0.3)';
  this.context.lineWidth = 1;
  
  // 两条垂直线(三等分)
  for (let i = 1; i < 3; i++) {
    this.context.beginPath();
    this.context.moveTo(x + width * i / 3, y);
    this.context.lineTo(x + width * i / 3, y + height);
    this.context.stroke();
  }
  
  // 两条水平线(三等分)
  for (let i = 1; i < 3; i++) {
    this.context.beginPath();
    this.context.moveTo(x, y + height * i / 3);
    this.context.lineTo(x + width, y + height * i / 3);
    this.context.stroke();
  }
}

✅ Canvas方案的优势总结

  1. 视觉效果专业:轻松添加网格、边框、高亮等效果
  2. 性能更好:一次绘制完成,无需多层组件叠加
  3. 扩展性强:逻辑集中,易于维护

现在裁剪框已经绘制好了,但用户还不能与之交互。接下来,我们进入最核心的部分:手势交互 。这直接关系到用户体验的流畅度。

2. PinchGesture手势交互实现

🤲 移动端手势:从简单到复杂

在移动设备上,用户期望的交互方式与PC完全不同:

复制代码
PC端:鼠标操作         移动端:手势操作
├── 点击 → 选择         ├── 单击 → 选择
├── 拖拽 → 移动         ├── 拖拽 → 移动
├── 滚轮 → 缩放         ├── 双指捏合 → 缩放
└──                    └── 双指旋转 → 旋转

🎯 鸿蒙的手势系统

鸿蒙提供了丰富的手势API,我们需要根据功能需求选择合适的组合:

typescript 复制代码
// 关键手势类型
PanGesture({})          // 平移手势(单指拖动)
PinchGesture({ fingers: 2 }) // 捏合手势(双指缩放)
RotationGesture({ fingers: 3 }) // 旋转手势(三指旋转)
TapGesture({ count: 2 }) // 双击手势

🔧 实现组合手势

在图像裁剪场景中,用户可能同时进行多个操作(比如边缩放边移动),所以我们需要并行处理这些手势:

typescript 复制代码
.gesture(
  GestureGroup(GestureMode.Parallel,  // 关键:并行模式
    // 1. 平移手势:移动图片
    PanGesture({})
      .onActionStart(() => this.onPanStart())
      .onActionUpdate((event) => this.onPanUpdate(event))
      .onActionEnd(() => this.onPanEnd()),
    
    // 2. 缩放手势:缩放图片
    PinchGesture({ fingers: 2 })
      .onActionStart(() => this.onPinchStart())
      .onActionUpdate((event) => this.onPinchUpdate(event))
      .onActionEnd(() => this.onPinchEnd()),
    
    // 3. 旋转手势:旋转图片
    RotationGesture({ fingers: 3 })
      .onActionStart(() => this.onRotateStart())
      .onActionUpdate((event) => this.onRotateUpdate(event))
      .onActionEnd(() => this.onRotateEnd())
  )
)

GestureMode.Parallel 表示这些手势可以同时识别和处理,不会互相干扰。

📐 手势处理的核心:旋转修正

这里有一个关键的难点:当图片旋转后,用户的拖动方向需要相应调整

想象一下:

  • 图片旋转0°:向右拖动 → 图片向右移动 ✅
  • 图片旋转90°:向右拖动 → 图片应该向下移动 ❓

我们需要根据旋转角度来修正移动方向:

typescript 复制代码
private onPanUpdate(event: GestureEvent): void {
  if (!this.isPanEnabled || !event) return;
  
  // 获取移动距离(转换为像素)
  const deltaX = this.getUIContext().vp2px(event.offsetX);
  const deltaY = this.getUIContext().vp2px(event.offsetY);
  
  // 根据旋转角度调整移动方向
  switch (this.rotation) {
    case 0:   // 0度:正常移动
      this.offsetX += deltaX / this.scale;
      this.offsetY += deltaY / this.scale;
      break;
      
    case 90:  // 90度:X和Y交换,Y取反
      this.offsetX += deltaY / this.scale;  // Y方向的移动变成X
      this.offsetY -= deltaX / this.scale;  // X方向的移动变成Y(取反)
      break;
      
    case 180: // 180度:X和Y都取反
      this.offsetX -= deltaX / this.scale;
      this.offsetY -= deltaY / this.scale;
      break;
      
    case 270: // 270度:X和Y交换,X取反
      this.offsetX -= deltaY / this.scale;
      this.offsetY += deltaX / this.scale;
      break;
  }
  
  // 更新变换矩阵
  this.updateTransformMatrix();
}

这个逻辑是手势处理的核心,它确保了无论图片如何旋转,用户的拖动操作都符合直觉。

🔄 缩放和旋转的处理

捏合手势(缩放)和旋转手势相对简单,主要是数学计算:

typescript 复制代码
// 缩放手势处理
private onPinchUpdate(event: GestureEvent): void {
  if (!event) return;
  
  // event.scale是缩放倍数
  // 1.0表示无缩放,<1.0表示缩小,>1.0表示放大
  const newScale = this.tempScale * event.scale;
  
  // 限制缩放范围(避免过大或过小)
  this.scale = Math.max(0.1, Math.min(newScale, 10.0));
  
  this.updateTransformMatrix();
}

// 旋转手势处理
private onRotateUpdate(event: GestureEvent): void {
  if (!event) return;
  
  // event.angle是旋转角度(弧度)
  const newRotation = this.tempRotate + event.angle * (180 / Math.PI);
  
  // 归一化到0-360度
  this.rotation = newRotation % 360;
  
  this.updateTransformMatrix();
}

手势交互做好了,图片可以自由移动、缩放、旋转了。但用户操作时可能会把图片拖到裁剪框外面,或者缩得太小填不满裁剪框。这就要进入下一个关键环节:边界检测与自动适配

3. 矩阵变换与边界检测

🧮 理解矩阵变换

在计算机图形学中,所有的平移、旋转、缩放操作都可以用矩阵来表示。鸿蒙提供了Matrix4类来简化这些计算:

typescript 复制代码
// 创建变换矩阵(注意顺序!)
const matrix = Matrix4.identity()
  .translate({ x: offsetX, y: offsetY })   // 1. 先平移
  .rotate({ z: 1, angle: rotation })       // 2. 再旋转
  .scale({ x: scale, y: scale });          // 3. 最后缩放

// 应用到Image组件
Image(this.imageSource)
  .transform(this.matrix)

变换顺序很重要!不同的顺序会导致不同的效果。我们选择"平移→旋转→缩放"的顺序,因为这样最符合用户的直觉。

🚧 边界检测:确保图片填满裁剪框

这是实现专业体验的关键功能。用户可能会:

  1. 把图片拖到裁剪框外面
  2. 把图片缩得太小
  3. 旋转后导致部分区域空白

我们需要在每次手势操作后检查并自动修正:

typescript 复制代码
private checkImageAdapt(): void {
  // 1. 计算图片实际显示大小
  const showWidth = this.imageWidth * this.adaptScale * this.scale;
  const showHeight = this.imageHeight * this.adaptScale * this.scale;
  
  // 2. 计算图片在组件中的位置
  const imageX = (this.componentWidth - showWidth) / 2;
  const imageY = (this.componentHeight - showHeight) / 2;
  
  // 3. 裁剪框位置(居中)
  const cropX = (this.componentWidth - this.cropWidth) / 2;
  const cropY = (this.componentHeight - this.cropHeight) / 2;
  
  // 4. 计算实际显示位置(考虑旋转)
  let showX = 0, showY = 0;
  if (this.rotation === 0) {
    showX = imageX + this.offsetX * this.scale;
    showY = imageY + this.offsetY * this.scale;
  } else if (this.rotation === 90) {
    // 旋转90度后,X方向移动受Y偏移影响
    showX = imageX - this.offsetY * this.scale;
    showY = imageY + this.offsetX * this.scale;
  }
  // ... 其他旋转角度
  
  // 5. 边界修正逻辑
  // X轴:确保图片覆盖裁剪框
  if (showX > cropX) {
    showX = cropX;  // 左边不能超出
  } else if (showX + showWidth < cropX + this.cropWidth) {
    showX = cropX + this.cropWidth - showWidth;  // 右边不能超出
  }
  
  // Y轴同理...
  
  // 6. 如果图片太小,自动放大
  if (this.cropWidth > showWidth || this.cropHeight > showHeight) {
    const xScale = this.cropWidth / showWidth;
    const yScale = this.cropHeight / showHeight;
    const newScale = Math.max(xScale, yScale);
    this.scale = this.scale * newScale;
  }
  
  // 7. 更新偏移量
  this.updateOffsetFromPosition(showX, showY, imageX, imageY);
}

📏 坐标转换:从屏幕到图像像素

当用户点击"裁剪"按钮时,我们需要把屏幕上的裁剪框转换为实际的图像像素坐标。这是整个流程中最复杂的部分:

typescript 复制代码
public async crop(): Promise<image.PixelMap> {
  // 1. 加载原始图像
  const file = fs.openSync(this.imageUri, fs.OpenMode.READ_ONLY);
  const imageSource = image.createImageSource(file.fd);
  const pixelMap = await imageSource.createPixelMap();
  
  // 2. 先旋转(如果需要)
  if (this.rotation !== 0) {
    pixelMap.rotateSync(this.rotation);
  }
  
  // 3. 计算总缩放比例
  const adaptScale = this.getAdaptScale();  // 组件适配缩放
  const totalScale = adaptScale * this.scale;  // 总缩放
  
  // 4. 关键:计算裁剪区域坐标
  // 屏幕坐标 → 缩放后坐标 → 原始图像坐标
  const x = (cropX - showX) / totalScale;
  const y = (cropY - showY) / totalScale;
  const cropWidth = this.cropWidth / totalScale;
  const cropHeight = this.cropHeight / totalScale;
  
  // 5. 边界检查
  x = Math.max(0, Math.min(x, originalWidth - cropWidth));
  y = Math.max(0, Math.min(y, originalHeight - cropHeight));
  
  // 6. 执行裁剪
  pixelMap.cropSync({
    x, y,
    size: { width: cropWidth, height: cropHeight }
  });
  
  return pixelMap;
}

这个转换过程就像地图的比例尺换算

  • 屏幕上1厘米 = 在特定缩放比例下 = 图像上N个像素
  • 我们需要反向计算出N的值

现在所有核心功能都实现了,但作为一个高质量的移动应用,我们还需要考虑性能优化。

4. 性能优化技巧

⚡ 手势事件节流

移动设备的触摸屏刷新率通常是60Hz(16.67ms/帧),如果我们的处理频率超过这个值,就是在浪费性能:

typescript 复制代码
private lastGestureTime: number = 0;
private readonly GESTURE_THROTTLE = 16; // 60fps

private onPanUpdate(event: GestureEvent): void {
  const now = Date.now();
  
  // 节流:如果距离上次处理不足16ms,跳过
  if (now - this.lastGestureTime < this.GESTURE_THROTTLE) {
    return;
  }
  this.lastGestureTime = now;
  
  // 处理手势...
}

🎨 Canvas渲染优化

Canvas重绘是比较耗时的操作,我们需要智能控制重绘时机:

typescript 复制代码
private shouldRedrawCanvas: boolean = true;

/**
 * 智能Canvas更新:避免频繁重绘
 */
private updateCanvasIfNeeded(): void {
  if (!this.shouldRedrawCanvas) return;
  
  // 标记为已更新
  this.shouldRedrawCanvas = false;
  
  // 使用requestAnimationFrame,在下一次屏幕刷新时绘制
  requestAnimationFrame(() => {
    this.redrawCanvas();
  });
}

// 批量更新状态
private batchUpdate(updates: () => void): void {
  this.shouldRedrawCanvas = true;  // 标记需要重绘
  updates();  // 执行状态更新
  this.updateCanvasIfNeeded();  // 智能重绘
}

💾 内存管理

图像处理是内存密集型操作,需要特别注意:

typescript 复制代码
public async crop(): Promise<image.PixelMap> {
  let pixelMap: image.PixelMap | null = null;
  let imageSource: image.ImageSource | null = null;
  let file: fileIo.File | null = null;
  
  try {
    file = fs.openSync(this.imageUri, fs.OpenMode.READ_ONLY);
    imageSource = image.createImageSource(file.fd);
    pixelMap = await imageSource.createPixelMap();
    
    // ... 裁剪处理
    
    return pixelMap;
  } finally {
    // 确保释放资源
    if (imageSource) imageSource.release();
    if (file) fileIo.closeSync(file);
  }
}

📚 技术要点总结

模块 核心技术 关键点 易错点
裁剪框 Canvas globalCompositeOperation挖空效果 忘记切换绘制模式
手势交互 GestureGroup 并行处理多手势 旋转后的方向修正
矩阵变换 Matrix4 变换顺序:平移→旋转→缩放 顺序错误导致效果异常
坐标转换 比例换算 屏幕坐标→缩放坐标→图像坐标 忘记考虑旋转角度
边界检测 自动适配 确保图片覆盖裁剪框 边界条件处理不完整
性能优化 节流控制 60fps限制,避免过度渲染 Canvas频繁重绘

🎉 收获与展望

通过本教程,你已经掌握了:

  1. Canvas高级绘制技巧 - 实现专业视觉效果
  2. 鸿蒙手势系统 - 处理复杂的用户交互
  3. 矩阵变换数学 - 理解图形变换原理
  4. 性能优化策略 - 打造流畅的移动体验
  5. 完整项目架构 - 从设计到实现的完整流程

下一篇预告

HarmonyOS 6实战(三):图像保存与文件系统优化

  • 💾 图像格式转换与压缩
  • 📁 鸿蒙文件系统最佳实践
  • ⚡ 异步IO性能优化
  • 🔒 隐私与权限管理

💡 实战建议

  1. 先从基础功能开始,逐步添加高级特性
  2. 在不同设备上测试,确保兼容性
  3. 收集用户反馈,持续优化体验
  4. 参考官方示例,学习最佳实践

完整的项目源码可以在GitHub仓库获取,建议下载后对照教程逐步理解。


教程结束,但学习永不止步! 🎉

如果你在实现过程中遇到问题,或者有改进建议,欢迎在评论区留言讨论。让我们一起在鸿蒙生态中创造更多精彩的应用!

(别忘了给文章点个赞,让更多开发者看到这篇干货教程!👍)

相关推荐
听麟2 小时前
HarmonyOS 6.0+ PC端手绘板协同创作工具开发实战:压感交互与跨端流转落地
华为·交互·harmonyos
晚霞的不甘2 小时前
Flutter for OpenHarmony构建全功能视差侧滑菜单系统:从动效设计到多页面导航的完整实践
前端·学习·flutter·microsoft·前端框架·交互
摘星编程2 小时前
React Native鸿蒙:Tree节点选择状态
react native·react.js·harmonyos
大雷神3 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第27篇:考试系统 - 成绩分析与错题
华为·harmonyos
菜鸟小芯4 小时前
【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&我的页面功能实现
flutter·harmonyos
灰灰勇闯IT4 小时前
Flutter for OpenHarmony:悬浮按钮(FloatingActionButton)最佳实践 —— 强化核心操作,提升用户效率
flutter·华为·交互
雨季6664 小时前
Flutter 三端应用实战:OpenHarmony “心流之泉”——在碎片洪流中,为你筑一眼专注的清泉
开发语言·前端·flutter·交互
一起养小猫4 小时前
Flutter for OpenHarmony 进阶:表达式解析算法与计算器核心实现
算法·flutter·harmonyos
听麟5 小时前
HarmonyOS 6.0+ PC端系统级桌面插件开发实战:ArkUI Widget进阶与系统交互深度集成
华为·交互·harmonyos