欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。


在鸿蒙(HarmonyOS)全场景分布式应用生态下,索引栏(IndexBar)组件作为长列表快速导航的核心UI元素,其跨端开发的核心挑战在于实现流畅的交互动效、保证多终端导航体验一致性,同时兼顾不同设备的触控/非触控操作特性。本文将从组件架构、交互动效、布局适配、跨端优化等维度,深度解读这套 React Native 索引栏组件的技术实现,展现如何构建一套适配手机、平板、智慧屏等鸿蒙全场景设备的高性能索引导航组件。

分层解耦

这套索引栏组件采用“锚点组件 + 索引栏组件 + 业务容器”的分层架构,是 React Native 适配鸿蒙跨端开发的典型实践。组件将索引锚点(IndexAnchor)与索引导航栏(IndexBar)解耦设计,既保证了核心导航逻辑的复用性,又便于针对不同鸿蒙终端进行交互细节的定制化调整。

// 锚点组件:负责标记列表位置
interface IndexAnchorProps {
  index: string;
  children: React.ReactNode;
}

// 索引栏组件:负责提供导航交互
interface IndexBarProps {
  indices: string[];
  onIndexSelect?: (index: string) => void;
  activeIndex?: string;
  showIndicator?: boolean;
}

架构设计体现了清晰的职责划分:

  • IndexAnchor:纯展示型组件,仅负责在长列表中标记对应索引的位置,无交互逻辑,保证了在不同终端的展示一致性;
  • IndexBar:交互型组件,封装索引导航的核心逻辑,包括点击交互、视觉反馈、动效展示,可根据鸿蒙不同终端的交互特性调整动效参数;
  • 业务容器:通过 ScrollView 承载长列表内容,对接索引栏的导航回调实现滚动跳转,完成业务层与组件层的解耦。

这种分层设计使得组件核心逻辑与业务逻辑完全分离,既便于在鸿蒙不同应用场景(通讯录、城市选择、商品分类)中复用,又支持针对智慧屏、平板、手机等不同终端的个性化适配。

交互

索引栏组件的核心价值在于提供直观、流畅的导航交互体验,其动效实现基于 React Native 原生 Animated 库,保证了在鸿蒙系统中的高性能渲染与跨端一致性。

视觉指示器动效

const fadeAnim = useRef(new Animated.Value(0)).current;

const handleIndexPress = (index: string) => {
  setCurrentIndicator(index);
  setIndicatorVisible(true);
  onIndexSelect && onIndexSelect(index);
  
  Animated.sequence([
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 150,
      useNativeDriver: true
    }),
    Animated.delay(1000),
    Animated.timing(fadeAnim, {
      toValue: 0,
      duration: 150,
      useNativeDriver: true
    })
  ]).start(() => {
    setIndicatorVisible(false);
  });
};

动效实现采用 React Native 原生动画体系,是适配鸿蒙跨端的关键技术选型:

  1. 原生驱动动画useNativeDriver: true 配置将动画计算移至原生线程执行,避免了 JS 线程与原生线程的通信开销,在鸿蒙低性能设备(如入门级智慧屏)上仍能保证动画流畅;
  2. 序列动画设计:通过 Animated.sequence 组合渐入、延迟、渐出三个阶段,实现指示器的完整展示周期,动效时长(150ms)符合鸿蒙系统的交互反馈规范,兼顾手机端的触控反馈与智慧屏端的遥控器操作反馈;
  3. 状态联动控制:动画结束后通过回调重置指示器可见状态,保证动画状态与组件状态的一致性,避免鸿蒙系统中因动画卡顿导致的状态不同步问题。

指示器样式

<Animated.View 
  style={[
    styles.indicator,
    {
      opacity: fadeAnim,
      transform: [{ scale: fadeAnim.interpolate({
        inputRange: [0, 1],
        outputRange: [0.8, 1]
      })}]
    }
  ]}
>
  <Text style={styles.indicatorText}>{currentIndicator}</Text>
</Animated.View>

指示器同时实现了透明度与缩放的组合动效,是提升跨端交互体验的核心设计:

  • 透明度渐变:保证指示器的平滑出现与消失,适配鸿蒙系统的动效设计规范;
  • 缩放插值动画:通过 interpolate 实现从 0.8 到 1 的缩放过渡,增加动效的层次感,在智慧屏等大屏设备上视觉反馈更明显;
  • 纯代码动效实现:无需依赖图片或 Lottie 等第三方动效库,降低了鸿蒙跨端开发的资源依赖与集成成本。

