前言

各位开发者,大家好!我是若城。

在鸿蒙应用开发过程中,我发现许多组件样式和工具方法具有高度的复用性,但每次新项目都需要重复编写,这极大地降低了开发效率。因此,我决定投入半年时间,打造一款专为鸿蒙生态设计的 UI 组件库 —— rchoui

项目简介

rchoui 是一个面向 HarmonyOS6 的企业级 UI 组件库,旨在提供开箱即用的高质量组件,让开发者告别"重复造轮子"。

核心特性

  • 丰富组件:涵盖基础组件、表单组件、弹窗组件、布局组件等
  • 设计规范:遵循统一的色彩体系和设计语言
  • 工具集成:内置常用工具方法,提升开发效率
  • 完善文档:每个模块都配有详细的设计思路和使用说明

开源计划

项目预计于 2026 年 7 月中旬正式开源,届时可通过三方库直接下载使用。在此期间,我会通过系列文章逐一介绍每个模块的设计思路与实现细节。

rchoui官网

目前暂定 rchoui 官网地址:http://rchoui.ruocheng.site/

需要注意的是 当前官网还在完善当中, 会在后续更新中逐步完善。届时可以为大家提供更加完善的说明文档

文档概述

本文深入剖析 RcImage 组件的预览功能实现机制,探讨如何通过弹窗系统、缩放控制、图片切换等交互设计,打造媲美原生相册应用的图片预览体验,同时分析事件回调系统的设计思路。


注意:当前的图片预览可能存在层级遮挡问题, 后期会逐步优化

第一章: 预览功能架构设计

1.1 预览功能配置体系

RcImage 的预览功能通过多层配置参数实现:

// 1. 基础预览开关
@Param previewable: boolean = false

// 2. 预览配置对象
@Param previewOptions: RcImagePreviewOptions = {}

// 3. 图片列表支持
@Param previewList: Array<string | Resource> = []
@Param previewIndex: number = 0

// 4. 预览事件回调
@Param onPreviewOpen: () => void = () => {}
@Param onPreviewClose: () => void = () => {}

1.2 预览配置接口设计

/**
 * 图片预览配置
 */
export interface RcImagePreviewOptions {
  /**
   * 是否显示遮罩层
   * @default true
   */
  showMask?: boolean

  /**
   * 是否显示关闭按钮
   * @default true
   */
  showClose?: boolean

  /**
   * 初始缩放比例
   * @default 1
   */
  initialScale?: number

  /**
   * 最小缩放比例
   * @default 0.5
   */
  minScale?: number

  /**
   * 最大缩放比例
   * @default 3
   */
  maxScale?: number

  /**
   * 关闭回调
   */
  onClose?: () => void
}

配置特点:

  • 渐进式: 仅 previewable: true 即可启用基础预览
  • 可选配置: 所有高级配置都有合理默认值
  • 类型安全: 接口定义确保配置正确性

1.3 预览状态管理

/**
 * 预览相关状态
 */
@Local showPreviewDialog: boolean = false      // 是否显示预览弹窗
@Local currentPreviewIndex: number = 0         // 当前预览图片索引
@Local previewScale: number = 1                // 当前缩放比例

状态协调机制:

点击图片
    ↓
previewable && loadStatus === 'success'
    ↓
调用 openPreview()
    ↓
┌─────────────────────────────────┐
│ 1. currentPreviewIndex = previewIndex │
│ 2. previewScale = initialScale || 1   │
│ 3. showPreviewDialog = true            │
│ 4. 触发 onPreviewOpen 回调              │
└─────────────────────────────────┘
    ↓
显示预览弹窗

第二章: 预览触发机制

2.1 点击事件处理流程

/**
 * 处理图片点击
 */
private handleImageClick() {
  // 1. 如果可预览且加载成功,先打开预览
  if (this.previewable && this.loadStatus === 'success') {
    this.openPreview()
  }

  // 2. 然后触发自定义点击回调
  if (this.onImageClick) {
    this.onImageClick()
  }
}

触发条件判断:

条件 说明 结果
previewable === false 未启用预览 仅触发 onImageClick
loadStatus !== 'success' 图片未加载成功 仅触发 onImageClick
previewable === true && loadStatus === 'success' 满足预览条件 先打开预览,再触发 onImageClick

2.2 打开预览方法实现

/**
 * 打开预览
 */
private openPreview() {
  // 1. 初始化预览索引(从参数获取)
  this.currentPreviewIndex = this.previewIndex
  
  // 2. 初始化缩放比例(从配置获取,默认 1)
  this.previewScale = this.previewOptions.initialScale || 1
  
  // 3. 显示预览弹窗
  this.showPreviewDialog = true

  // 4. 触发打开回调
  if (this.onPreviewOpen) {
    this.onPreviewOpen()
  }
}

初始化策略:

  • 索引定位: 使用 previewIndex 定位当前图片在列表中的位置
  • 缩放重置: 每次打开预览都重置为初始缩放比例
  • 状态同步: 确保 UI 状态与数据状态一致

