HarmonyOS NEXT TXT 阅读器开发:文件读取、编码处理与阅读进度实战

前言

TXT 文本阅读是文件管理应用中最基础也最常用的功能之一,看似简单却涉及文件 IO、字符编码、排版渲染和状态持久化等多项技术。本文基于 HarmonyExplorer 项目,深入讲解 TXT 阅读器(Text Viewer)页面的完整开发流程,包括 File Kit 文件读取、UTF-8/GBK 编码处理、字体字号设置、夜间模式切换、阅读进度保存、翻页与滚动、TextUtil 工具类和 TextViewer ViewModel 等核心技术。

一、TXT 阅读器架构设计

1.1 架构分层概述

TXT 阅读器采用 UI → ViewModel → Service → KitManager 分层架构,TextFileService 负责文件读取和编码处理,TextViewerViewModel 管理阅读状态和用户设置。

interface ReadingConfig {
  fontSize: number;
  fontFamily: string;
  lineHeight: number;
  letterSpacing: number;
  isNightMode: boolean;
  backgroundColor: string;
  textColor: string;
}

@Observed
class TextViewerViewModel {
  public filePath: string = '';
  public content: string = '';
  public charset: string = 'UTF-8';
  public config: ReadingConfig = {
    fontSize: 16, fontFamily: 'HarmonyOS Sans', lineHeight: 1.8,
    letterSpacing: 0.5, isNightMode: false,
    backgroundColor: '#FFFFFF', textColor: '#333333'
  };
  public scrollOffset: number = 0;
  public totalChars: number = 0;
}

1.2 状态管理设计

阅读器状态分为内容状态和设置状态两类,状态分类管理 便于独立维护和持久化。

提示:阅读设置应通过 AppStorage 全局共享,使设置变更在不同阅读页面间保持一致,同时通过 Preferences 持久化用户偏好。

二、File Kit 文件读取

2.1 文件读取服务

TextFileService 封装 File Kit 的 fs 模块,提供同步和异步两种文件读取方式。

import { fileIo as fs } from '@kit.CoreFileKit';

class TextFileService {
  public static async readTextFile(filePath: string): Promise<string> {
    const file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
    try {
      const stat: fs.Stat = fs.statSync(filePath);
      const buffer: ArrayBuffer = new ArrayBuffer(stat.size);
      fs.readSync(file.fd, buffer);
      const encoding: string = EncodingDetector.detectEncoding(buffer);
      const decoder: util.TextDecoder = new util.TextDecoder(encoding);
      const text: string = decoder.decodeWithStream(new Uint8Array(buffer));
      return text;
    } finally {
      fs.closeSync(file);
    }
  }
}

2.2 文件读取策略

不同大小的文本文件需要采用不同的读取策略,平衡加载速度和内存占用。

文件大小 读取策略 内存占用 加载速度
< 1MB 全量读取 极快
1-10MB 全量读取
10-50MB 分块读取
> 50MB 分块+索引 极低

三、文本编码处理

3.1 编码检测实现

中文文本文件可能使用 UTF-8 或 GBK 编码,需要通过 BOM 头和字节特征自动检测编码格式。

import { util } from '@kit.ArkTS';

class EncodingDetector {
  public static detectEncoding(buffer: ArrayBuffer): string {
    const bytes: Uint8Array = new Uint8Array(buffer);
    if (bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
      return 'UTF-8';
    }
    if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) {
      return 'UTF-16LE';
    }
    if (EncodingDetector.isUtf8(bytes)) { return 'UTF-8'; }
    return 'GBK';
  }

  private static isUtf8(bytes: Uint8Array): boolean {
    let i: number = 0;
    while (i < bytes.length) {
      const byte: number = bytes[i];
      if (byte < 0x80) { i++; }
      else if (byte >= 0xC0 && byte < 0xE0) {
        if (i + 1 >= bytes.length || (bytes[i + 1] & 0xC0) !== 0x80) { return false; }
        i += 2;
      } else if (byte >= 0xE0 && byte < 0xF0) {
        if (i + 2 >= bytes.length) { return false; }
        if ((bytes[i + 1] & 0xC0) !== 0x80 || (bytes[i + 2] & 0xC0) !== 0x80) { return false; }
        i += 3;
      } else { return false; }
    }
    return true;
  }
}

3.2 编码转换处理

