HarmonyOS APP实战-基于Image Kit的图像处理APP - 第11篇:编辑记录本地存储
1. 开篇
上一篇我们完成了水印与文字叠加功能:通过Canvas绘制在PixelMap上,支持用户输入文本、选择颜色和位置拖动,最终生成带水印的新图片。核心代码封装为WatermarkOverlay组件,嵌入图片编辑页面,与缩放、旋转、滤镜等功能并列。用户点击保存后,水印会永久写入图片。
图片处理APP的功能已近完整,但每次编辑后的操作(缩放、裁剪、滤镜、水印等)都未记录,用户无法追踪历史操作。本篇将使用本地持久化存储,记录每次编辑的源图路径、操作类型和时间戳,并在独立的历史页面中展示列表,支持单条删除。这样用户既能回顾编辑历程,也能清理无用记录,为后续批量导出或分享提供依据。
2. 核心实现
2.1 基础配置:引入数据存储模块
在pages目录下新增EditHistoryPage.ets,同时在utils中创建PreferenceManager.ts,封装@ohos.data.preferences的常用操作。需要先在module.json5中声明权限(Preferences无需额外权限,但涉及文件路径读取仍需ohos.permission.READ_MEDIA,此权限已在系列第2篇中配置,此处复用)。
typescript
// utils/PreferenceManager.ts
import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
// 本地存储文件名
const STORE_NAME = 'edit_history_store';
// 存储记录的 key
const HISTORY_KEY = 'edit_history_list';
// 单条历史记录的数据结构
export interface HistoryRecord {
uri: string; // 图片路径(临时文件或相册URI)
operationType: string; // 操作类型:scale, crop, rotate, filter, brightness, watermark 等
timestamp: number; // 时间戳(毫秒)
thumbnail?: string; // 缩略图base64(可选,减小存储压力)
}
export class PreferenceManager {
private pref: preferences.Preferences | null = null;
// 初始化存储实例
async init(context: Context): Promise<void> {
try {
this.pref = await preferences.getPreferences(context, STORE_NAME);
console.info('PreferenceManager init success');
} catch (err) {
let e = err as BusinessError;
console.error(`PreferenceManager init failed, code: ${e.code}, message: ${e.message}`);
}
}
// 获取所有历史记录
async getHistoryRecords(): Promise<HistoryRecord[]> {
if (!this.pref) return [];
try {
const jsonStr = await this.pref.get(HISTORY_KEY, '[]') as string;
if (!jsonStr) return [];
return JSON.parse(jsonStr) as HistoryRecord[];
} catch (err) {
console.error('Failed to get history records:', err);
return [];
}
}
// 添加一条记录
async addRecord(record: HistoryRecord): Promise<void> {
if (!this.pref) return;
const records = await this.getHistoryRecords();
records.push(record); // 追加到尾部(最新记录在尾部)
// 只保留最近100条,避免数据无限膨胀
const trimmed = records.length > 100 ? records.slice(-100) : records;
await this.pref.put(HISTORY_KEY, JSON.stringify(trimmed));
await this.pref.flush();
}
// 删除某条记录(通过uri+timestamp唯一标识)
async deleteRecord(uri: string, timestamp: number): Promise<void> {
if (!this.pref) return;
const records = await this.getHistoryRecords();
const filtered = records.filter(item => !(item.uri === uri && item.timestamp === timestamp));
await this.pref.put(HISTORY_KEY, JSON.stringify(filtered));
await this.pref.flush();
}
// 清空所有记录
async clearAllRecords(): Promise<void> {
if (!this.pref) return;
await this.pref.put(HISTORY_KEY, '[]');
await this.pref.flush();
}
}
// 全局单例
export const historyManager = new PreferenceManager();
关键点说明
preferences.getPreferences(context, name):创建/打开一个轻量级存储实例,返回promise。preferences.Preferences.put(key, value):写入键值对,value支持string/number/boolean/ArrayBuffer等,此处使用JSON序列化后的字符串。flush():将内存数据同步写入持久化文件,必须调用才能保证断电不丢。- 记录用数组存储,写入时做截断(保留最近100条),避免因频繁操作导致存储过大。
HistoryRecord的uri字段记录图片路径,便于后续点击跳转回编辑页重新打开时识别图片。
2.2 核心逻辑:在编辑完成后自动记录历史
在图片处理流程中,每当用户完成一次编辑(如缩放到指定值、应用滤镜、保存水印图片等),都需要调用historyManager.addRecord。以第10篇的水印保存为例,在保存完成回调中添加记录。
typescript
// pages/ImageEditPage.ets(片段,演示集成位置)
import { historyManager, HistoryRecord } from '../utils/PreferenceManager';
// 假设第10篇中保存水印图片的函数
async function saveWatermarkImage(finalPixelMap: PixelMap) {
// ... 编码并保存到相册的原有逻辑
// 保存成功后,记录历史
const currentTime = Date.now();
const record: HistoryRecord = {
uri: savedUri, // 实际保存后的图片URI(从fileUri或相册返回)
operationType: 'watermark',
timestamp: currentTime,
thumbnail: undefined // 可扩展,此处略
};
await historyManager.addRecord(record);
}
同理,在其他编辑操作(缩放、裁剪、旋转、滤镜、亮度调整)的保存回调中,也需插入类似代码。为降低耦合,可以在一个公共的handleSave()方法中集中处理记录逻辑。
typescript
// 统一保存方法(示例)
async function handleSave(finalPixelMap: PixelMap, operation: string) {
// 编码保存...
const savedUri = await savePixelMapToFile(finalPixelMap);
// 记录历史
const record: HistoryRecord = {
uri: savedUri,
operationType: operation,
timestamp: Date.now()
};
await historyManager.addRecord(record);
// 提示保存成功
promptAction.showToast({ message: `已保存并记录${operation}操作` });
}
关键点说明
operationType使用字符串标识,例如'scale', 'crop', 'rotate', 'filter', 'brightness', 'watermark',可在常量文件中定义枚举。- 注意每次保存的
uri是完整的文件路径(如file://...),历史页面点击后可用来重新加载。 - 记录时间使用
Date.now()毫秒数,展示时格式化显示。
2.3 完整页面:EditHistoryPage
新建pages/EditHistoryPage.ets,展示历史记录列表,支持单条删除和清空。
typescript
// pages/EditHistoryPage.ets
import { historyManager, HistoryRecord } from '../utils/PreferenceManager';
import router from '@ohos.router';
@Entry
@Component
struct EditHistoryPage {
@State private records: HistoryRecord[] = [];
@State private loading: boolean = true;
aboutToAppear() {
this.loadRecords();
}
async loadRecords() {
this.loading = true;
const data = await historyManager.getHistoryRecords();
// 按时间戳降序排列(最新在前面)
data.sort((a, b) => b.timestamp - a.timestamp);
this.records = data;
this.loading = false;
}
// 删除单条
async deleteItem(record: HistoryRecord) {
AlertDialog.show({
title: '删除确认',
message: `确定删除 ${this.formatOperation(record.operationType)} 记录?`,
primaryButton: {
value: '取消',
action: () => {}
},
secondaryButton: {
value: '删除',
action: async () => {
await historyManager.deleteRecord(record.uri, record.timestamp);
this.loadRecords(); // 刷新列表
}
}
});
}
// 清空全部
async clearAll() {
AlertDialog.show({
title: '清空确认',
message: '将删除所有历史记录,不可恢复',
primaryButton: {
value: '取消',
action: () => {}
},
secondaryButton: {
value: '清空',
action: async () => {
await historyManager.clearAllRecords();
this.loadRecords();
}
}
});
}
// 将操作类型转换为中文显示
formatOperation(type: string): string {
const map: Record<string, string> = {
'scale': '缩放',
'crop': '裁剪',
'rotate': '旋转',
'flip': '翻转',
'filter': '滤镜',
'brightness': '亮度对比度',
'watermark': '水印'
};
return map[type] || type;
}
// 格式化时间
formatTime(ts: number): string {
const date = new Date(ts);
const pad = (n: number): string => n < 10 ? '0' + n : String(n);
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('编辑历史')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.lineHeight(40)
.margin({ left: 16 })
Blank()
Button('清空记录')
.type(ButtonType.Normal)
.fontColor('#ff0000')
.fontSize(14)
.backgroundColor(Color.Transparent)
.onClick(() => this.clearAll())
}
.width('100%')
.height(56)
.backgroundColor('#f5f5f5')
// 列表内容
if (this.loading) {
LoadingProgress()
.width(48)
.height(48)
.margin({ top: 100 })
} else if (this.records.length === 0) {
Text('暂无编辑记录')
.fontSize(16)
.fontColor('#999')
.margin({ top: 100 })
} else {
List({ space: 8 }) {
ForEach(this.records, (item: HistoryRecord, index: number) => {
ListItem() {
Row() {
// 图标(可选)
Image($r('app.media.app_icon')) // 占位图标
.width(40)
.height(40)
.borderRadius(4)
.margin({ right: 12 })
Column() {
Text(this.formatOperation(item.operationType))
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(item.uri.length > 30 ? item.uri.substring(0, 30) + '...' : item.uri)
.fontSize(12)
.fontColor('#888')
.maxLines(1)
Text(this.formatTime(item.timestamp))
.fontSize(11)
.fontColor('#aaa')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Button('删除')
.type(ButtonType.Normal)
.fontColor('#ff3333')
.fontSize(13)
.backgroundColor(Color.Transparent)
.onClick(() => this.deleteItem(item))
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
}
})
}
.width('100%')
.padding(12)
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#f0f0f0')
}
}
关键点说明
- 使用
@State装饰器使列表响应式更新,每次增删后调用loadRecords()重新加载。 - 排序采用降序,最新记录显示在最上面。
- 删除操作前弹出确认对话框,避免误删。
formatOperation将内部类型标识映射为中文,便于用户理解。formatTime将毫秒时间戳格式化为'yyyy-MM-dd HH:mm:ss'。- 列表项的URI太长时截断显示,完整URI可通过点击记录跳转到编辑页时加载(功能可后续扩展)。
2.4 集成到页面路由
在主页面添加一个按钮跳转到历史页:
typescript
// 例如在主页面的菜单中
Button('编辑历史')
.onClick(() => {
router.pushUrl({ url: 'pages/EditHistoryPage' });
})
同时,在EntryAbility的onCreate或onForeground中初始化historyManager:
typescript
// EntryAbility.ets
import { historyManager } from '../utils/PreferenceManager';
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// ...
historyManager.init(this.context);
}
完成上述步骤后,启动APP,进行任意编辑操作并保存,再进入编辑历史页面,即可看到记录。
3. 运行验证
- 打开APP,选择一张图片进入编辑页。
- 进行一次操作,例如应用一个灰度滤镜,然后点击保存(提示保存成功)。
- 进入编辑历史页面(通过主菜单或编辑页返回后点击"历史"按钮)。
- 页面显示最新一条记录,包含"滤镜"操作、图片路径部分信息和保存时间。
- 对同一张图片或另一张图片进行水印叠加,保存后再进入历史页,列表新增一条水印记录,并按时间降序排列。
- 点击某条记录后的"删除"按钮,弹出确认框,确认后该条消失。
- 点击"清空记录",所有记录被删除,列表变为空提示。
预期表现:所有历史记录持久化存储,关闭APP再打开依然存在。删除操作彻底移除指定项。

4. 小结与预告
本篇为APP增加了编辑历史本地存储能力,核心产出包括:
PreferenceManager单例类,封装了基于@ohos.data.preferences的增删查操作。EditHistoryPage完整页面,展示按时间排序的历史列表,支持单条删除和清空。- 历史记录自动捕获每次编辑保存动作,与第10篇的水印功能无缝集成。
这一功能使用户能回溯编辑过程,也为后续跨设备同步或分享编辑参数提供了数据基础。
下一篇《性能优化与资源释放》将分析图片处理中的内存泄漏与性能瓶颈,重点优化大图加载(采样率控制)、及时释放PixelMap资源、避免重复解码等,确保APP在高频编辑场景下依然流畅稳定。