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