检测到编码后使用对应的 TextDecoder 解码,GBK 编码需要指定正确的编码名称。

  1. 读取文件前 3 字节判断是否存在 UTF-8 BOM 头
  2. 检查前 2 字节是否为 UTF-16LE 的 BOM 标识
  3. 通过字节特征验证判断是否为合法 UTF-8 编码
  4. 以上条件均不满足时默认使用 GBK 编码解码

提示:部分旧版 TXT 文件可能使用 GB2312 或 GB18030 编码,这些编码与 GBK 兼容,可以使用 GBK 解码器统一处理。

四、字体设置功能

4.1 字体选择实现

阅读器支持多种字体选择,通过 fontFamily 属性切换。

@Builder
buildFontSettings(): void {
  Row() {
    ForEach(FontConfig.getAvailableFonts(), (font: FontOption) => {
      Text(font.displayName).fontSize(14)
        .fontColor(this.viewModel.config.fontFamily === font.value ? '#007DFF' : '#333333')
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .borderRadius(16)
        .backgroundColor(this.viewModel.config.fontFamily === font.value ? '#E6F0FF' : '#F5F5F5')
        .margin({ right: 8 })
        .onClick(() => {
          this.viewModel.config.fontFamily = font.value;
          this.saveReadingConfig();
        })
    })
  }.width('100%').padding(16)
}

class FontConfig {
  public static getAvailableFonts(): Array<FontOption> {
    return [
      { displayName: '默认', value: 'HarmonyOS Sans' },
      { displayName: '宋体', value: 'serif' },
      { displayName: '黑体', value: 'sans-serif' },
      { displayName: '等宽', value: 'monospace' }
    ];
  }
}

interface FontOption { displayName: string; value: string; }

4.2 字体加载优化

系统字体直接使用无需加载,自定义字体通过 FontRegistry 注册并缓存到沙箱目录,首次使用时显示加载进度。

五、字号调整功能

5.1 字号控制

字号调整通过修改 ViewModel 的 fontSize 属性实现,支持增大、减小和重置三种操作。

class FontSizeController {
  private static readonly MIN_SIZE: number = 12;
  private static readonly MAX_SIZE: number = 32;
  private static readonly STEP: number = 2;

  public static increase(viewModel: TextViewerViewModel): void {
    viewModel.config.fontSize = Math.min(viewModel.config.fontSize + FontSizeController.STEP, FontSizeController.MAX_SIZE);
  }

  public static decrease(viewModel: TextViewerViewModel): void {
    viewModel.config.fontSize = Math.max(viewModel.config.fontSize - FontSizeController.STEP, FontSizeController.MIN_SIZE);
  }
}

5.2 字号适配说明

字号范围 适用场景 行高建议 每行字数
12-14 信息密集型 1.6 40-50
15-18 默认阅读 1.8 30-40
19-24 大字阅读 2.0 20-30
25-32 老人模式 2.2 15-20

六、夜间模式切换

6.1 夜间模式实现

夜间模式通过切换背景色和文字色实现,降低屏幕亮度,保护用户视力。

class ThemeController {
  private static readonly DAY_BG: string = '#FFFFFF';
  private static readonly DAY_TEXT: string = '#333333';
  private static readonly NIGHT_BG: string = '#1A1A1A';
  private static readonly NIGHT_TEXT: string = '#999999';

  public static toggleNightMode(viewModel: TextViewerViewModel): void {
    viewModel.config.isNightMode = !viewModel.config.isNightMode;
    if (viewModel.config.isNightMode) {
      viewModel.config.backgroundColor = ThemeController.NIGHT_BG;
      viewModel.config.textColor = ThemeController.NIGHT_TEXT;
    } else {
      viewModel.config.backgroundColor = ThemeController.DAY_BG;
      viewModel.config.textColor = ThemeController.DAY_TEXT;
    }
  }
}

6.2 主题切换动画

夜间模式切换时添加渐变动画,避免突兀的颜色变化影响阅读体验。平滑过渡 提升了用户体验。

提示:夜间模式下除了文字和背景色,还应调整进度条、按钮等 UI 元素的颜色,保持整体视觉一致性。

七、阅读进度保存

7.1 进度持久化实现

阅读进度通过 Preferences 持久化存储,记录每个文件的滚动位置和阅读百分比。

import { preferences } from '@kit.ArkData';

class ReadingProgressManager {
  private static readonly PREF_NAME: string = 'text_reading_progress';

