欢迎加入开源鸿蒙跨平台社区: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 关键配置要点

  1. DevEco Studio 需启用 Kotlin 插件,配置 KuiklyUI 仓库地址(在init.gradle中添加仓库链接)

  2. 确保 HarmonyOS SDK 已安装 API 10 + 相关模块(SDK Platform、SDK Tools)

  3. 真机 / 模拟器需正常连接,开发者模式与 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 的observableobservableList实现响应式数据,如水印配置、图片列表等

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 应用开发提供了实用参考。期待大家基于此探索更多生态创新,打造优质原生应用!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