Windows 平台 KuiklyUI 实战:ArkTS 与 Kuikly 混合开发 —— 打造 HarmonyOS 原生级水印图片应用
本次实战以 ArkTS 与 Kuikly 混合开发模式,完成了 HarmonyOS 原生水印图片应用搭建。ArkTS 保障原生体验,Kuikly 提升跨平台开发效率,二者协同构建了高效复用的开发范式。项目可直接拓展跨平台支持或升级 AI 水印等功能,为 HarmonyOS 应用开发提供了实用参考。期待大家基于此探索更多生态创新,打造优质原生应用!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
引言
在 HarmonyOS 生态高速发展的当下,跨平台开发与原生体验的平衡成为开发者关注的核心议题。ArkTS 作为 HarmonyOS 的原生开发语言,凭借其高效的 UI 渲染能力和深度系统集成优势,成为构建原生应用的首选;而腾讯 Oteam 推出的 KuiklyUI 框架,基于 Kotlin Multiplatform(KMP)实现了一套代码多端运行的跨平台能力,兼顾了开发效率与原生性能。
本文将带您探索 ArkTS 与 Kuikly 混合开发的全新模式,以打造一款功能完备的 HarmonyOS 原生级水印图片应用为例,从环境搭建、项目架构设计、核心功能实现到多端适配部署,进行全方位、手把手的实战教学。无论您是 HarmonyOS 开发新手,还是寻求跨平台解决方案的资深开发者,都能通过本文掌握混合开发的核心技巧,快速构建高性能、高颜值的原生应用。
本文配套模板项目:KuiklyUI-mini
本文配套成品项目:Kuikly-photo
应用核心功能预览
本次开发的水印图片应用将具备以下核心功能,覆盖日常图片处理的核心场景:
1.图片选择:支持从系统相册选取图片,兼容 HarmonyOS 文件管理系统
2.水印编辑:
文字水印:支持自定义文字内容、字体大小、颜色、透明度、旋转角度
图片水印:支持添加本地图片作为水印,可缩放、旋转、调整透明度
水印布局:支持单水印、平铺水印两种模式,可调整水印间距、边距
3.实时预览:编辑过程中实时展示水印效果,所见即所得
4.图片导出:支持将处理后的图片保存到系统相册,支持多种图片格式(JPG/PNG)
5.历史记录:保存最近编辑的图片项目,支持二次编辑
6.原生适配:完美适配 HarmonyOS 手机、平板等设备,支持深色模式、安全区域适配
混合开发模式的核心优势
1.原生体验保障:ArkTS 负责 UI 层与系统能力交互,确保 HarmonyOS 平台下的原生操作体验
2.开发效率提升:KuiklyUI 负责业务逻辑与通用组件,实现跨平台复用,减少重复开发
3.性能无损耗:KuiklyUI 采用原生渲染机制,结合 ArkTS 的高效渲染,避免跨平台框架常见的性能瓶颈
4.扩展性强:后续可快速将应用扩展到 Android、iOS 等平台,无需重构核心逻辑
1. 开发环境准备
工欲善其事,必先利其器。以下为本次开发所需核心工具清单,已掌握环境搭建的开发者可直接跳过配置步骤,确保工具版本符合要求即可:
1.1 核心工具与版本要求
1.操作系统:Windows 10/11 64 位(本文基于 Windows 平台开发)
2.JDK 版本:JDK 17+(推荐 Adoptium JDK 17)
3.开发 IDE:DevEco Studio 5.0+(Build 5.0.3.400+)
4.系统 SDK:HarmonyOS SDK API Version 10+(需包含原生应用开发模块)
5.编程语言:Kotlin 1.9.20+、ArkTS(DevEco Studio 内置支持)
6.构建工具:Gradle 8.5+(DevEco Studio 自动适配)
7.运行环境:HarmonyOS 3.0 + 真机 / 模拟器
8.依赖仓库:KuiklyUI 官方仓库(https://maven.oteam.com.cn/repository/public/)
1.2 关键配置要点
-
DevEco Studio 需启用 Kotlin 插件,配置 KuiklyUI 仓库地址(在
init.gradle中添加仓库链接) -
确保 HarmonyOS SDK 已安装 API 10 + 相关模块(SDK Platform、SDK Tools)
-
真机 / 模拟器需正常连接,开发者模式与 USB 调试已开启
验证 KuiklyUI 依赖:在项目build.gradle中添加核心依赖后同步无报错
dependencies {
implementation "com.tencent.kuikly:core:1.0.0"
implementation "com.tencent.kuikly:render-ohos:1.0.0"
}
2. 项目架构设计
合理的项目架构是应用开发高效、可维护的基础。本项目采用分层架构设计,结合 ArkTS 的原生特性与 KuiklyUI 的跨平台优势,实现逻辑与 UI 分离、业务与系统能力分离。
2.1 项目整体架构
项目采用 "三层架构 + 跨平台模块" 的设计模式,分为以下层级(从下到上):
1.基础层(Base Layer):包含工具类、常量定义、异常处理、基础组件封装
2.核心层(Core Layer):包含跨平台业务逻辑(KuiklyUI 实现)、数据模型、状态管理
3.应用层(App Layer):包含 ArkTS 原生 UI、系统能力调用、页面路由、权限管理
4.跨平台模块(Cross-Platform Module):KuiklyUI 实现的通用组件、业务逻辑,可复用至其他平台
┌─────────────────────────────────────────────────────┐
│ 应用层(App Layer) │
│ - ArkTS页面(UI)、路由管理、权限管理、系统API调用 │
├─────────────────────────────────────────────────────┤
│ 核心层(Core Layer) │
│ - 数据模型(Data Model):图片信息、水印配置、历史记录 │
│ - 业务逻辑(Business Logic):水印处理、图片导出 │
│ - 状态管理(State Management):响应式数据绑定 │
├─────────────────────────────────────────────────────┤
│ 基础层(Base Layer) │
│ - 工具类(Utils):图片处理工具、日期工具、字符串工具 │
│ - 常量定义(Constants):配置常量、路径常量、权限常量 │
│ - 异常处理(Exception Handler):统一异常捕获处理 │
│ - 基础组件(Base Components):通用UI组件封装 │
├─────────────────────────────────────────────────────┤
│ 跨平台模块(Cross-Platform Module) │
│ - KuiklyUI通用组件:水印编辑组件、图片预览组件 │
│ - 跨平台业务逻辑:水印计算、图片处理核心算法 │
└─────────────────────────────────────────────────────┘
2.2 项目目录结构
基于上述架构,项目目录结构如下(结合模板项目 KuiklyUI-mini 扩展):
Kuikly-photo/
├── app/ // 应用主模块
│ ├── src/
│ │ ├── main/
│ │ │ ├── arkts/ // ArkTS原生代码目录
│ │ │ │ ├── api/ // 系统API封装
│ │ │ │ │ ├── AlbumApi.ets // 相册API封装
│ │ │ │ │ ├── FileApi.ets // 文件操作API封装
│ │ │ │ │ └── PermissionApi.ets // 权限API封装
│ │ │ │ ├── components/ // ArkTS原生组件
│ │ │ │ │ ├── common/ // 通用原生组件
│ │ │ │ │ │ ├── TitleBar.ets // 标题栏组件
│ │ │ │ │ │ ├── LoadingDialog.ets // 加载对话框
│ │ │ │ │ │ └── PermissionRequest.ets // 权限请求组件
│ │ │ │ │ └── watermark/ // 水印相关原生组件
│ │ │ │ │ ├── WatermarkTextEditor.ets // 文字水印编辑组件
│ │ │ │ │ ├── WatermarkImageEditor.ets // 图片水印编辑组件
│ │ │ │ │ └── WatermarkPreview.ets // 水印预览组件
│ │ │ │ ├── pages/ // 应用页面
│ │ │ │ │ ├── MainPage.ets // 主页面
│ │ │ │ │ ├── ImageSelectPage.ets // 图片选择页面
│ │ │ │ │ ├── WatermarkEditPage.ets // 水印编辑页面
│ │ │ │ │ ├── HistoryPage.ets // 历史记录页面
│ │ │ │ │ └── SettingPage.ets // 设置页面
│ │ │ │ ├── router/ // 路由管理
│ │ │ │ │ ├── Router.ets // 路由配置
│ │ │ │ │ └── RouterPaths.ets // 路由路径常量
│ │ │ │ ├── utils/ // ArkTS工具类
│ │ │ │ │ ├── ImageUtils.ets // 图片处理工具
│ │ │ │ │ ├── FileUtils.ets // 文件操作工具
│ │ │ │ │ └── PermissionUtils.ets // 权限工具
│ │ │ │ ├── App.ets // 应用入口
│ │ │ │ └── MainAbility.ets // 主Ability
│ │ │ ├── resources/ // 资源目录
│ │ │ │ ├── base/ // 基础资源
│ │ │ │ │ ├── color.json // 颜色资源
│ │ │ │ │ ├── string.json // 字符串资源
│ │ │ │ │ ├── dimension.json // 尺寸资源
│ │ │ │ │ └── media/ // 媒体资源(图片、图标)
│ │ │ │ ├── rawfile/ // 原始文件资源
│ │ │ │ └── profile/ // 配置文件
│ │ │ ├── config.json // 应用配置文件
│ │ │ └── module.json5 // 模块配置文件
│ └── build.gradle // 模块构建配置
├── kuikly-core/ // Kuikly跨平台核心模块
│ ├── src/
│ │ ├── commonMain/ // 通用代码(多端共享)
│ │ │ ├── kotlin/
│ │ │ │ ├── com/
│ │ │ │ │ └── tencent/
│ │ │ │ │ └── kuikly/
│ │ │ │ │ ├── model/ // 数据模型
│ │ │ │ │ │ ├── ImageInfo.kt // 图片信息模型
│ │ │ │ │ │ ├── WatermarkConfig.kt // 水印配置模型
│ │ │ │ │ │ └── HistoryRecord.kt // 历史记录模型
│ │ │ │ │ ├── repository/ // 数据仓库
│ │ │ │ │ │ ├── HistoryRepository.kt // 历史记录仓库
│ │ │ │ │ │ └── WatermarkRepository.kt // 水印配置仓库
│ │ │ │ │ ├── service/ // 业务服务
│ │ │ │ │ │ ├── WatermarkService.kt // 水印处理服务
│ │ │ │ │ │ └── ImageProcessService.kt // 图片处理服务
│ │ │ │ │ └── util/ // 跨平台工具类
│ │ │ │ │ ├── WatermarkUtils.kt // 水印计算工具
│ │ │ │ │ └── DateUtils.kt //
2.3 核心数据模型设计
数据模型是应用的数据载体,基于 Kotlin Multiplatform 实现,确保跨平台数据一致性。核心数据模型如下:
2.3.1 图片信息模型(ImageInfo.kt)
package com.tencent.kuikly.model
import kotlinx.serialization.Serializable
/**
* 图片信息模型
* @param id 图片唯一标识
* @param path 图片本地路径
* @param name 图片名称
* @param size 图片大小(字节)
* @param width 图片宽度(像素)
* @param height 图片高度(像素)
* @param mimeType 图片格式(image/jpeg、image/png等)
* @param createTime 图片创建时间(时间戳)
*/
@Serializable
data class ImageInfo(
val id: String,
val path: String,
val name: String,
val size: Long,
val width: Int,
val height: Int,
val mimeType: String,
val createTime: Long
)
2.3.2 水印配置模型(WatermarkConfig.kt)
package com.tencent.kuikly.model
import kotlinx.serialization.Serializable
/**
* 水印类型枚举
*/
enum class WatermarkType {
TEXT, // 文字水印
IMAGE // 图片水印
}
/**
* 水印布局类型枚举
*/
enum class WatermarkLayoutType {
SINGLE, // 单水印
TILE // 平铺水印
}
/**
* 文字水印配置
* @param text 水印文字内容
* @param fontSize 字体大小(像素)
* @param color 字体颜色(十六进制字符串,含透明度,例:#FF0000FF)
* @param opacity 透明度(0.0-1.0)
* @param rotation 旋转角度(-360.0-360.0)
* @param fontName 字体名称(默认系统字体)
*/
@Serializable
data class TextWatermarkConfig(
var text: String = "水印文字",
var fontSize: Float = 32f,
var color: String = "#FF000000",
var opacity: Float = 0.8f,
var rotation: Float = 0f,
var fontName: String = "sans-serif"
)
/**
* 图片水印配置
* @param imagePath 水印图片本地路径
* @param scale 缩放比例(0.1-2.0)
* @param opacity 透明度(0.0-1.0)
* @param rotation 旋转角度(-360.0-360.0)
*/
@Serializable
data class ImageWatermarkConfig(
var imagePath: String = "",
var scale: Float = 1.0f,
var opacity: Float = 0.8f,
var rotation: Float = 0f
)
/**
* 水印全局配置
* @param type 水印类型
* @param layoutType 布局类型
* @param textConfig 文字水印配置(当type为TEXT时生效)
* @param imageConfig 图片水印配置(当type为IMAGE时生效)
* @param tileSpacing 平铺间距(像素,当layoutType为TILE时生效)
* @param margin 边距(像素,当layoutType为TILE时生效)
*/
@Serializable
data class WatermarkConfig(
var type: WatermarkType = WatermarkType.TEXT,
var layoutType: WatermarkLayoutType = WatermarkLayoutType.SINGLE,
var textConfig: TextWatermarkConfig = TextWatermarkConfig(),
var imageConfig: ImageWatermarkConfig = ImageWatermarkConfig(),
var tileSpacing: Int = 50,
var margin: Int = 30
)
2.3.3 历史记录模型(HistoryRecord.kt)
package com.tencent.kuikly.model
import kotlinx.serialization.Serializable
/**
* 编辑历史记录模型
* @param id 记录唯一标识
* @param originalImage 原始图片信息
* @param resultImage 处理后图片信息
* @param watermarkConfig 水印配置
* @param editTime 编辑时间(时间戳)
*/
@Serializable
data class HistoryRecord(
val id: String,
val originalImage: ImageInfo,
val resultImage: ImageInfo,
val watermarkConfig: WatermarkConfig,
val editTime: Long
)
2.4 状态管理设计
采用 KuiklyUI 的响应式状态管理结合 ArkTS 的 Observed 装饰器,实现数据的双向绑定与状态同步:
1.跨平台业务状态:使用 KuiklyUI 的observable和observableList实现响应式数据,如水印配置、图片列表等
2.原生 UI 状态:使用 ArkTS 的@Observed和@ObjectLink装饰器,实现 UI 与数据的联动
3.状态同步机制:通过接口回调将 KuiklyUI 的跨平台状态同步到 ArkTS 原生 UI,确保状态一致性
核心状态管理类(WatermarkState.kt):
package com.tencent.kuikly.model
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.reactive.handler.observableList
/**
* 水印编辑状态管理类
*/
class WatermarkEditState {
// 当前选中的图片信息
var selectedImage by observable<ImageInfo?>(null)
// 水印配置
var watermarkConfig by observable(WatermarkConfig())
// 编辑过程中的预览图片路径
var previewImagePath by observable("")
// 是否正在处理图片
var isProcessing by observable(false)
// 处理进度(0-100)
var processProgress by observable(0)
// 错误信息
var errorMessage by observable("")
// 重置状态
fun reset() {
selectedImage = null
watermarkConfig = WatermarkConfig()
previewImagePath = ""
isProcessing = false
processProgress = 0
errorMessage = ""
}
}
/**
* 历史记录状态管理类
*/
class HistoryState {
// 历史记录列表
var historyRecords by observableList<HistoryRecord>()
// 是否正在加载历史记录
var isLoading by observable(false)
// 错误信息
var errorMessage by observable("")
}
3. 项目初始化与基础配置
基于模板项目 KuiklyUI-mini,完成项目的初始化与基础配置,为核心功能开发打下基础。
3.1 项目导入与配置
1.下载模板项目:
访问KuiklyUI-mini,点击 "下载 zip" 下载模板项目
解压下载的 zip 文件到本地目录(例:E:\KuiklyProjects\KuiklyUI-mini)
2.导入项目到 DevEco Studio:
打开 DevEco Studio,点击 "Open Project",选择解压后的 KuiklyUI-mini 项目目录
等待项目同步完成(首次同步需下载依赖,约 5-10 分钟,视网络速度而定)
3.项目重命名与配置修改:
右键项目根目录,选择 "Refactor > Rename",将项目名称改为 Kuikly-photo
修改settings.gradle文件中的项目名称和包名:
rootProject.name = "Kuikly-photo"
include(":app", ":kuikly-core", ":kuikly-ui")
修改app模块下的module.json5文件,更新包名、应用名称、图标等信息:
{
"module": {
"name": "app",
"type": "entry",
"description": "水印图片应用",
"mainElement": "MainAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": [
"pages/MainPage"
],
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/MainAbility.ets",
"description": "主Ability",
"icon": "$media:app_icon",
"label": "水印图片大师",
"type": "page",
"visible": true
}
],
"reqPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "需要访问相册以选择图片",
"usedScene": {
"abilities": [
"MainAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_IMAGEVIDEO",
"reason": "需要保存图片到相册",
"usedScene": {
"abilities": [
"MainAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "需要访问媒体文件",
"usedScene": {
"abilities": [
"MainAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "需要写入媒体文件",
"usedScene": {
"abilities": [
"MainAbility"
],
"when": "always"
}
}
]
}
}
3.2 核心依赖配置
修改项目各模块的build.gradle文件,添加核心依赖:
3.2.1 项目根目录build.gradle
buildscript {
repositories {
maven { url "https://maven.oteam.com.cn/repository/public/" }
mavenCentral()
google()
}
dependencies {
classpath "com.tencent.kuikly:gradle-plugin:1.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20"
classpath "com.huawei.agconnect:agcp-harmonyos:1.6.0.300"
}
}
allprojects {
repositories {
maven { url "https://maven.oteam.com.cn/repository/public/" }
mavenCentral()
google()
maven { url "https://developer.huawei.com/repo/" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3.2.2 app模块build.gradle
plugins {
id 'com.huawei.ohos.hap'
id 'com.huawei.ohos.deploy'
id 'org.jetbrains.kotlin.plugin.compose'
id 'com.tencent.kuikly.plugin'
}
ohos {
compileSdkVersion 10
defaultConfig {
minSdkVersion 10
targetSdkVersion 10
applicationId "com.tencent.kuikly.photowatermark"
versionCode 10000
versionName "1.0.0"
testInstrumentationRunner "ohos.test.runner.ParameterizedTestRunner"
}
buildTypes {
debug {
debuggable true
signingConfig 'debug'
}
release {
debuggable false
signingConfig 'release'
}
}
packagingOptions {
exclude 'META-INF/*.MD'
exclude 'META-INF/*.md'
}
}
dependencies {
// HarmonyOS原生依赖
implementation fileTree(dir: 'libs', include: ['*.jar', '*.har'])
implementation 'com.huawei.ohos:ui:10.0.10.100'
implementation 'com.huawei.ohos:ui-components:10.0.10.100'
implementation 'com.huawei.ohos:ability:10.0.10.100'
implementation 'com.huawei.ohos:data:10.0.10.100'
// Kotlin依赖
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
// KuiklyUI依赖
implementation project(':kuikly-core')
implementation project(':kuikly-ui')
implementation "com.tencent.kuikly:core:1.0.0"
implementation "com.tencent.kuikly:render-ohos:1.0.0"
// 图片处理依赖
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'com.tencent:mmkv:1.3.10' // 数据存储
// 测试依赖
testImplementation 'junit:junit:4.13.2'
ohosTestImplementation 'com.huawei.ohos:test-runner:10.0.10.100'
}
3.2.3 kuikly-core模块build.gradle
plugins {
id 'com.tencent.kuikly.kmp'
id 'org.jetbrains.kotlin.plugin.serialization'
}
kuikly {
target {
ohos()
android() // 预留Android平台支持
}
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation "com.tencent.kuikly:core:1.0.0"
}
ohosMain.dependencies {
implementation 'com.huawei.ohos:ui:10.0.10.100'
}
androidMain.dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
}
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}
3.3 基础工具类实现
基础工具类是应用开发的基石,封装常用功能,提高开发效率。以下实现核心工具类:
3.3.1 日期工具类(DateUtils.kt)
package com.tencent.kuikly.util
import java.text.SimpleDateFormat
import java.util.*
object DateUtils {
/**
* 格式化时间戳为字符串
* @param timestamp 时间戳(毫秒)
* @param pattern 格式模板(默认:yyyy-MM-dd HH:mm:ss)
* @return 格式化后的时间字符串
*/
fun formatTimestamp(timestamp: Long, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
val sdf = SimpleDateFormat(pattern, Locale.getDefault())
return sdf.format(Date(timestamp))
}
/**
* 获取当前时间戳(毫秒)
*/
fun getCurrentTimestamp(): Long {
return System.currentTimeMillis()
}
/**
* 生成唯一ID(基于时间戳+随机数)
*/
fun generateUniqueId(): String {
val timestamp = getCurrentTimestamp()
val random = Random().nextInt(10000)
return "${timestamp}_${random.toString().padStart(4, '0')}"
}
}
3.3.2 图片工具类(ImageUtils.kt)
package com.tencent.kuikly.util
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Rect
import java.io.File
import java.io.FileOutputStream
object ImageUtils {
/**
* 读取图片文件为Bitmap
* @param path 图片路径
* @param maxWidth 最大宽度(超过则缩放)
* @param maxHeight 最大高度(超过则缩放)
* @return Bitmap对象
*/
fun loadImage(path: String, maxWidth: Int = 1920, maxHeight: Int = 1080): Bitmap? {
try {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)
// 计算缩放比例
val widthRatio = options.outWidth.toFloat() / maxWidth.toFloat()
val heightRatio = options.outHeight.toFloat() / maxHeight.toFloat()
var inSampleSize = 1
if (heightRatio > 1 || widthRatio > 1) {
inSampleSize = if (widthRatio > heightRatio) widthRatio.toInt() else heightRatio.toInt()
}
options.inJustDecodeBounds = false
options.inSampleSize = inSampleSize
options.inPreferredConfig = Bitmap.Config.ARGB_8888
return BitmapFactory.decodeFile(path, options)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* 保存Bitmap到文件
* @param bitmap Bitmap对象
* @param savePath 保存路径
* @param format 图片格式(Bitmap.CompressFormat.JPEG/Bitmap.CompressFormat.PNG)
* @param quality 质量(0-100,仅JPEG有效)
* @return 是否保存成功
*/
fun saveBitmap(bitmap: Bitmap, savePath: String, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 90): Boolean {
try {
val file = File(savePath)
if (!file.parentFile?.exists()!!) {
file.parentFile?.mkdirs()
}
val outputStream = FileOutputStream(file)
val result = bitmap.compress(format, quality, outputStream)
outputStream.flush()
outputStream.close()
return result
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
/**
* 缩放Bitmap
* @param bitmap 原始Bitmap
* @param scale 缩放比例(0.1-2.0)
* @return 缩放后的Bitmap
*/
fun scaleBitmap(bitmap: Bitmap, scale: Float): Bitmap {
val matrix = Matrix()
matrix.postScale(scale, scale)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
/**
* 旋转Bitmap
* @param bitmap 原始Bitmap
* @param degrees 旋转角度(度)
* @return 旋转后的Bitmap
*/
fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
/**
* 十六进制颜色字符串转ColorInt
* @param colorHex 十六进制颜色字符串(例:#FF0000FF)
* @return ColorInt
*/
fun hexToColor(colorHex: String): Int {
return try {
Color.parseColor(colorHex)
} catch (e: Exception) {
Color.BLACK
}
}
}
3.3.3 文件工具类(FileUtils.kt)
package com.tencent.kuikly.util
import java.io.File
object FileUtils {
/**
* 获取应用私有存储目录
* @param context 上下文
* @return 私有存储目录路径
*/
fun getAppPrivateDir(context: Any): String {
return if (context is ohos.app.Context) {
context.filesDir.absolutePath
} else {
""
}
}
/**
* 获取图片保存目录
* @param context 上下文
* @return 图片保存目录路径
*/
fun getImageSaveDir(context: Any): String {
val privateDir = getAppPrivateDir(context)
val imageDir = "$privateDir/WatermarkImages"
val file = File(imageDir)
if (!file.exists()) {
file.mkdirs()
}
return imageDir
}
/**
* 生成图片保存文件名
* @param originalName 原始文件名
* @param format 图片格式(.jpg/.png)
* @return 保存文件名
*/
fun generateImageFileName(originalName: String, format: String = ".jpg"): String {
val timestamp = DateUtils.getCurrentTimestamp()
val nameWithoutExt = originalName.substringBeforeLast(".")
return "${nameWithoutExt}_watermark_$timestamp$format"
}
/**
* 删除文件
* @param path 文件路径
* @return 是否删除成功
*/
fun deleteFile(path: String): Boolean {
val file = File(path)
return if (file.exists()) {
file.delete()
} else {
true
}
}
/**
* 获取文件大小(字节)
* @param path 文件路径
* @return 文件大小
*/
fun getFileSize(path: String): Long {
val file = File(path)
return if (file.exists() && file.isFile()) {
file.length()
} else {
0L
}
}
}
3.4 基础组件封装
封装通用基础组件,统一 UI 风格与交互逻辑,提高开发效率。
3.4.1 标题栏组件(TitleBar.ets)
import router from '@ohos.router';
import { Resource } from '@ohos/ui';
import { stringId } from '../resources/stringId';
@Component
export struct TitleBar {
@Prop title: string = ''; // 标题
@Prop showBack: boolean = true; // 是否显示返回按钮
@Prop rightText: string = ''; // 右侧文字
@Prop rightIcon: Resource | undefined; // 右侧图标
@Prop onRightClick: () => void = () => {}; // 右侧点击事件
build() {
Row() {
// 返回按钮
if (showBack) {
Button() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
}
.width(44)
.height(44)
.backgroundColor(Color.Transparent)
.onClick(() => {
router.back();
})
} else {
Blank().width(44);
}
// 标题
Text(title)
.fontSize($r('app.dimension.title_bar_font_size'))
.fontWeight(FontWeight.Bold)
.color($r('app.color.title_bar_text_color'))
.flexGrow(1)
.textAlign(TextAlign.Center)
// 右侧按钮
if (rightText !== '' || rightIcon) {
Button() {
if (rightIcon) {
Image(rightIcon)
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
} else {
Text(rightText)
.fontSize($r('app.dimension.title_bar_right_font_size'))
.color($r('app.color.title_bar_right_text_color'))
}
}
.width(44)
.height(44)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.onRightClick();
})
} else {
Blank().width(44);
}
}
.width('100%')
.height($r('app.dimension.title_bar_height'))
.backgroundColor($r('app.color.title_bar_bg_color'))
.padding({ top: $r('app.dimension.title_bar_top_padding') })
.alignItems(Alignment.Center)
}
}
3.4.2 加载对话框组件(LoadingDialog.ets)
@Component
export struct LoadingDialog {
@Link isShow: boolean; // 是否显示
@Prop message: string = '处理中...'; // 提示信息
build() {
if (this.isShow) {
Stack() {
// 遮罩层
Decorator()
.width('100%')
.height('100%')
.backgroundColor($r('app.color.dialog_mask_color'))
.onClick(() => {
// 点击遮罩不关闭
})
// 对话框内容
Column() {
Progress()
.type(ProgressType.Circular)
.width(40)
.height(40)
.color($r('app.color.primary_color'))
Text(this.message)
.fontSize($r('app.dimension.loading_dialog_font_size'))
.color($r('app.color.loading_dialog_text_color'))
.margin({ top: 16 })
}
.width(120)
.height(120)
.backgroundColor($r('app.color.white'))
.borderRadius($r('app.dimension.dialog_border_radius'))
.padding(16)
.alignItems(Alignment.Center)
}
.position({ left: 0, top: 0 })
.zIndex(9999)
}
}
}
3.4.3 权限请求组件(PermissionRequest.ets)
import { Permissions } from '../constants/PermissionConstants';
import { PermissionUtils } from '../utils/PermissionUtils';
@Component
export struct PermissionRequest {
@Prop permissions: Permissions[]; // 需要请求的权限列表
@Link hasPermission: boolean; // 是否已获取权限
@Prop onGranted: () => void; // 权限授予后的回调
@Prop onDenied: () => void; // 权限拒绝后的回调
build() {
Column() {
if (!this.hasPermission) {
Column() {
Image($r('app.media.ic_permission'))
.width(60)
.height(60)
.margin({ bottom: 16 })
Text($r('app.string.permission_title'))
.fontSize($r('app.dimension.permission_title_font_size'))
.fontWeight(FontWeight.Bold)
.color($r('app.color.text_primary_color'))
.margin({ bottom: 8 })
Text($r('app.string.permission_desc'))
.fontSize($r('app.dimension.permission_desc_font_size'))
.color($r('app.color.text_secondary_color'))
.textAlign(TextAlign.Center)
.margin({ bottom: 24 })
.width('80%')
Button($r('app.string.request_permission'))
.width('60%')
.height($r('app.dimension.button_height'))
.backgroundColor($r('app.color.primary_color'))
.fontSize($r('app.dimension.button_font_size'))
.fontWeight(FontWeight.Medium)
.color($r('app.color.white'))
.borderRadius($r('app.dimension.button_border_radius'))
.onClick(async () => {
const granted = await PermissionUtils.requestPermissions(this.permissions);
if (granted) {
this.hasPermission = true;
this.onGranted();
} else {
this.onDenied();
}
})
Button($r('app.string.go_settings'), { type: ButtonType.Text })
.width('60%')
.height($r('app.dimension.button_height'))
.fontSize($r('app.dimension.button_font_size'))
.fontWeight(FontWeight.Medium)
.color($r('app.color.primary_color'))
.borderRadius($r('app.dimension.button_border_radius'))
.onClick(() => {
PermissionUtils.openPermissionSettings();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(Alignment.Center)
}
}
}
}
3.5项目初始化验证
1.配置应用入口:修改App.ets文件,设置应用入口页面为 MainPage
import router from '@ohos.router';
import { UIContext } from '@ohos/ui';
@Entry
@Component
struct App {
build() {
Router() {
Route()
.path('/pages/MainPage')
.builder(() => MainPage())
}
.onAppear(() => {
console.log('App started');
})
}
}
@Component
struct MainPage {
build() {
Column() {
TitleBar(title: $r('app.string.app_name'), showBack: false)
Blank().flexGrow(1)
Text($r('app.string.welcome_message'))
.fontSize($r('app.dimension.welcome_font_size'))
.fontWeight(FontWeight.Medium)
.color($r('app.color.text_primary_color'))
Blank().flexGrow(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.page_bg_color'))
}
}
2.运行项目
Android端模拟器
1.导入Kuikly-mini文件,打开外部模拟器,Android Studio会自动识别并连接外部模拟器
2.快捷键shift+F10运行
3.运行成功后效果如图所示

华为云真机端
1.打开DevEco Studio,打开ohosapp文件
2.配置证书和签名文件
3.打开Kuikly-mini文件,点击2.0_ohos_build.bat进行编译,生成.so文件
4.构建HAP包
5.调试云真机,上传.hap文件
6.运行成功结果如图所示


结语
本次实战以 ArkTS 与 Kuikly 混合开发模式,完成了 HarmonyOS 原生水印图片应用搭建。ArkTS 保障原生体验,Kuikly 提升跨平台开发效率,二者协同构建了高效复用的开发范式。项目可直接拓展跨平台支持或升级 AI 水印等功能,为 HarmonyOS 应用开发提供了实用参考。期待大家基于此探索更多生态创新,打造优质原生应用!
更多推荐

所有评论(0)