  public static async saveProgress(
    context: Context, filePath: string, scrollOffset: number, totalChars: number
  ): Promise<void> {
    const prefs: preferences.Preferences =
      await preferences.getPreferences(context, ReadingProgressManager.PREF_NAME);
    const key: string = ReadingProgressManager.generateKey(filePath);
    const progress: ReadingProgress = {
      filePath: filePath, scrollOffset: scrollOffset,
      percentage: totalChars > 0 ? scrollOffset / totalChars : 0,
      saveTime: Date.now()
    };
    await prefs.put(key, JSON.stringify(progress));
    await prefs.flush();
  }

  public static async getProgress(
    context: Context, filePath: string
  ): Promise<ReadingProgress | null> {
    const prefs: preferences.Preferences =
      await preferences.getPreferences(context, ReadingProgressManager.PREF_NAME);
    const key: string = ReadingProgressManager.generateKey(filePath);
    const hasKey: boolean = await prefs.has(key);
    if (!hasKey) { return null; }
    const jsonStr: string = await prefs.get(key, '') as string;
    if (jsonStr.length === 0) { return null; }
    return JSON.parse(jsonStr);
  }

  private static generateKey(filePath: string): string {
    let hash: number = 0;
    for (let i = 0; i < filePath.length; i++) {
      hash = ((hash << 5) - hash) + filePath.charCodeAt(i);
      hash = hash & hash;
    }
    return 'text_' + hash;
  }
}

7.2 进度恢复流程

打开文件时自动检查并恢复阅读进度:加载文件内容,查询 Preferences 进度,通过 Scroller.scrollTo 恢复位置,进度无效则从开头开始。

八、翻页与滚动模式

8.1 滚动模式实现

滚动模式使用 Scroll 组件实现文本的垂直滚动浏览,是最常见的阅读方式。

@Entry
@Component
struct TextViewerPage {
  @State viewModel: TextViewerViewModel = new TextViewerViewModel();
  private scroller: Scroller = new Scroller();

  build(): void {
    Stack() {
      Column() {
        Row() {
          Image('resources/back_icon.png').width(24).height(24)
            .onClick(() => RouterUtil.back())
          Text(this.viewModel.fileName).fontSize(16).layoutWeight(1).margin({ left: 8 })
          Image('resources/font_icon.png').width(24).height(24)
            .onClick(() => this.showSettingsPanel())
          Image(this.viewModel.config.isNightMode ? 'resources/mode_day.png' : 'resources/mode_night.png')
            .width(24).height(24)
            .onClick(() => ThemeController.toggleNightMode(this.viewModel))
        }.width('100%').height(48).padding({ left: 16, right: 16 })
        Scroll(this.scroller) {
          Text(this.viewModel.content)
            .fontSize(this.viewModel.config.fontSize)
            .fontFamily(this.viewModel.config.fontFamily)
            .fontColor(this.viewModel.config.textColor)
            .lineHeight(this.viewModel.config.fontSize * this.viewModel.config.lineHeight)
            .letterSpacing(this.viewModel.config.letterSpacing)
            .padding({ left: 20, right: 20, top: 16, bottom: 48 }).width('100%')
        }.width('100%').layoutWeight(1)
        .backgroundColor(this.viewModel.config.backgroundColor).scrollBar(BarState.Off)
        .onScroll((xOffset: number, yOffset: number) => { this.viewModel.scrollOffset += yOffset; })
      }
    }.width('100%').height('100%').backgroundColor(this.viewModel.config.backgroundColor)
  }
}

8.2 翻页模式实现

阅读模式 交互方式 优势 劣势
滚动模式 上下滑动 连续性好 长文本定位难
翻页模式 左右滑动 阅读感强 分页计算复杂
覆盖翻页 左右覆盖 过渡自然 渲染开销大

九、TextUtil 工具类

9.1 文本处理工具

TextUtil 封装了文本相关的工具方法,包括分页计算、字数统计、编码检测等功能。

class TextUtil {
  public static countChars(text: string): number {
    let count: number = 0;
    for (let i = 0; i < text.length; i++) {
      const char: string = text.charAt(i);
      if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
        count++;
      }
    }
    return count;
  }

  public static countLines(text: string): number {
    let lines: number = 1;
    for (let i = 0; i < text.length; i++) {
      if (text.charAt(i) === '\n') { lines++; }
    }
    return lines;
  }
}

