日程卡片宽度计算:HarmonyOS跨天可见天数与百分比转换全解析

【免费下载链接】IssueSolutionDemos 用于管理和运行HarmonyOS Issue解决方案Demo集锦。 【免费下载链接】IssueSolutionDemos 项目地址: https://gitcode.com/HarmonyOS_DTSE/IssueSolutionDemos

引言:跨天日程布局的技术痛点

在HarmonyOS应用开发中,日程卡片的宽度计算是实现流畅用户体验的关键环节。开发者常面临以下挑战:如何准确计算跨天日程在不同日期的可见天数?如何将可见天数转换为合适的宽度百分比以实现响应式布局?本文将深入剖析这两个核心问题,通过代码实例和流程图解,提供一套完整的解决方案。

读完本文,你将掌握:

  • 跨天日程可见天数的精确计算方法
  • 可见天数到宽度百分比的转换逻辑
  • HarmonyOS中实现响应式日程布局的最佳实践
  • 处理边界情况的实用技巧

一、跨天可见天数计算原理

1.1 核心概念:可见天数定义

跨天日程(Multi-day Schedule)指持续时间超过24小时的日程安排。在日历视图中,这类日程需要在不同日期显示不同的宽度比例。可见天数(Visible Days)是指从指定日期开始,该日程还将持续显示的天数。

// 日程模型核心定义
export class ScheduleItem {
  id: string
  title: string
  startDate: Date  // 开始日期
  endDate: Date    // 结束日期
  // 其他属性...
  
  // 判断是否是跨天日程
  isMultiDay(): boolean {
    return this.duration > 1
  }
  
  // 计算日程在某个日期的可见天数
  getVisibleDaysFrom(date: Date): number {
    // 实现代码将在下文详细解析
  }
}

1.2 算法实现:时间戳比较法

// 计算日程在某个日期的可见天数
getVisibleDaysFrom(date: Date): number {
  if (!this.isInDate(date)) {
    return 0; // 如果不在该日期范围内,可见天数为0
  }

  // 标准化日期:只比较年月日,忽略时分秒
  const normalizeDate = (d: Date): number => {
    return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  };

  // 计算从当前日期到日程结束还有几天
  const diff = normalizeDate(this.getEndDate()) - normalizeDate(date);
  return Math.floor(diff / DAY_TIME) + 1; // DAY_TIME = 24*60*60*1000
}

上述代码的核心思路是:

  1. 标准化日期:将所有日期转换为只包含年月日的时间戳
  2. 计算指定日期与日程结束日期之间的差值
  3. 将差值转换为天数并加1(包含当天)

1.3 流程图解:可见天数计算流程

mermaid

1.4 边界情况处理

场景 示例 计算结果
单天日程 2023-10-01 09:00 - 2023-10-01 17:00 1天
跨天日程-第一天 2023-10-01 - 2023-10-03,指定日期=2023-10-01 3天
跨天日程-中间天 2023-10-01 - 2023-10-03,指定日期=2023-10-02 2天
跨天日程-最后一天 2023-10-01 - 2023-10-03,指定日期=2023-10-03 1天
不在日期范围内 2023-10-01 - 2023-10-03,指定日期=2023-09-30 0天

二、可见天数到宽度百分比的转换

2.1 转换公式与实现

在获取可见天数后,需要将其转换为宽度百分比以实现响应式布局。核心公式为:

宽度百分比 = 可见天数 × 100%

实现代码如下:

// 计算跨天日程在特定日期的宽度占比
static getScheduleWidthPercent(schedule: ScheduleItem, date: Date): number {
  if (schedule.duration === 1) {
    return 100; // 单天日程占满整列
  } else {
    // 获取这个日期开始后的可见天数
    const visibleDays = schedule.getVisibleDaysFrom(date);
    // 返回百分比宽度
    return 100 * visibleDays;
  }
}

2.2 转换逻辑解析