索引栏布局:

<View style={styles.indexBarContainer}>
  {indices.map((index) => (
    <TouchableOpacity
      key={index}
      style={[
        styles.indexBarItem,
        activeIndex === index && styles.activeIndexItem
      ]}
      onPress={() => handleIndexPress(index)}
    >
      <Text style={[
        styles.indexBarText,
        activeIndex === index && styles.activeIndexText
      ]}>
        {index}
      </Text>
    </TouchableOpacity>
  ))}
</View>

索引栏的布局设计充分考虑了鸿蒙多终端的操作特性:

  1. 触控区域适配:索引项(IndexBarItem)的尺寸设计兼顾手机端的指尖触控与智慧屏端的遥控器焦点操作——手机端保证最小触控区域(48dp),智慧屏端可通过样式扩展增大焦点区域;
  2. 状态可视化:通过 activeIndex 控制激活态样式,在不同终端均能清晰展示当前选中的索引,适配智慧屏远距离观看的视觉需求;
  3. Flex 布局基础:索引栏容器采用垂直 Flex 布局,自动均分索引项的布局空间,在鸿蒙不同屏幕尺寸的设备上均能保持均匀的索引分布。

长列表导航:

const scrollToIndex = (index: string) => {
  setActiveIndex(index);
  // 这里应该滚动到对应锚点位置
  console.log(`滚动到索引: ${index}`);
};

<ScrollView 
  ref={scrollViewRef}
  style={styles.scrollView}
  showsVerticalScrollIndicator={false}
>
  {alphabet.map(letter => (
    <IndexAnchor key={letter} index={letter}>
      {/* 分组内容 */}
    </IndexAnchor>
  ))}
</ScrollView>

长列表与索引栏的联动设计体现了跨端导航的核心思路:

  • 引用控制:通过 scrollViewRef 控制滚动视图,在鸿蒙系统中可直接调用原生滚动方法,相比自定义滚动逻辑具有更高的性能;
  • 锚点标记IndexAnchor 组件为每个索引标记位置,便于计算滚动偏移量,适配鸿蒙不同终端的屏幕尺寸差异;
  • 无侵入设计:索引导航逻辑不侵入列表渲染逻辑,保证了长列表在鸿蒙原子化服务、全屏应用等不同形态下的兼容性。

数据分组:

const groupedContacts = alphabet.reduce((acc: any, letter) => {
  acc[letter] = contacts.filter(contact => contact.initial === letter);
  return acc;
}, {});

数据分组逻辑采用 reduce 方法实现,是 React Native 适配鸿蒙跨端的性能优化实践:

  1. 预计算分组:在组件初始化时完成数据分组,避免渲染阶段的重复计算,降低鸿蒙低性能设备的 JS 线程负载;
  2. 空值处理:对无数据的索引组展示“暂无联系人”提示,避免空布局导致的UI异常,适配鸿蒙系统的界面完整性要求;
  3. 惰性渲染:仅渲染有数据的联系人项,减少不必要的组件实例化,提升鸿蒙设备的列表渲染效率。

条件渲染:

{groupedContacts[letter].length > 0 ? (
  <View style={styles.contactGroup}>
    <Text style={styles.groupTitle}>{letter}</Text>
    {groupedContacts[letter].map(renderContactItem)}
  </View>
) : (
  <View style={styles.emptyGroup}>
    <Text style={styles.emptyText}>暂无联系人</Text>
  </View>
)}

条件渲染逻辑保证了在鸿蒙不同终端的资源高效利用:

  • 避免空列表项的渲染,减少 DOM 节点数量,降低鸿蒙智慧屏等设备的内存占用;
  • 统一的空状态展示,保证跨终端的视觉一致性,符合鸿蒙系统的UX设计规范;
  • 分组标题与内容的结构化渲染,便于鸿蒙系统的无障碍服务解析列表结构,提升应用的无障碍适配能力。


这套索引栏组件在 React Native 适配鸿蒙的过程中,贯穿了以下核心优化思路:

动效

所有动效均基于 React Native 原生 Animated 库实现,并启用 useNativeDriver: true,将动画计算移至原生线程,避免 JS 线程阻塞导致的动效卡顿,尤其适配鸿蒙智慧屏等对动效流畅性要求较高的设备。

交互

  • 索引项的触控区域尺寸设计兼顾手机端指尖操作与智慧屏遥控器焦点操作;
  • 视觉指示器的动效时长与反馈强度适配不同终端的交互习惯,智慧屏端可通过参数调整增强视觉反馈。

响应式