2.3 关闭预览方法实现

/**
 * 关闭预览
 */
private closePreview() {
  // 1. 隐藏预览弹窗
  this.showPreviewDialog = false
  
  // 2. 重置缩放比例
  this.previewScale = 1

  // 3. 触发组件级关闭回调
  if (this.onPreviewClose) {
    this.onPreviewClose()
  }

  // 4. 触发配置级关闭回调
  if (this.previewOptions.onClose) {
    this.previewOptions.onClose()
  }
}

双重回调机制:

  • onPreviewClose: 组件参数级回调,用于组件外部监听
  • previewOptions.onClose: 配置对象级回调,用于细粒度控制

第三章: 预览弹窗UI实现

3.1 弹窗层级结构

@Builder
renderPreviewDialog() {
  if (this.showPreviewDialog) {
    Stack() {
      // 层级1: 遮罩层(最底层)
      if (this.previewOptions.showMask !== false) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0, 0, 0, 0.8)')
          .onClick(() => this.closePreview())
      }

      // 层级2: 预览图片(中间层)
      Column() {
        Image(this.getCurrentPreviewImage())
          .width('100%')
          .height('100%')
          .objectFit(ImageFit.Contain)
          .scale({ x: this.previewScale, y: this.previewScale })
          .animation({
            duration: 200,
            curve: Curve.EaseInOut
          })
      }
      .width('90%')
      .height('70%')

      // 层级3: 控制按钮(最上层)
      Column() {
        // 关闭、缩放、切换按钮
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .position({ x: 0, y: 0 })
    .zIndex(1000)  // 确保在最上层
  }
}

层级设计理念:

  • 遮罩层: 半透明黑色背景,点击关闭预览
  • 图片层: 居中显示,支持缩放动画
  • 控制层: 交互按钮,绝对定位不遮挡图片

3.2 遮罩层设计

// 遮罩层配置
if (this.previewOptions.showMask !== false) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0, 0, 0, 0.8)')  // 80% 不透明黑色
    .onClick(() => {
      this.closePreview()  // 点击遮罩关闭预览
    })
}

遮罩层功能:

  • 视觉聚焦: 半透明黑色背景突出图片主体
  • 快捷关闭: 点击遮罩层即可关闭预览
  • 可配置: showMask: false 可禁用遮罩层

3.3 图片层设计

Column() {
  Image(this.getCurrentPreviewImage())
    .width('100%')
    .height('100%')
    .objectFit(ImageFit.Contain)  // 完整显示图片
    .scale({ x: this.previewScale, y: this.previewScale })  // 缩放变换
    .animation({
      duration: 200,              // 动画时长 200ms
      curve: Curve.EaseInOut      // 缓动曲线
    })
}
.width('90%')   // 占屏幕宽度 90%
.height('70%')  // 占屏幕高度 70%

图片层特点:

  • 完整显示: 使用 Contain 模式确保图片不被裁剪
  • 流畅缩放: 200ms 动画提供平滑的缩放体验
  • 合理尺寸: 90% 宽度 × 70% 高度,留出按钮空间

第四章: 缩放功能实现

4.1 缩放算法设计

/**
 * 预览图片缩放
 */
private scalePreviewImage(direction: 'in' | 'out') {
  // 1. 获取缩放范围配置
  const minScale = this.previewOptions.minScale || 0.5
  const maxScale = this.previewOptions.maxScale || 3
  const step = 0.2  // 每次缩放 20%
  
  // 2. 计算新的缩放比例
  if (direction === 'in') {
    // 放大: 当前比例 + 步进,不超过最大值
    this.previewScale = Math.min(this.previewScale + step, maxScale)
  } else {
    // 缩小: 当前比例 - 步进,不低于最小值
    this.previewScale = Math.max(this.previewScale - step, minScale)
  }
}

缩放参数对比:

参数 默认值 推荐范围 说明
initialScale 1 0.5 - 2 初始显示比例
minScale 0.5 0.3 - 0.8 最小缩放(50%)
maxScale 3 2 - 5 最大缩放(300%)
step 0.2 0.1 - 0.5 每次缩放步进

4.2 缩放边界保护

//  使用 Math.min 和 Math.max 确保边界
this.previewScale = Math.min(this.previewScale + step, maxScale)
this.previewScale = Math.max(this.previewScale - step, minScale)

//  不使用边界保护会导致异常
this.previewScale += step  // 可能超过 maxScale
this.previewScale -= step  // 可能低于 minScale

边界保护效果:

场景1: 放大到极限
previewScale = 2.8, maxScale = 3, step = 0.2
→ Math.min(2.8 + 0.2, 3) = Math.min(3.0, 3) = 3.0 ✅

场景2: 缩小到极限
previewScale = 0.6, minScale = 0.5, step = 0.2
→ Math.max(0.6 - 0.2, 0.5) = Math.max(0.4, 0.5) = 0.5 ✅