上述代码实现了以下逻辑:

  1. 对于单天日程(duration=1),直接返回100%宽度
  2. 对于跨天日程,获取从指定日期开始的可见天数
  3. 将可见天数乘以100,得到宽度百分比

2.3 可视化示例

mermaid

注:饼图数值表示宽度百分比,实际显示时会根据容器宽度进行调整

三、完整实现与应用

3.1 日程模型类完整代码

import { DAY_TIME } from "../utils/TimeUtils"

export class ScheduleItem {
  id: string
  title: string
  year: number
  startMonth: number
  startDay: number
  duration: number
  startTime: string
  endTime: string
  color: string
  private _startDate: Date | null = null // 缓存开始日期
  private _endDate: Date | null = null // 缓存结束日期

  constructor(id: string, title: string, year: number, startMonth: number, startDay: number, duration: number,
    startTime: string, endTime: string, color: string) {
    this.id = id
    this.title = title
    this.year = year
    this.startMonth = startMonth
    this.startDay = startDay
    this.duration = duration
    this.startTime = startTime
    this.endTime = endTime
    this.color = color
  }

  // 获取开始日期(带缓存)
  getStartDate(): Date {
    if (!this._startDate) {
      this._startDate = new Date(this.year, this.startMonth - 1, this.startDay)
    }
    return this._startDate
  }

  // 获取结束日期(带缓存)
  getEndDate(): Date {
    if (!this._endDate) {
      const startDate = this.getStartDate()
      this._endDate = new Date(startDate)
      this._endDate.setDate(startDate.getDate() + this.duration - 1)
    }
    return this._endDate
  }

  // 判断日程是否在指定日期
  isInDate(date: Date): boolean {
    // 使用时间戳比较,只比较年月日
    const normalizeDate = (d: Date): number => {
      return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
    }

    const targetTime = normalizeDate(date)
    const startTime = normalizeDate(this.getStartDate())
    const endTime = normalizeDate(this.getEndDate())

    return targetTime >= startTime && targetTime <= endTime
  }

  // 计算日程在某个日期的可见天数
  getVisibleDaysFrom(date: Date): number {
    if (!this.isInDate(date)) {
      return 0
    }

    // 计算从当前日期到日程结束还有几天
    const normalizeDate = (d: Date): number => {
      return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
    }

    const diff = normalizeDate(this.getEndDate()) - normalizeDate(date)
    return Math.floor(diff / DAY_TIME) + 1
  }
}

3.2 布局计算工具类

import { ScheduleItem } from '../model/ScheduleModel'
import { timeToHours } from './TimeUtils'

// 日程位置和尺寸计算
export class LayoutCalculator {
  // 计算日程在时间轴上的垂直位置
  static getScheduleTopPosition(schedule: ScheduleItem, firstHour: number): number {
    const startTimeParts = schedule.startTime.split(':')
    const hours = Number(startTimeParts[0])
    const minutes = Number(startTimeParts[1])
    const totalHours = hours + minutes / 60

    // 计算顶部位置,每小时60px
    return (totalHours - firstHour) * 60
  }

  // 计算日程的高度
  static getScheduleHeight(schedule: ScheduleItem): number {
    const startHour = timeToHours(schedule.startTime)
    const endHour = timeToHours(schedule.endTime)
    const durationHours = endHour - startHour

    // 计算高度,每小时60px
    return Math.max(durationHours * 60, 30) // 最小高度30px
  }

  // 计算跨天日程在特定日期的宽度占比
  static getScheduleWidthPercent(schedule: ScheduleItem, date: Date): number {
    if (schedule.duration === 1) {
      return 100 // 单天日程占满整列
    } else {
      // 获取这个日期开始后的可见天数
      const visibleDays = schedule.getVisibleDaysFrom(date)
      // 返回百分比宽度
      return 100 * visibleDays
    }
  }