所有布局尺寸均采用 DP 单位,索引栏的宽度、高度、间距等样式均基于相对尺寸设计,在鸿蒙手机(窄屏)、平板(宽屏)、智慧屏(超大屏)上均能保持合理的视觉比例与操作体验。

状态一致性

通过 activeIndex 控制索引栏的激活状态,保证在鸿蒙分布式场景下,多设备间的索引导航状态可通过属性同步,实现一致的导航体验。

总结

这套 React Native 索引栏组件不仅实现了字母导航、锚点跳转、视觉反馈等核心功能,更构建了一套完整的鸿蒙跨端适配体系。核心要点可总结为:

  • 架构层面:锚点与索引栏分层解耦,核心逻辑与业务逻辑分离,适配鸿蒙多终端的复用与定制需求;
  • 动效层面:原生驱动动画 + 序列动效设计,保证跨终端的流畅交互体验;
  • 布局层面:响应式尺寸设计 + 条件渲染,适配鸿蒙不同屏幕尺寸的设备特性;
  • 性能层面:预计算分组 + 惰性渲染,提升鸿蒙设备的列表渲染与交互性能。

在实际的鸿蒙跨端项目中,可基于该组件进一步扩展,比如支持鸿蒙系统的深色模式适配、对接分布式数据管理实现多设备索引状态同步、适配智慧屏的遥控器焦点导航逻辑,充分发挥 React Native “一次开发,多端部署”的技术优势,构建真正适配鸿蒙全场景生态的索引导航组件。


本文将深入分析一个功能完整的 React Native 索引栏组件实现,该组件采用了现代化的函数式组件架构,支持字母索引、点击跳转、指示器动画等多种功能,同时兼顾了 React Native 与 HarmonyOS 的跨端兼容性。

接口设计

组件首先通过 TypeScript 接口定义了核心数据结构和配置选项:

interface IndexAnchorProps {
  index: string;
  children: React.ReactNode;
}

interface IndexBarProps {
  indices: string[];
  onIndexSelect?: (index: string) => void;
  activeIndex?: string;
  showIndicator?: boolean;
}

这种类型定义方式体现了良好的 TypeScript 实践,通过可选属性和明确的类型约束,确保了组件使用时的类型安全。

索引锚点组件

IndexAnchor 组件是一个简单的容器组件,用于显示索引锚点和对应的内容:

const IndexAnchor: React.FC<IndexAnchorProps> = ({ index, children }) => {
  return (
    <View style={styles.anchorContainer}>
      <Text style={styles.anchorIndex}>{index}</Text>
      {children}
    </View>
  );
};

这种设计的优点:

  1. 简洁明了:组件结构简单,职责清晰
  2. 灵活性:通过 children 属性支持任意内容的嵌套
  3. 可复用性:可以在不同场景中使用,如联系人列表、城市选择等

索引栏组件

IndexBar 组件是整个实现的核心,负责显示侧边索引栏和点击效果:

const IndexBar: React.FC<IndexBarProps> = ({
  indices,
  onIndexSelect,
  activeIndex,
  showIndicator = true
}) => {
  const [indicatorVisible, setIndicatorVisible] = useState(false);
  const [currentIndicator, setCurrentIndicator] = useState('');
  const fadeAnim = useRef(new Animated.Value(0)).current;

  const handleIndexPress = (index: string) => {
    setCurrentIndicator(index);
    setIndicatorVisible(true);
    onIndexSelect && onIndexSelect(index);
    
    Animated.sequence([
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 150,
        useNativeDriver: true
      }),
      Animated.delay(1000),
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 150,
        useNativeDriver: true
      })
    ]).start(() => {
      setIndicatorVisible(false);
    });
  };

  // 渲染逻辑
};

索引栏组件的技术要点:

  1. 状态管理:使用 useState 管理指示器的可见性和当前显示的索引
  2. 动画效果:使用 Animated API 实现指示器的淡入淡出和缩放动画
  3. 事件处理:实现了索引点击事件,触发回调函数并显示指示器
  4. 性能优化:使用 useNativeDriver: true 让动画在原生线程执行,提高性能
  5. 用户体验:通过动画效果增强用户交互体验

主应用

IndexBarComponentApp 组件展示了索引栏组件的使用示例:

const IndexBarComponentApp = () => {
  const [activeIndex, setActiveIndex] = useState('A');
  const scrollViewRef = useRef<ScrollView>(null);
  
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
  const contacts = [
    { id: '1', name: '阿里巴巴', phone: '138****1234', initial: 'A' },
    // 更多联系人...
  ];

  const scrollToIndex = (index: string) => {
    setActiveIndex(index);
    // 这里应该滚动到对应锚点位置
    console.log(`滚动到索引: ${index}`);
  };

  // 渲染逻辑
};

