HarmonyOS APP实战-基于Image Kit的图像处理APP - 第11篇:数据持久化与配置存储

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条),避免因频繁操作导致存储过大。
  • HistoryRecorduri字段记录图片路径,便于后续点击跳转回编辑页重新打开时识别图片。

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' });
  })

同时,在EntryAbilityonCreateonForeground中初始化historyManager

typescript 复制代码
// EntryAbility.ets
import { historyManager } from '../utils/PreferenceManager';

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  // ...
  historyManager.init(this.context);
}

完成上述步骤后,启动APP,进行任意编辑操作并保存,再进入编辑历史页面,即可看到记录。


3. 运行验证

  1. 打开APP,选择一张图片进入编辑页。
  2. 进行一次操作,例如应用一个灰度滤镜,然后点击保存(提示保存成功)。
  3. 进入编辑历史页面(通过主菜单或编辑页返回后点击"历史"按钮)。
  4. 页面显示最新一条记录,包含"滤镜"操作、图片路径部分信息和保存时间。
  5. 对同一张图片或另一张图片进行水印叠加,保存后再进入历史页,列表新增一条水印记录,并按时间降序排列。
  6. 点击某条记录后的"删除"按钮,弹出确认框,确认后该条消失。
  7. 点击"清空记录",所有记录被删除,列表变为空提示。

预期表现:所有历史记录持久化存储,关闭APP再打开依然存在。删除操作彻底移除指定项。


4. 小结与预告

本篇为APP增加了编辑历史本地存储能力,核心产出包括:

  • PreferenceManager单例类,封装了基于@ohos.data.preferences的增删查操作。
  • EditHistoryPage完整页面,展示按时间排序的历史列表,支持单条删除和清空。
  • 历史记录自动捕获每次编辑保存动作,与第10篇的水印功能无缝集成。

这一功能使用户能回溯编辑过程,也为后续跨设备同步或分享编辑参数提供了数据基础。

下一篇《性能优化与资源释放》将分析图片处理中的内存泄漏与性能瓶颈,重点优化大图加载(采样率控制)、及时释放PixelMap资源、避免重复解码等,确保APP在高频编辑场景下依然流畅稳定。

相关推荐
特立独行的猫a2 小时前
Node.js三方库鸿蒙 PC 移植:鸿蒙PC开发者面对面 · 线下课件分享
华为·harmonyos·三方库移植·鸿蒙pc
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第23篇:弹性动效与手势 — EdgeEffect.Spring 与手势系统实战
spring·华为·harmonyos
l134062082354 小时前
鸿蒙应用开发实战【88】— DevEco Studio高效开发技巧
华为·harmonyos·鸿蒙系统
一缕清烟在人间4 小时前
鸿蒙应用开发实战【54】— 应用详情页AppDetailPage开发(下)
华为·harmonyos·鸿蒙系统
l134062082355 小时前
鸿蒙应用开发实战【85】— 应用签名与正式HAP打包
华为·harmonyos·鸿蒙系统
爸爸6196 小时前
鸿蒙应用开发实战【06】— 应用入口与生命周期管理(EntryAbility 深度解析)
华为·harmonyos·鸿蒙系统
不羁的木木6 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第8篇:智能家居控制——手机控制智能灯实战
智能手机·智能家居·harmonyos
用户0934077735146 小时前
HarmonyOS WPS Open SDK 实践:用能力分层重构 OpenFileRequest
typescript·harmonyos
春卷同学7 小时前
HarmonyOS掌上记账APP开发实践第21篇:Tabs 底部导航 — 鸿蒙应用的标准导航范式
华为·harmonyos
子非鱼94277 小时前
03-Flutter 鸿蒙实战 03:登录注册与本地会话持久化
网络·flutter·华为·harmonyos