  // 检测日程是否有重叠
  static hasOverlap(schedule1: ScheduleItem, schedule2: ScheduleItem, date: Date): boolean {
    // 如果不在同一天,则不重叠
    if (!schedule1.isInDate(date) || !schedule2.isInDate(date)) {
      return false
    }

    // 获取开始和结束时间
    const start1 = timeToHours(schedule1.startTime)
    const end1 = timeToHours(schedule1.endTime)
    const start2 = timeToHours(schedule2.startTime)
    const end2 = timeToHours(schedule2.endTime)

    // 检查时间段是否重叠
    return (start1 < end2) && (start2 < end1)
  }
}

3.3 组件中应用示例

// 在自定义组件中应用宽度计算
@Builder
ScheduleCard(schedule: ScheduleItem, date: Date) {
  Column() {
    Text(schedule.title)
      .fontSize(14)
      .margin(5)
  }
  .width(`${LayoutCalculator.getScheduleWidthPercent(schedule, date)}%`)
  .height(`${LayoutCalculator.getScheduleHeight(schedule)}px`)
  .backgroundColor(schedule.color)
  .borderRadius(8)
  .margin({ left: 2, right: 2, top: 2 })
}

四、边界情况与性能优化

4.1 常见边界情况处理

  1. 日期标准化:确保不同时区和时间戳的日期能够正确比较
// 标准化日期:只比较年月日,忽略时分秒和时区
const normalizeDate = (d: Date): number => {
  return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
}
  1. 最小高度限制:避免短时间日程变得难以点击
// 计算高度,每小时60px,最小高度30px
return Math.max(durationHours * 60, 30)
  1. 重叠检测:处理同一时间段内多个日程的显示问题
// 检测日程是否有重叠
static hasOverlap(schedule1: ScheduleItem, schedule2: ScheduleItem, date: Date): boolean {
  // 实现代码见3.2节
}

4.2 性能优化策略

  1. 日期缓存:避免重复创建日期对象
// 使用缓存减少日期对象创建开销
private _startDate: Date | null = null // 缓存开始日期
private _endDate: Date | null = null   // 缓存结束日期

// 获取开始日期(带缓存)
getStartDate(): Date {
  if (!this._startDate) {
    this._startDate = new Date(this.year, this.startMonth - 1, this.startDay)
  }
  return this._startDate
}
  1. 批量计算:对同一日期的多个日程进行批量处理
// 批量计算某一日期所有日程的宽度
calculateAllWidths(schedules: ScheduleItem[], date: Date): Map<string, number> {
  const result = new Map<string, number>()
  schedules.forEach(schedule => {
    if (schedule.isInDate(date)) {
      result.set(schedule.id, LayoutCalculator.getScheduleWidthPercent(schedule, date))
    }
  })
  return result
}

五、总结与展望

5.1 核心知识点回顾

本文详细介绍了HarmonyOS应用中计算跨天日程卡片宽度的完整解决方案,包括:

  • 可见天数计算:通过标准化日期和时间戳比较法实现
  • 百分比转换:基于可见天数计算宽度占比的核心公式
  • 完整实现:日程模型类与布局计算工具类代码
  • 边界处理:日期比较、最小高度、重叠检测等关键问题
  • 性能优化:日期缓存和批量计算等实用技巧

5.2 未来优化方向

  1. 动态列宽支持:适应不同屏幕尺寸和方向的列宽计算
  2. 动画过渡效果:实现日程卡片宽度变化时的平滑过渡
  3. 自定义宽度规则:允许用户定义不同的宽度计算策略
  4. 性能进一步优化:使用Worker线程处理大量日程计算

六、参考资料

  1. HarmonyOS开发者文档 - 布局开发指南
  2. 《ArkUI-X应用开发实战》- 响应式布局章节
  3. HarmonyOS开源项目IssueSolutionDemos源码分析

【免费下载链接】IssueSolutionDemos 用于管理和运行HarmonyOS Issue解决方案Demo集锦。 【免费下载链接】IssueSolutionDemos 项目地址: https://gitcode.com/HarmonyOS_DTSE/IssueSolutionDemos

Logo

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

更多推荐