主应用组件的技术要点:

  1. 状态管理:使用 useState 管理当前激活的索引
  2. 引用管理:使用 useRef 管理 ScrollView 引用,用于滚动操作
  3. 数据处理:将联系人按字母分组,生成 groupedContacts 对象
  4. 渲染逻辑:使用 map 遍历字母表,为每个字母生成一个 IndexAnchor 组件
  5. 事件处理:实现了 scrollToIndex 函数,用于处理索引点击事件

索引栏组件的样式系统设计合理,支持多种视觉效果:

  1. 基础样式:定义组件的基本结构和布局
  2. 索引栏样式:定义侧边索引栏的样式,包括位置、大小、颜色等
  3. 指示器样式:定义点击索引时显示的指示器样式
  4. 激活状态样式:定义索引的激活状态样式
  5. 联系人样式:定义联系人列表的样式,包括头像、姓名、电话等

动画效果

索引栏组件的动画效果是其技术亮点之一:

Animated.sequence([
  Animated.timing(fadeAnim, {
    toValue: 1,
    duration: 150,
    useNativeDriver: true
  }),
  Animated.delay(1000),
  Animated.timing(fadeAnim, {
    toValue: 0,
    duration: 150,
    useNativeDriver: true
  })
]).start(() => {
  setIndicatorVisible(false);
});

动画效果的技术要点:

  1. 序列动画:使用 Animated.sequence 实现动画序列
  2. 淡入淡出:使用 Animated.timing 实现透明度的变化
  3. 延迟效果:使用 Animated.delay 实现指示器的显示延迟
  4. 缩放效果:通过 interpolate 实现指示器的缩放效果
  5. 回调处理:动画结束后调用回调函数,隐藏指示器

响应式布局

索引栏组件的布局设计考虑了不同设备的屏幕尺寸:

  1. 固定定位:索引栏使用固定定位,始终显示在屏幕右侧
  2. 自适应高度:索引栏的高度根据索引数量自动调整
  3. 居中对齐:索引栏在垂直方向上居中对齐
  4. 指示器位置:指示器显示在屏幕中央,确保在不同尺寸的设备上都能良好显示

索引栏组件实现了丰富的功能特性:

  1. 字母索引:支持 A-Z 字母索引,点击字母可以跳转到对应位置
  2. 指示器动画:点击索引时显示带有动画效果的指示器
  3. 激活状态:当前激活的索引会高亮显示
  4. 联系人分组:联系人按字母分组显示,每个分组有对应的索引锚点
  5. 空分组处理:对于没有联系人的字母分组,显示"暂无联系人"提示
  6. 滚动操作:点击索引可以触发滚动操作,跳转到对应位置

应用场景

索引栏组件适用于多种应用场景:

  1. 联系人列表:按姓名首字母索引,快速定位联系人
  2. 城市选择:按城市名称首字母索引,快速定位城市
  3. 商品分类:按分类名称首字母索引,快速定位商品分类
  4. 音乐列表:按歌曲名称首字母索引,快速定位歌曲
  5. 字典应用:按单词首字母索引,快速定位单词

索引栏组件在设计时充分考虑了跨端兼容性,主要体现在以下几个方面:

  1. 组件兼容性:使用的 ViewTextTouchableOpacityScrollView 等组件在 React Native 和 HarmonyOS 中都有对应实现
  2. API 兼容性:使用的 useStateuseRefAnimated 等 API 在两个平台上都可用
  3. 样式兼容性:使用的 StyleSheet API 在两个平台上的使用方式基本一致,flexbox 布局在两个平台上都得到了良好支持
  4. 事件处理:使用的触摸事件在两个平台上都有对应的实现

在 React Native 和 HarmonyOS 跨端开发中,索引栏组件需要注意以下实现细节:

  1. 动画性能:不同平台的动画性能可能存在差异,需要进行性能测试和优化
  2. 滚动实现:不同平台的滚动 API 可能存在差异,需要确保滚动操作的一致性
  3. 触摸反馈:不同平台的触摸反馈机制可能存在差异,需要确保交互体验一致
  4. 样式适配:不同平台的默认样式可能存在差异,需要确保视觉效果的一致性

  1. 模块化设计:将组件拆分为 IndexAnchorIndexBar 两个独立的组件,提高了代码的可维护性和可复用性
  2. 组合式设计:通过组合 IndexAnchorIndexBar 组件,实现了完整的索引功能
  3. 可配置性IndexBar 组件提供了丰富的配置选项,如 indicesonIndexSelectactiveIndexshowIndicator
  4. 类型安全:充分利用 TypeScript 的类型系统,确保组件使用时的类型安全

