Categories
Tags
Ai 生成 API学习 API简化 api请求 API调用 best-practices Blogging Caching catchTag catchTags class CLI Config context Context Context.Tag CSS Customization Demo development DocC Docker dual API Effect effect Effect.Service Effect.succeed Example extension ffmpeg filterOrFail flatMap Fuwari gen generator grep hooks HTML HTTP响应 IDE自动补全 iOS javascript JavaScript Javascript Layer.effect Layer.provide Layers Linux Markdown Mock n8n Next.js ParseError pipe pokemon PostCSS process.env progress Promise promise provideService PWA react React React Hook Form React Query React Router react-native Scheduler Schema Schema.Class security Service Worker Services SSR state-management suspense Tagged Errors TaggedError TanStack Query TanStack Start tips tryPromise tsconfig TypeScript typescript Video VS Code vscode Web API Web Development yield yt-dlp Zod 不透明类型 二叉树 代码组织 任务调度 优先级 使用服务 依赖注入 依赖管理 值语义 入门教程 最佳实践 最小堆 函数式编程 函数组合 前端 前端开发 副作用 副作用控制 可视化 可组合性 可维护性 可访问性 命令行 响应过滤 多个错误 实现 实践指南 层 层依赖 层组合 工具链 并发控制 应用架构 延迟执行 开发技巧 开发教程 开源 异步处理 异步操作 异步编程 性能优化 手写系列 排序 接口设计 插件开发 数据结构 数据获取 数据解码 数据验证 无限滚动 日历 日志分析 服务 服务依赖 服务定义 服务实现 服务提供 测试 源码分析 状态管理 环境变量 生成器 离线支持 程序分离 算法 类型安全 类型定义 类型推断 类型系统 类定义 线性代码 组合 翻译 自动化 自定义错误 表单验证 记忆化 设计模式 语义化 运维 运行时验证 部分应用 配置 配置变量 配置服务 配置管理 重构 错误处理 错误定义 错误恢复 项目设置
1557 words
8 minutes
React Native 实现横向无限滚动日历详解
React Native 横向无限滚动日历详解
横向无限滚动是现代移动应用中常见的交互模式,特别是在日历、图片浏览等场景中。本文将深入分析React Native Calendars库中周日历(WeekCalendar)组件的无限滚动实现,剖析其核心原理和数据流转逻辑。
核心实现原理
WeekCalendar组件实现了一个横向无限滚动的日历视图,主要实现逻辑如下:
1. 数据结构设计
export const NUMBER_OF_PAGES = 6;
const NUM_OF_ITEMS = NUMBER_OF_PAGES * 2 + 1; // NUMBER_OF_PAGES before + NUMBER_OF_PAGES after + current- 使用一个长度为13(NUMBER_OF_PAGES*2+1)的数组存储日期数据
- 中间位置(索引6)为当前周,左右各6个周
- 这种设计允许用户在有限DOM元素范围内体验”无限”滚动
2. 滚动机制实现
<FlatList
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
initialScrollIndex={NUMBER_OF_PAGES}
data={listData}
renderItem={renderItem}
getItemLayout={getItemLayout}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
onEndReached={onEndReached}
onEndReachedThreshold={1 / NUM_OF_ITEMS}
/>- 使用
FlatList组件实现水平滚动 pagingEnabled确保翻页效果initialScrollIndex={NUMBER_OF_PAGES}初始显示中间位置getItemLayout预计算每项位置,优化性能viewabilityConfigCallbackPairs监听可见项变化onEndReached处理到达边缘的情况
3. 日期数组生成逻辑
function getDatesArray(date: string, firstDay: number, numberOfDays?: number) {
return [...Array(NUM_OF_ITEMS).keys()].map((index) => {
if (isCustomNumberOfDays(numberOfDays)) {
return getDateForDayRange(date, index - NUMBER_OF_PAGES, numberOfDays as number);
}
return getDate(date, firstDay, index - NUMBER_OF_PAGES);
});
}- 通过
index - NUMBER_OF_PAGES计算周的偏移量 - 对于中间位置,偏移量为0,表示当前周
- 对于小于中间的索引,偏移量为负,表示过去的周
- 对于大于中间的索引,偏移量为正,表示未来的周
数据流转逻辑详解
从滑到顶点(最早日期)的数据流转
当用户持续右滑查看过去的日期时,数据流转过程如下:
1. 滑动检测与边缘判断
const onViewableItemsChanged = useCallback(({viewableItems}) => {
// 省略前面的检查...
if (shouldFixRTL) {
// RTL环境特殊处理...
} else {
currentIndex.current = currItems.indexOf(newDate);
visibleWeek.current = newDate;
setDate(newDate, UpdateSources.WEEK_SCROLL);
// 检查是否到达左侧边缘(最早日期)
if (visibleWeek.current === currItems[0]) {
onEndReached(); // 触发数据重置
}
}
}, [onEndReached, shouldFixRTL]);当检测到可见项变为数组第一项时,触发边缘重置流程。
2. 顶点重置过程
const onEndReached = useCallback(() => {
changedItems.current = true; // 标记数据正在变更
items.current = getDatesArray(visibleWeek.current, firstDay, numberOfDays);
setListData(items.current); // 更新FlatList数据
currentIndex.current = NUMBER_OF_PAGES; // 重置索引到中间
list?.current?.scrollToIndex({index: NUMBER_OF_PAGES, animated: false}); // 无动画跳转
}, [firstDay, numberOfDays]);重置过程包括:
- 标记变更状态,防止循环触发
- 以当前可见周为中心点重新生成日期数组
- 将当前索引重置到中间位置
- 无动画跳转回中间,用户不会察觉这个跳转
3. 上下文数据更新
setDate(newDate, UpdateSources.WEEK_SCROLL);每次可见周变化时,通过setDate更新上下文,触发整个日历系统的状态更新。
4. 完整顶点数据流程
- 用户右滑 → 可见项变为数组第一项
- 触发边缘检测 → 调用
onEndReached - 重新生成以当前周为中心的日期数组
- 无动画跳转到中间位置
- 用户可继续右滑查看更早日期
- 整个过程对用户无感知,体验连续滚动
从滑到终点(最新日期)的数据流转
当用户持续左滑查看未来日期时,数据流转过程如下:
1. 终点触发机制
在标准实现中,终点检测主要通过两种方式:
// 方式1:通过FlatList的onEndReached属性
<FlatList
onEndReached={onEndReached}
onEndReachedThreshold={1 / NUM_OF_ITEMS}
/>
// 方式2:在RTL环境中,通过检测最后一项
if (visibleWeek.current === currItems[currItems.length - 1]) {
onEndReached();
}当用户接近列表末尾约1/13的位置,就会触发重置流程,确保在实际到达最后一项前就开始准备新数据。
2. 终点数据重置特点
当从终点(最新日期)重置时:
- 当前可见周是最新日期
- 生成的新数组以这个最新日期为中心
- 中间位置保持为当前最新周
- 之前的位置变成过去的周
- 之后的位置生成更未来的周日期
3. 日期生成的特殊处理
function getDate(date: string, firstDay: number, weekIndex: number, numberOfDays?: number) {
// ...前面的计算逻辑
const today = new XDate();
const offsetFromNow = newDate.diffDays(today);
const isSameWeek = offsetFromNow > 0 && offsetFromNow < (numberOfDays ?? 7);
return toMarkingFormat(isSameWeek ? today : newDate);
}对于当前周有特殊处理,确保正确标记和显示今天的日期。
4. 完整终点数据流程
- 用户左滑查看未来日期 → 接近/到达列表右侧
- 触发
onEndReached或通过可见性检测到达终点 - 以当前最新周为中心生成新的日期数组
- 无动画跳转回中间位置
- 用户可继续左滑查看更多未来日期
- 整个过程对用户无感知
RTL支持与特殊处理
对于RTL(从右到左)语言环境,组件有特殊处理机制:
const shouldFixRTL = useMemo(() => !constants.isRN73() && constants.isAndroidRTL, []);
// RTL环境下的特殊处理
if (shouldFixRTL) {
const newDateOffset = -1 * (NUMBER_OF_PAGES - currItems.indexOf(newDate));
const adjustedNewDate = currItems[NUMBER_OF_PAGES - newDateOffset];
// ...后续逻辑
}在RTL环境中:
- 滚动方向与实际数据索引方向相反
- 需要特殊计算可见项的实际位置
- 边缘检测逻辑也会相应调整
性能优化技巧
该实现包含几个关键的性能优化点:
- 预计算布局:通过
getItemLayout避免动态测量
const getItemLayout = useCallback((_, index: number) => {
return {
length: containerWidth,
offset: containerWidth * index,
index
};
}, [containerWidth]);- 状态变更标记:使用
changedItems.current防止循环触发 - 无动画跳转:在数据重置时使用
animated: false避免用户察觉 - 有限DOM元素:只渲染13个周视图,而不是无限多的DOM元素
总结
React Native中横向无限滚动的实现本质上是一种”障眼法”:
- 使用有限数量的视图元素
- 在用户滚动到边缘时重置数据和位置
- 通过无缝过渡创造无限滚动的错觉
这种实现既保证了良好的性能,又提供了顺畅的用户体验,是一种优雅的无限滚动解决方案。这一模式不仅适用于日历,也可应用于图片浏览、轮播图等多种场景。
React Native 实现横向无限滚动日历详解
https://0bipinnata0.my/posts/react-native/横向无限滚动/