云计算百科
云计算领域专业知识百科平台

【开源鸿蒙跨平台开发--KuiklyUI--06】实战:ArkTS与Kuikly混合开发——打造HarmonyOS原生级水印图片应用

实战:ArkTS与Kuikly混合开发——打造HarmonyOS原生级水印图片应用

摘要:随着HarmonyOS Next的推进,原生应用开发成为热点。如何在保持跨平台高效开发的同时,又能充分利用鸿蒙原生ArkTS的强大能力?本文将通过一个“图片水印工具”实战项目,深度解析 Kuikly框架(Kotlin DSL) 与 ArkTS(Native) 的混合开发模式。我们将从零开始,构建一个集图片选择、Canvas绘图、沙箱文件管理于一体的鸿蒙应用,并解决开发过程中遇到的类型安全、API兼容性等真实挑战。


1. 引言:为什么选择“混合开发”?

在移动应用开发中,我们常面临一个两难选择:

  • 跨平台框架(如Kuikly/KMP):一套代码运行在Android、iOS和HarmonyOS上,开发效率极高,适合业务逻辑和通用UI。
  • 原生开发(ArkTS):直接调用系统底层API(如PhotoViewPicker、Canvas Drawing),性能最好,功能最全。

此外,选择混合开发还有一个重要原因:框架的成熟度。 Kuikly作为一个新兴的跨平台框架,虽然在业务逻辑复用上表现出色,但在某些特定领域的原生能力支持上(如复杂的高性能绘图、最新的系统API适配)尚在完善中。 与其等待框架封装所有能力,不如 缺什么补什么 ——利用ArkTS直接实现那些框架尚未覆盖或难以完美封装的功能。

1.1 混合开发模式对比

特性纯 Kuikly/KMP纯 ArkTSKuikly + ArkTS 混合模式
开发效率 高 (一处编写,多端运行) 中 (仅针对鸿蒙) 高 (业务复用 + 能力互补)
性能表现 中高 (接近原生) 极高 (原生) 极高 (关键路径原生优化)
系统能力 依赖官方/社区封装 100% 覆盖 100% 覆盖 (随时可调原生接口)
UI 灵活性 DSL 声明式 ArkUI 声明式 灵活 (DSL 主体 + ArkTS 插件)
适用场景 列表、详情页、表单 复杂动画、底层硬件调用 常规业务快速迭代 + 核心功能深度定制

KuiklyUI-photo 项目通过“Kotlin DSL写业务 + Native Module调能力”的架构,完美融合了两者。本文将展示如何在Kuikly应用中,嵌入一个纯ArkTS编写的高性能图片水印模块。

2. 项目架构与工程化设计

本项目基于 KuiklyUI-mini 模板,该模板专为轻量级混合开发设计,结构如下:

KuiklyUI-mini/
├── androidApp/ # Android 宿主
├── iosApp/ # iOS 宿主
├── ohosApp/ # HarmonyOS 宿主 (ArkTS)
│ ├── entry/src/main/ets/
│ │ ├── pages/
│ │ │ ├── Index.ets # Kuikly渲染入口 (承载DSL页面)
│ │ │ └── NativeGalleryPage.ets # [核心] 原生水印页面 (ArkTS实现)
│ │ └── kuikly/modules/
│ │ └── KRBridgeModule.ets # [核心] 跨端桥接模块
│ └── build-profile.json5 # 鸿蒙构建配置
└── shared/ # Kotlin Multiplatform 共享层
└── src/commonMain/kotlin/
└── GalleryPage.kt # 通用业务逻辑

模板项目地址:Kuikly-mini 在这里插入图片描述 项目地址: Kuikly-photo 在这里插入图片描述

2.1 模块分工

  • shared (KMP): 负责通用的页面逻辑、网络请求、数据模型定义以及简单的UI布局(如列表页、设置页)。这部分代码编译后会生成 .har 或 .so 供鸿蒙工程调用。
  • ohosApp (ArkTS): 负责鸿蒙特有的能力实现,如文件系统访问、高性能绘图、传感器调用等。对于性能要求极高或界面交互非常复杂的场景,直接使用 ArkTS 编写 Component。