状态管理

  1. 精细化状态:使用多个状态变量管理不同的状态,如 activeIndexindicatorVisiblecurrentIndicator
  2. 引用管理:使用 useRef 管理 ScrollView 引用和动画值,避免了不必要的状态更新
  3. 副作用管理:使用 Animated API 管理动画效果,确保动画的正确执行和清理

  1. 使用 useMemo 缓存计算结果:对于 groupedContacts 等计算结果,可以使用 useMemo 缓存,避免在每次渲染时重复计算
  2. 使用 React.memo 优化组件渲染:对于 IndexAnchorIndexBar 组件,可以使用 React.memo 避免不必要的重渲染
  3. 滚动优化:实现真正的滚动到对应位置的功能,而不是只打印日志
  4. 批量渲染:对于大量联系人的场景,可以考虑使用虚拟滚动,提高渲染性能

功能扩展

  1. 自定义索引:支持自定义索引,不仅仅限于字母
  2. 索引排序:支持自定义索引的排序方式
  3. 索引过滤:支持过滤掉没有对应内容的索引
  4. 样式定制:支持更多的样式定制选项,如颜色、大小、位置等
  5. 动画定制:支持自定义动画效果和时长

滚动实现

索引栏组件的滚动实现是其核心功能之一:

const scrollToIndex = (index: string) => {
  setActiveIndex(index);
  // 这里应该滚动到对应锚点位置
  console.log(`滚动到索引: ${index}`);
};

虽然当前实现只是打印日志,但在实际应用中,应该实现真正的滚动功能:

const scrollToIndex = (index: string) => {
  setActiveIndex(index);
  // 找到对应索引的元素
  const element = document.getElementById(`anchor-${index}`);
  if (element) {
    // 滚动到对应元素
    element.scrollIntoView({ behavior: 'smooth' });
  }
};

在 React Native 中,可以使用 ScrollViewscrollTo 方法实现滚动:

const scrollToIndex = (index: string) => {
  setActiveIndex(index);
  // 计算滚动位置
  const scrollPosition = calculateScrollPosition(index);
  // 滚动到对应位置
  scrollViewRef.current?.scrollTo({
    y: scrollPosition,
    animated: true
  });
};

实际应用示例

联系人列表示例

索引栏组件最常见的应用场景是联系人列表:

  1. 数据准备:将联系人按姓名首字母分组
  2. 组件渲染:使用 IndexBarIndexAnchor 组件渲染联系人列表
  3. 交互处理:实现点击索引跳转到对应联系人分组的功能

城市选择示例

索引栏组件也可以用于城市选择:

  1. 数据准备:将城市按名称首字母分组
  2. 组件渲染:使用 IndexBarIndexAnchor 组件渲染城市列表
  3. 交互处理:实现点击索引跳转到对应城市分组的功能

商品分类示例

索引栏组件还可以用于商品分类:

  1. 数据准备:将商品分类按名称首字母分组
  2. 组件渲染:使用 IndexBarIndexAnchor 组件渲染商品分类列表
  3. 交互处理:实现点击索引跳转到对应商品分类的功能

本文分析的 React Native 索引栏组件展示了如何实现一个功能完整、用户体验良好的索引栏组件。该组件采用了现代化的 React 开发实践,通过 TypeScript 类型系统、动画效果和灵活的配置选项,实现了丰富的功能和良好的用户体验。

组件的技术亮点包括:

  1. 模块化设计:将组件拆分为 IndexAnchorIndexBar 两个独立的组件,提高了代码的可维护性和可复用性
  2. 动画效果:使用 Animated API 实现了流畅的指示器动画,增强了用户体验
  3. 数据处理:使用 reduce 函数实现了高效的联系人分组逻辑
  4. 可配置性:提供了丰富的配置选项,满足不同场景的需求
  5. 跨端兼容性:在设计时充分考虑了 React Native 和 HarmonyOS 的跨端兼容性

该组件可以广泛应用于联系人列表、城市选择、商品分类等场景,为应用提供了直观、高效的导航方式。通过本文的技术解读,希望能够为 React Native 跨端开发提供有益的参考,帮助开发者构建更高质量、更具用户友好性的移动应用。