场景3: 继续点击无效
previewScale = 3.0, maxScale = 3, step = 0.2
→ Math.min(3.0 + 0.2, 3) = Math.min(3.2, 3) = 3.0 (保持不变) ✅

4.3 缩放控制UI

// 缩放按钮组
Row() {
  // 缩小按钮
  Text('−')
    .fontSize(30)
    .fontColor(Color.White)
    .width(40)
    .height(40)
    .textAlign(TextAlign.Center)
    .borderRadius(20)
    .backgroundColor('rgba(0, 0, 0, 0.5)')
    .margin({ right: 10 })
    .onClick(() => {
      this.scalePreviewImage('out')  // 缩小
    })

  // 放大按钮
  Text('+')
    .fontSize(30)
    .fontColor(Color.White)
    .width(40)
    .height(40)
    .textAlign(TextAlign.Center)
    .borderRadius(20)
    .backgroundColor('rgba(0, 0, 0, 0.5)')
    .onClick(() => {
      this.scalePreviewImage('in')  // 放大
    })
}
.position({ x: '50%', y: '90%' })  // 底部居中
.translate({ x: '-50%', y: '-50%' })  // 修正定位偏移

按钮设计要点:

  • 半透明背景: rgba(0, 0, 0, 0.5) 不遮挡图片但清晰可见
  • 圆形按钮: borderRadius: 20 形成完美圆形
  • 底部居中: 使用 position + translate 实现精确居中

第五章: 图片切换功能

5.1 循环索引算法

/**
 * 切换预览图片
 */
private changePreviewImage(direction: 'prev' | 'next') {
  // 边界保护: 无图片列表时不处理
  if (this.previewList.length === 0) return

  // 计算新索引
  if (direction === 'prev') {
    // 上一张: (当前索引 - 1 + 总数) % 总数
    this.currentPreviewIndex = 
      (this.currentPreviewIndex - 1 + this.previewList.length) % this.previewList.length
  } else {
    // 下一张: (当前索引 + 1) % 总数
    this.currentPreviewIndex = 
      (this.currentPreviewIndex + 1) % this.previewList.length
  }

  // 重置缩放比例
  this.previewScale = this.previewOptions.initialScale || 1
}

循环索引原理:

图片列表: [A, B, C, D]  (length = 4)
索引范围: 0, 1, 2, 3

场景1: 当前索引 2,点击"下一张"
→ (2 + 1) % 4 = 3 % 4 = 3  ✅

场景2: 当前索引 3,点击"下一张"(循环到开头)
→ (3 + 1) % 4 = 4 % 4 = 0  ✅

场景3: 当前索引 0,点击"上一张"(循环到末尾)
→ (0 - 1 + 4) % 4 = 3 % 4 = 3  ✅

场景4: 当前索引 1,点击"上一张"
→ (1 - 1 + 4) % 4 = 4 % 4 = 0  ✅

关键技巧:

  • 加总数再取模: 避免负数索引
  • 取模运算: 实现自动循环
  • 边界安全: 任何索引值都能正确循环

5.2 获取当前预览图片

/**
 * 获取当前预览的图片
 */
private getCurrentPreviewImage(): string | Resource {
  // 如果有预览列表,返回列表中的图片
  if (this.previewList.length > 0) {
    return this.previewList[this.currentPreviewIndex]
  }
  // 否则返回当前图片
  return this.imageSrc
}

智能切换逻辑:

  • 列表模式: 从 previewList 中取图片,支持多图切换
  • 单图模式: 无列表时使用 imageSrc,仍可缩放
  • 动态切换: 索引改变时自动更新显示的图片

5.3 切换控制UI

// 仅在有多张图片时显示切换按钮
if (this.previewList.length > 1) {
  Row() {
    // 上一张按钮
    Text('<')
      .fontSize(30)
      .fontColor(Color.White)
      .width(40)
      .height(40)
      .textAlign(TextAlign.Center)
      .borderRadius(20)
      .backgroundColor('rgba(0, 0, 0, 0.5)')
      .position({ x: 20, y: '50%' })  // 左侧居中
      .translate({ x: 0, y: '-50%' })
      .onClick(() => {
        this.changePreviewImage('prev')
      })

    // 下一张按钮
    Text('>')
      .fontSize(30)
      .fontColor(Color.White)
      .width(40)
      .height(40)
      .textAlign(TextAlign.Center)
      .borderRadius(20)
      .backgroundColor('rgba(0, 0, 0, 0.5)')
      .position({ x: '95%', y: '50%' })  // 右侧居中
      .translate({ x: '-100%', y: '-50%' })
      .onClick(() => {
        this.changePreviewImage('next')
      })
  }
  .width('100%')
  .height('100%')
}

按钮定位策略:

  • 左侧按钮: position({ x: 20, y: '50%' }) 距左 20,垂直居中
  • 右侧按钮: position({ x: '95%', y: '50%' }) 距右 5%,垂直居中
  • 条件渲染: 只有 2 张及以上图片才显示切换按钮

提示
更多实战操作请查看下一章节

Logo

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

更多推荐