9.2 工具类设计原则

TextUtil 采用静态方法设计,无状态依赖,方便全局调用。工具类 应保持纯粹性,不包含业务逻辑。

十、TextViewer ViewModel 完整实现

10.1 ViewModel 状态管理

TextViewerViewModel 统一管理文本内容、阅读设置和阅读进度,是 TXT 阅读器的核心控制器

@Observed
class TextViewerViewModel {
  public filePath: string = '';
  public fileName: string = '';
  public content: string = '';
  public charset: string = 'UTF-8';
  public config: ReadingConfig = new ReadingConfig();
  public scrollOffset: number = 0;
  public totalChars: number = 0;
  public isLoading: boolean = false;
  private repository: FileRepository = new FileRepository();

  public async loadFile(filePath: string): Promise<void> {
    this.isLoading = true;
    this.filePath = filePath;
    this.fileName = FileUtil.getFileName(filePath);
    try {
      this.content = await TextFileService.readTextFile(filePath);
      this.totalChars = TextUtil.countChars(this.content);
    } catch (error) {
      LogUtil.error('文件加载失败: ' + error);
    } finally {
      this.isLoading = false;
    }
  }

  public async saveConfig(context: Context): Promise<void> {
    await PreferenceUtil.putObject(context, 'reading_config', this.config);
  }
}

10.2 生命周期管理

ViewModel 的生命周期管理涉及以下关键节点:

  • 页面 onHide 时保存阅读进度和配置
  • 页面 onShow 时恢复进度和配置
  • aboutToDisappear 时释放文本资源

提示:页面 onHide 或 aboutToDisappear 时应保存阅读进度和配置,页面 onShow 时恢复进度和配置。

十一、设置面板实现

11.1 设置面板布局

设置面板以底部弹窗形式展示,包含字体、字号、行距和夜间模式等设置项。

@Builder
buildSettingsPanel(): void {
  Column() {
    Row().width(40).height(4).backgroundColor('#DDDDDD').borderRadius(2)
      .margin({ top: 8, bottom: 16 })
    Row() {
      Text('字号').fontSize(14).fontColor('#333333')
      Blank()
      Text('A').fontSize(12).width(32).height(32).textAlign(TextAlign.Center)
        .backgroundColor('#F5F5F5').borderRadius(16)
        .onClick(() => FontSizeController.decrease(this.viewModel))
      Text(this.viewModel.config.fontSize.toString()).fontSize(14).width(48).textAlign(TextAlign.Center)
      Text('A').fontSize(18).width(32).height(32).textAlign(TextAlign.Center)
        .backgroundColor('#F5F5F5').borderRadius(16)
        .onClick(() => FontSizeController.increase(this.viewModel))
    }.width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
    Divider().color('#F0F0F0')
    Row() {
      Text('夜间模式').fontSize(14).fontColor('#333333')
      Blank()
      Toggle({ type: ToggleType.Switch, isOn: this.viewModel.config.isNightMode })
        .onChange((isOn: boolean) => { ThemeController.toggleNightMode(this.viewModel); })
    }.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
  }.width('100%').backgroundColor(this.viewModel.config.backgroundColor).borderRadius({ topLeft: 16, topRight: 16 })
}

在这里插入图片描述
在这里插入图片描述

TXT 阅读器页面效果展示,支持字号调整和夜间模式

十二、性能优化与总结

12.1 性能优化要点

  1. 大文件采用分块加载和虚拟列表渲染
  2. 字号和字体变更使用防抖避免频繁重绘
  3. 阅读进度保存使用节流减少 IO 操作

12.2 功能扩展方向

扩展功能 实现方案 技术依赖
书签功能 Preferences 存储书签 Preferences
全文搜索 文本索引与高亮 自定义搜索
TTS 朗读 语音合成服务 Audio Kit

提示:超大文本文件(超过 10MB)应采用分块加载策略,避免一次性读取导致内存溢出。

总结

本文基于 HarmonyExplorer 项目完整讲解了 HarmonyOS NEXT TXT 阅读器的开发流程,涵盖了 File Kit 文件读取、UTF-8/GBK 编码处理、字体字号设置、夜间模式切换、阅读进度保存、翻页与滚动、TextUtil 工具类和 TextViewer ViewModel 等核心功能。通过合理的架构设计和性能优化策略,开发者可以构建出体验优秀的文本阅读应用。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关资源

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