真实演示案例代码:









import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, TouchableOpacity, Animated } from 'react-native';

// Base64 Icons for IndexBar component
const INDEXBAR_ICONS = {
  location: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDIuNUM4LjU1IDIuNSA1LjUgNS41NSA1LjUgOS4wMUM1LjUgMTUuNSAxMiAyMS41IDEyIDIxLjVDMTIgMjEuNSAxOC41IDE1LjUgMTguNSA5LjAxQzE4LjUgNS41NSAxNS40NSAyLjUgMTIgMi41WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTEyIDkuNWMxLjM4IDAgMi41LTEuMTIgMi41LTIuNXMtMS4xMi0yLjUtMi41LTIuNS0yLjUgMS4xMi0yLjUgMi41IDEuMTIgMi41IDIuNSAyLjVaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  phone: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDhWNUExIDEgMCAwIDAgMTYgM0g4QTEgMSAwIDAgMCA2IDV2M2EyIDIgMCAwIDAgLTIgMnY4YTIgMiAwIDAgMCAyIDJoOGEyIDIgMCAwIDAgMi0yVjEwYTIgMiAwIDAgMC0yLTJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgMTd2MCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  mail: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDIuNUM4LjU1IDIuNSA1LjUgNS41NSA1LjUgOS4wMUM1LjUgMTUuNSAxMiAyMS41IDEyIDIxLjVDMTIgMjEuNSAxOC41IDE1LjUgMTguNSA5LjAxQzE4LjUgNS41NSAxNS40NSAyLjUgMTIgMi41WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTEyIDkuNWMxLjM4IDAgMi41LTEuMTIgMi41LTIuNXMtMS4xMi0yLjUtMi41LTIuNS0yLjUgMS4xMi0yLjUgMi41IDEuMTIgMi41IDIuNSAyLjVaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  user: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNy41OCAyIDQgNS41OCA0IDEwdjRjMCA0LjQyIDMuNTggOCA4IDhzOCAzLjU4IDggOHYtNGMwLTQuNDItMy41OC04LTgtOFYxMFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEwYzAtMi4yIDEuOC00IDQtNCAyLjIgMCA0IDEuOCA0IDQgMCAyLjItMS44IDQtNCA0cy00LTEuOC00LTRaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  star: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggIGQ9Ik0xMiAyLjVMMTQuMDkgNy41MkwxOS41IDEwLjQ4TDE1LjkxIDEzLjQ0TDE3LjE4IDE5LjA5TDEyIDE2LjIzTDYuODIgMTkuMDlMOC4wOSAxMy40NEwyLjY4IDEwLjQ4TDguMDkgNy41MkwxMiAyLjVaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  heart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDIxLjVMMSAxMS4yQzEuNjIgOS41MiAzLjQgOC4yNSA1LjQyIDcuNzFDNy40NSA3LjE3IDkuNjMgNy40IDExLjQ1IDguM0wxMiA4LjU4TDEyLjU1IDguM0MxNC4zNyA3LjQgMTYuNTUgNy4xNyAxOC41OCA3LjcxdDIuODEgMi40M0wxMjIuNSAyMS41WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  settings: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgNHYxNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNNyAxMmgxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K'
};

// IndexAnchor Component
interface IndexAnchorProps {
  index: string;
  children: React.ReactNode;
}

const IndexAnchor: React.FC<IndexAnchorProps> = ({ index, children }) => {
  return (
    <View style={styles.anchorContainer}>
      <Text style={styles.anchorIndex}>{index}</Text>
      {children}
    </View>
  );
};

// IndexBar Component
interface IndexBarProps {
  indices: string[];
  onIndexSelect?: (index: string) => void;
  activeIndex?: string;
  showIndicator?: boolean;
}