2.2 编译与运行流程

  • Shared 编译: Gradle 将 shared 模块的 Kotlin 代码编译为跨平台中间产物。
  • 资源同步: 构建脚本将生成的 JS/Native 产物复制到 ohosApp 的资源目录。
  • HAP 打包: DevEco Studio 将 ArkTS 代码与 Kuikly 运行时打包成 .hap 文件。
  • 运行时加载: 应用启动时,Kuikly 引擎加载 Shared 层的 DSL,渲染出原生组件;当需要调用特定功能时,通过 Bridge 唤起 ArkTS 模块。
  • 3. 核心功能实现:NativeGalleryPage.ets

    这是我们本次实战的重头戏。我们需要用ArkTS实现一个页面,具备以下功能:

  • 选择图片:调用鸿蒙系统相册。
  • 参数设置:自定义水印文字、颜色、字体样式、时间戳。
  • 合成水印:使用 ohos.graphics.drawing 进行位图处理。
  • 预览与保存:展示处理后的图片。
  • 3.1 强类型状态管理与模型定义

    为了符合ArkTS严格的类型检查(arkts-no-any-unknown),我们首先定义数据模型。在 HarmonyOS Next 中,ArkTS 采用了更严格的静态类型检查,这虽然增加了初期的编码成本,但极大地提高了运行时的稳定性和性能。

    // 定义颜色结构,用于Canvas绘制时的颜色配置
    class WatermarkColor {
    alpha: number = 255
    red: number = 0
    green: number = 0
    blue: number = 0
    }

    // 定义选项Item结构,用于UI列表渲染
    class ColorItem {
    name: string = ''
    value: WatermarkColor = new WatermarkColor()
    }

    class FontStyleItem {
    name: string = ''
    value: string = ''
    }

    在组件中管理状态,我们使用了 @State 装饰器。这是 ArkUI 响应式编程的核心,当 @State 变量改变时,依赖该变量的 UI 组件会自动重新渲染。

    @Entry
    @Component
    struct GalleryNativePage {
    // 图片路径,使用 @State 驱动 Image 组件刷新
    @State imagePath: string = '';
    // 处理后的图片路径
    @State watermarkedPath: string = '';
    // 水印文字配置
    @State watermarkText: string = 'Kuikly Native Watermark';
    @State watermarkColor: WatermarkColor = { alpha: 255, red: 255, green: 0, blue: 0 };
    @State selectedFontStyle: string = 'Normal';
    @State showTimestamp: boolean = false;

    // 预定义配置项,无需响应式变化,作为普通成员变量
    private colors: Array<ColorItem> = [
    { name: 'Red', value: { alpha: 255, red: 255, green: 0, blue: 0 } },
    { name: 'Green', value: { alpha: 255, red: 0, green: 255, blue: 0 } },
    { name: 'Blue', value: { alpha: 255, red: 0, green: 0, blue: 255 } },
    { name: 'Black', value: { alpha: 255, red: 0, green: 0, blue: 0 } },
    { name: 'White', value: { alpha: 255, red: 255, green: 255, blue: 255 } }
    ];
    // … 其他状态

    3.2 原生UI交互:构建参数配置面板

    为了让用户能实时调整水印效果,我们利用ArkTS声明式UI构建了一个配置面板。这部分代码展示了如何使用 TextInput、Toggle 和 Flex 布局来构建交互界面。ArkUI 的声明式语法非常直观,类似 Flutter 或 SwiftUI。

    1. 水印文字输入 (TextInput)

    // 水印文字输入
    Text("水印文字:")
    .alignSelf(ItemAlign.Start)
    .margin({ bottom: 5 })

    TextInput({ text: this.watermarkText, placeholder: '请输入水印文字' })
    .onChange((value) => {
    // 实时更新状态,驱动UI重绘
    this.watermarkText = value;
    })
    .margin({ bottom: 15 })
    .width('100%')

    2. 颜色与样式选择 (Flex布局)

    为了美观地展示颜色选项,我们使用了 Flex 布局包裹 Circle 组件。Flex 布局在处理不定数量、需要自动换行的子元素时非常强大。

    // 颜色选择区域
    Text("水印颜色:")
    .alignSelf(ItemAlign.Start)
    .margin({ bottom: 5 })

    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
    ForEach(this.colors, (item: ColorItem, index: number) => {
    Circle({ width: 30, height: 30 })
    .fill(this.getUIAbilityColor(item.value)) // 辅助函数转换颜色格式
    .stroke(this.selectedColorIndex === index ? Color.Gray : Color.Transparent) // 选中态描边
    .strokeWidth(2)
    .margin({ right: 10, bottom: 10 })
    .onClick(() => {
    this.selectedColorIndex = index;
    this.watermarkColor = item.value;
    })
    })
    }
    .margin({ bottom: 15 })

    3. 时间戳开关 (Toggle)

    // 时间戳开关
    Row() {
    Text("添加时间戳")
    .margin({ right: 10 })
    Toggle({ type: ToggleType.Switch, isOn: this.showTimestamp })
    .onChange((isOn: boolean) => {
    this.showTimestamp = isOn;
    })
    }
    .width('100%')
    .justifyContent(FlexAlign.Start)
    .margin({ bottom: 15 })

    3.3 调用系统相册与沙箱处理

    鸿蒙系统的安全机制要求我们不能直接使用相册的原始URI进行所有操作(尤其是跨应用/跨进程读取时),通常建议将文件复制到应用的沙箱目录(CacheDir)下。

    为什么需要沙箱? 应用沙箱是一种安全隔离机制,防止恶意应用读取其他应用的数据。通过 PhotoViewPicker 获取的 URI 实际上是一个临时授权的访问凭证。为了后续稳定地进行图片处理(如解码、重编码),将其持久化到应用自身的私有目录是最稳妥的做法。

    import picker from '@ohos.file.picker';
    import fs from '@ohos.file.fs';

    async selectImage() {
    try {
    const photoSelectOptions = new picker.PhotoSelectOptions();
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;
    const photoViewPicker = new picker.PhotoViewPicker();
    const photoSelectResult = await photoViewPicker.select(photoSelectOptions);

    if (photoSelectResult.photoUris.length > 0) {
    let uri = photoSelectResult.photoUris[0];
    // 获取上下文
    const context = getContext(this) as common.UIAbilityContext;
    // 打开原始文件
    const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
    // 构造沙箱路径
    const fileName = `picked_${Date.now()}.jpg`;
    const outPath = context.cacheDir + '/' + fileName;
    // 复制文件
    fs.copyFileSync(file.fd, outPath);
    fs.closeSync(file);

    // 更新状态,fileUri用于Image组件显示
    // 这一步至关重要,确保Image组件能通过标准协议加载图片
    this.imagePath = fileUri.getUriFromPath(outPath);
    }
    } catch (err) {
    console.error('selectImage failed', err);
    }
    }

    3.4 高性能绘图:Canvas与PixelMap

    这是水印功能的核心。我们使用 @ohos.multimedia.image 解码图片,使用 @ohos.graphics.drawing 进行绘制。

    技术背景: 鸿蒙的 Drawing API 是基于 Skia 图形库的底层封装,提供了比 ArkUI Canvas 组件更底层的控制能力和更高的性能。它允许我们直接在 PixelMap(位图内存)上进行操作,不需要将图片渲染到屏幕上的 Canvas 组件即可完成处理,非常适合后台图片处理任务。

    import image from '@ohos.multimedia.image';
    import drawing from '@ohos.graphics.drawing';

    async addWatermark() {
    if (!this.imagePath) return;
    try {
    // 1. 创建 PixelMap (设置为可编辑)
    // 注意:默认解码出来的 PixelMap 可能是只读的,必须设置 editable: true
    const file = fs.openSync(this.imagePath, fs.OpenMode.READ_ONLY);
    const imageSource = image.createImageSource(file.fd);
    const decodingOptions: image.DecodingOptions = {
    editable: true,
    desiredPixelFormat: image.PixelMapFormat.RGBA_8888, // 推荐使用 RGBA_8888 格式,兼容性最好
    }
    const pixelMap = await imageSource.createPixelMap(decodingOptions);

    // 2. 绑定 Drawing Canvas
    // 将 Canvas 的绘制目标绑定到 pixelMap 的内存地址
    const canvas = new drawing.Canvas(pixelMap);

    // 3. 配置画笔 (Brush) 和 字体 (Font)
    const brush = new drawing.Brush();
    brush.setColor(this.watermarkColor); // 直接使用我们定义的 WatermarkColor 对象

    const font = new drawing.Font();
    // 动态计算字号:根据图片宽度自适应,避免大图文字过小
    font.setSize(pixelMap.getImageInfoSync().size.width / 20);

    // 4. 处理字体样式 (粗体/斜体)
    // 提示:部分API在不同版本鸿蒙SDK中支持度不同,需做兼容处理
    if (this.selectedFontStyle === 'Italic' || this.selectedFontStyle === 'BoldItalic') {
    font.setSkewX(0.25); // 设置倾斜,模拟斜体效果
    }
    // 注意:setFakeBoldText 等 API 可能在部分设备上不可用,生产环境建议引入完整的字体文件

    // 5. 绘制文本
    // TextBlob 是文本绘制的高效封装,支持复杂的排版
    let finalWatermarkText = this.watermarkText;
    if (this.showTimestamp) {
    // 追加时间戳逻辑
    const now = new Date();
    finalWatermarkText += `\\n${now.getFullYear()}${now.getMonth()+1}${now.getDate()}`;
    }

    const textBlob = drawing.TextBlob.makeFromString(
    finalWatermarkText,
    font,
    drawing.TextEncoding.TEXT_ENCODING_UTF8
    );
    canvas.attachBrush(brush);
    // 绘制位置:左下角偏移
    canvas.drawTextBlob(textBlob, 50, pixelMap.getImageInfoSync().size.height 100);
    canvas.detachBrush();

    // 6. 重新打包保存
    // 将修改后的 PixelMap 重新编码为 JPEG
    const imagePacker = image.createImagePacker();
    const data = await imagePacker.packing(pixelMap, { format: "image/jpeg", quality: 98 });

    // 写入沙箱
    const outPath = getContext(this).cacheDir + `/watermarked_${Date.now()}.jpg`;
    const outFile = fs.openSync(outPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    fs.writeSync(outFile.fd, data);

    // 显示结果
    this.watermarkedPath = fileUri.getUriFromPath(outPath);

    } catch (err) {
    console.error("addWatermark failed", err);
    }
    }

    4. 跨端桥接:KRBridgeModule

    如何从Kuikly页面跳转到这个原生ArkTS页面?KuiklyUI-mini 提供了灵活的模块注册机制,允许我们将原生能力封装为 KRModule,供 Shared 层调用。

    我们在 KRBridgeModule.ets 中建立桥梁,通过 JSON 传递参数,实现灵活的页面跳转与数据交互。

    // ohosApp/entry/src/main/ets/kuikly/modules/KRBridgeModule.ets
    import router from '@ohos.router';

    export class KRBridgeModule extends KRModule {

    // 暴露给 Shared 层的接口
    // KRAny 通常是一个 JSON 字符串,这种弱类型设计为了兼容不同平台的参数结构
    private openPage(params: KRAny) {
    try {
    let records = JSON.parse(params as string) as Record<string, string>;
    let url = records["url"]; // 目标页面名

    if (url) {
    // 使用鸿蒙原生路由跳转
    // 注意:目标页面必须在 main_pages.json 中注册
    router.pushUrl({
    url: 'pages/Index', // 这里的 'pages/Index' 是为了演示,实际上应该根据 url 参数映射到具体的 Native 页面
    params: { pageName: url }
    })
    }
    } catch (e) {
    console.error(`openPage failed: ${e}`)
    }
    }
    }

    在 Kotlin (Shared层) 中,我们只需简单调用:

    // shared/src/commonMain/kotlin/…/RouterPage.kt
    fun openNativeWatermarkPage() {
    // 构建 JSON 参数
    KRBridgeModule.openPage(JSONObject().apply {
    put("url", "native_watermark")
    }.toString())
    }

    5. 开发中的“坑”与经验总结

    在实际开发过程中,我们遇到了一些跨平台与原生对接的典型问题,以下是我们的解决方案。

    5.1 类型检查陷阱 (ArkTS Strict Mode)

    鸿蒙 ArkTS 对 any 类型的容忍度越来越低。在开发初期,为了图省事使用了 colors: Array<any>,导致编译器报错:

    Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)

    解决方案:

    • 严禁使用 any:即使是临时代码,也尽量定义 interface 或 class。
    • 使用 Record 类型:对于键值对结构,使用 Record<string, Object> 替代 Map<any, any>。
    • 显式类型转换:在调用系统 API 返回 Object 时,使用 as 关键字进行安全的类型断言。

    5.2 命名空间冲突

    在使用 common.Color 时,发现 @ohos.app.ability.common 并没有导出 Color,而 UI 组件库中有 Color 枚举,Drawing 库中又有自己的颜色定义。

    解决方案:

    • 自定义 WatermarkColor 类,明确 RGBA 结构,避免依赖模糊的系统类型。
    • 编写辅助函数 getUIAbilityColor 将数据模型转换为 UI 组件需要的 CSS 样式字符串(如 rgba(255,0,0,1))。

    5.3 Drawing API 版本差异

    drawing.Typeface.makeDefault() 和 font.setFakeBoldText() 在某些SDK版本中可能未开放或行为不一致。鸿蒙 API 迭代速度极快,文档有时会滞后。

    解决方案:

    • 防御性编程:使用 try-catch 包裹 API 调用。
    • 寻找替代方案:例如使用 font.setSkewX() 模拟斜体。
    • 关注官方变更日志:及时更新 SDK 并检查废弃接口。

    5.4 图片权限与路径

    直接使用相册返回的 uri 读取图片有时会遇到权限问题,或者 Image 组件无法直接加载某些格式的 uri。

    解决方案: “Copy to Sandbox” 模式。将选中的图片复制到 context.cacheDir,生成标准的 file:// 路径,既解决了权限问题,也统一了路径格式。这在处理多媒体文件时是一个通用的最佳实践。

    6. 运行效果与性能分析

  • 启动速度:得益于 ArkTS 的 AOT 编译,页面加载几乎是瞬时的。
  • 内存占用:在处理 4K 图片时,内存峰值控制在合理范围内。手动触发 GC 后,内存迅速回落,说明 pixelMap 的释放机制工作正常。
  • 交互体验:
    • 首页:点击“打开原生水印工具”,流畅跳转至 ArkTS 编写的 Native 页面。
    • 选图:调起系统 PhotoPicker,体验丝滑,无卡顿。
    • 编辑:
      • 输入 “Hello Harmony”。
      • 点击红色圆点,文字变红。
      • 点击 “Italic”,文字变斜。
      • 打开“时间戳”开关,自动追加当前时间。
    • 生成:点击“添加水印”,Canvas 绘制耗时通常在 50ms 以内(视图片大小而定),用户感知极快。
  • 运行效果: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

    在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

    7. 结语与展望

    通过这个实战,我们验证了 Kuikly + ArkTS 混合开发的强大潜力:

    • Kuikly 负责跨平台业务,节省 70% 的重复工作量。
    • ArkTS 负责高性能图形处理和系统交互,保证原生级体验。

    这种架构模式,为从 Android/iOS 向 HarmonyOS 迁移的开发者提供了一条平滑且高效的路径。未来,随着 Kuikly 对鸿蒙原生能力的进一步封装,我们或许能用 Kotlin DSL 完成更多工作,但在当下,掌握 ArkTS 混合开发无疑是通往鸿蒙生态的最佳门票。

    7.1 下一步计划

    • 手势操作:支持水印文字的拖拽、缩放、旋转(需结合 Matrix 变换)。
    • 图片滤镜:引入 OpenGL 或 Native C++ 层,实现更高级的滤镜效果。
    • 批量处理:利用 Worker 线程,实现多图批量加水印功能。

    拓展阅读

    • 【开源鸿蒙跨平台开发–KuiklyUI–01】 Windows平台Kuikly OpenHarmony开发环境搭建及脚本编译模板工程流程
    • 【开源鸿蒙跨平台开发–KuiklyUI–02】华为云真机部署实战指南
    • Kuikly官方文档
    • OpenHarmony官方文档
    • Kotlin Multiplatform官方文档
    • 鸿蒙开发者文档:developer.huawei.com 欢迎加入开源鸿蒙跨平台社区:开源鸿蒙跨平台开发者社区

    作者:Goway_Hui 时间:2026-02-02 版权:本文基于 KuiklyUI-mini 开源项目实践撰写。

    如果觉得本项目对你有帮助,欢迎点赞收藏!

    (本文代码基于 HarmonyOS API 12+ 开发)

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【开源鸿蒙跨平台开发--KuiklyUI--06】实战:ArkTS与Kuikly混合开发——打造HarmonyOS原生级水印图片应用
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!