const IndexBar: React.FC<IndexBarProps> = ({ 
  indices, 
  onIndexSelect, 
  activeIndex,
  showIndicator = true
}) => {
  const [indicatorVisible, setIndicatorVisible] = useState(false);
  const [currentIndicator, setCurrentIndicator] = useState('');
  const fadeAnim = useRef(new Animated.Value(0)).current;

  const handleIndexPress = (index: string) => {
    setCurrentIndicator(index);
    setIndicatorVisible(true);
    onIndexSelect && onIndexSelect(index);
    
    Animated.sequence([
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 150,
        useNativeDriver: true
      }),
      Animated.delay(1000),
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 150,
        useNativeDriver: true
      })
    ]).start(() => {
      setIndicatorVisible(false);
    });
  };

  return (
    <>
      <View style={styles.indexBarContainer}>
        {indices.map((index) => (
          <TouchableOpacity
            key={index}
            style={[
              styles.indexBarItem,
              activeIndex === index && styles.activeIndexItem
            ]}
            onPress={() => handleIndexPress(index)}
          >
            <Text style={[
              styles.indexBarText,
              activeIndex === index && styles.activeIndexText
            ]}>
              {index}
            </Text>
          </TouchableOpacity>
        ))}
      </View>
      
      {showIndicator && indicatorVisible && (
        <Animated.View 
          style={[
            styles.indicator,
            {
              opacity: fadeAnim,
              transform: [{ scale: fadeAnim.interpolate({
                inputRange: [0, 1],
                outputRange: [0.8, 1]
              })}]
            }
          ]}
        >
          <Text style={styles.indicatorText}>{currentIndicator}</Text>
        </Animated.View>
      )}
    </>
  );
};

// Main App Component
const IndexBarComponentApp = () => {
  const [activeIndex, setActiveIndex] = useState('A');
  const scrollViewRef = useRef<ScrollView>(null);
  
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
  const contacts = [
    { id: '1', name: '阿里巴巴', phone: '138****1234', initial: 'A' },
    { id: '2', name: '百度公司', phone: '139****5678', initial: 'B' },
    { id: '3', name: '腾讯科技', phone: '137****9012', initial: 'T' },
    { id: '4', name: '字节跳动', phone: '136****3456', initial: 'Z' },
    { id: '5', name: '美团点评', phone: '135****7890', initial: 'M' },
    { id: '6', name: '滴滴出行', phone: '134****2345', initial: 'D' },
    { id: '7', name: '拼多多', phone: '133****6789', initial: 'P' },
    { id: '8', name: '京东商城', phone: '132****0123', initial: 'J' },
    { id: '9', name: '快手科技', phone: '131****4567', initial: 'K' },
    { id: '10', name: '小米科技', phone: '130****8901', initial: 'X' }
  ];

  const scrollToIndex = (index: string) => {
    setActiveIndex(index);
    // 这里应该滚动到对应锚点位置
    console.log(`滚动到索引: ${index}`);
  };

  const renderContactItem = (contact: any) => (
    <View key={contact.id} style={styles.contactItem}>
      <View style={styles.contactAvatar}>
        <Text style={styles.contactAvatarText}>{contact.initial}</Text>
      </View>
      <View style={styles.contactInfo}>
        <Text style={styles.contactName}>{contact.name}</Text>
        <Text style={styles.contactPhone}>{contact.phone}</Text>
      </View>
    </View>
  );

  const groupedContacts = alphabet.reduce((acc: any, letter) => {
    acc[letter] = contacts.filter(contact => contact.initial === letter);
    return acc;
  }, {});

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>索引栏组件</Text>
        <Text style={styles.headerSubtitle}>点击索引栏自动跳转到对应位置</Text>
      </View>
      
      <View style={styles.content}>
        <ScrollView 
          ref={scrollViewRef}
          style={styles.scrollView}
          showsVerticalScrollIndicator={false}
        >
          {alphabet.map(letter => (
            <IndexAnchor key={letter} index={letter}>
              {groupedContacts[letter].length > 0 ? (
                <View style={styles.contactGroup}>
                  <Text style={styles.groupTitle}>{letter}</Text>
                  {groupedContacts[letter].map(renderContactItem)}
                </View>
              ) : (
                <View style={styles.emptyGroup}>
                  <Text style={styles.emptyText}>暂无联系人</Text>
                </View>
              )}
            </IndexAnchor>
          ))}
        </ScrollView>
        
        <IndexBar
          indices={alphabet}
          onIndexSelect={scrollToIndex}
          activeIndex={activeIndex}
          showIndicator={true}
        />
      </View>
      
      <View style={styles.featuresSection}>
        <Text style={styles.featuresTitle}>功能特性</Text>
        <View style={styles.featureList}>
          <View style={styles.featureItem}>
            <Text style={styles.featureBullet}></Text>
            <Text style={styles.featureText}>字母索引导航</Text>
          </View>
          <View style={styles.featureItem}>
            <Text style={styles.featureBullet}></Text>
            <Text style={styles.featureText}>点击跳转锚点</Text>
          </View>
          <View style={styles.featureItem}>
            <Text style={styles.featureBullet}></Text>
            <Text style={styles.featureText}>视觉反馈指示器</Text>
          </View>
          <View style={styles.featureItem}>
            <Text style={styles.featureBullet}></Text>
            <Text style={styles.featureText}>平滑滚动体验</Text>
          </View>
          <View style={styles.featureItem}>
            <Text style={styles.featureBullet}></Text>
            <Text style={styles.featureText}>丰富的Base64图标</Text>
          </View>
        </View>
      </View>
      
      <View style={styles.usageSection}>
        <Text style={styles.usageTitle}>使用说明</Text>
        <Text style={styles.usageText}>
          索引栏组件用于快速导航到指定内容位置,
          适用于通讯录、城市选择、分类列表等场景。
        </Text>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 索引栏组件 | 现代化UI组件库</Text>
      </View>
    </View>
  );
};

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  header: {
    backgroundColor: '#0f172a',
    paddingTop: 30,
    paddingBottom: 25,
    paddingHorizontal: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#1e293b',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#f8fafc',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#94a3b8',
    textAlign: 'center',
  },
  content: {
    flex: 1,
    flexDirection: 'row',
  },
  scrollView: {
    flex: 1,
  },
  indexBarContainer: {
    position: 'absolute',
    right: 20,
    top: 20,
    bottom: 20,
    justifyContent: 'center',
    zIndex: 1000,
  },
  indexBarItem: {
    width: 24,
    height: 24,
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
    marginVertical: 2,
  },
  activeIndexItem: {
    backgroundColor: '#3b82f6',
  },
  indexBarText: {
    fontSize: 12,
    color: '#64748b',
    fontWeight: '500',
  },
  activeIndexText: {
    color: '#ffffff',
  },
  indicator: {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: [{ translateX: -30 }, { translateY: -30 }],
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: 'rgba(59, 130, 246, 0.9)',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 1001,
  },
  indicatorText: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#ffffff',
  },
  anchorContainer: {
    marginBottom: 10,
  },
  anchorIndex: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#0f172a',
    backgroundColor: '#f1f5f9',
    paddingVertical: 8,
    paddingHorizontal: 16,
  },
  contactGroup: {
    backgroundColor: '#ffffff',
  },
  groupTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#0f172a',
    padding: 16,
    backgroundColor: '#f8fafc',
  },
  emptyGroup: {
    padding: 30,
    alignItems: 'center',
  },
  emptyText: {
    fontSize: 14,
    color: '#94a3b8',
  },
  contactItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  contactAvatar: {
    width: 48,
    height: 48,
    borderRadius: 24,
    backgroundColor: '#dbeafe',
    justifyContent: 'center',
    alignItems: 'center',
    marginRight: 12,
  },
  contactAvatarText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#3b82f6',
  },
  contactInfo: {
    flex: 1,
  },
  contactName: {
    fontSize: 16,
    fontWeight: '500',
    color: '#0f172a',
    marginBottom: 4,
  },
  contactPhone: {
    fontSize: 14,
    color: '#64748b',
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    padding: 20,
    marginBottom: 20,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.08,
    shadowRadius: 2,
  },
  featuresTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#0f172a',
    marginBottom: 15,
  },
  featureList: {
    paddingLeft: 15,
  },
  featureItem: {
    flexDirection: 'row',
    marginBottom: 10,
  },
  featureBullet: {
    fontSize: 16,
    color: '#3b82f6',
    marginRight: 8,
  },
  featureText: {
    fontSize: 16,
    color: '#64748b',
    flex: 1,
  },
  usageSection: {
    backgroundColor: '#ffffff',
    padding: 20,
    marginBottom: 20,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.08,
    shadowRadius: 2,
  },
  usageTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#0f172a',
    marginBottom: 10,
  },
  usageText: {
    fontSize: 16,
    color: '#64748b',
    lineHeight: 24,
  },
  footer: {
    paddingVertical: 20,
    alignItems: 'center',
    backgroundColor: '#0f172a',
  },
  footerText: {
    color: '#94a3b8',
    fontSize: 14,
  },
});

export default IndexBarComponentApp;

请添加图片描述


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:

请添加图片描述
本文介绍了基于React Native开发的鸿蒙跨平台索引栏组件,通过分层架构设计实现锚点组件、索引栏与业务容器的解耦,支持多终端定制化适配。组件采用原生动画驱动技术,实现流畅的视觉指示器动效,并优化触控区域与布局适配,确保在手机、平板、智慧屏等鸿蒙设备上的一致体验。通过预计算数据分组、惰性渲染等优化手段,提升了组件在低性能设备上的运行效率,为鸿蒙全场景应用提供高效的长列表导航解决方案。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