【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Tag 标签(通过 type 属性控制标签颜色)

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


在鸿蒙(HarmonyOS)全场景分布式应用生态下,轮播(Swipe)组件作为移动端和大屏端高频使用的UI元素,其跨端开发的核心挑战在于兼顾多终端交互体验一致性、动画流畅性与性能稳定性。本文将从架构设计、核心功能实现、跨端适配策略等维度,深度解读这套 React Native 轮播组件的技术细节,展现如何构建一套适配手机、平板、智慧屏等鸿蒙全场景设备的高性能轮播组件。

状态驱动

这套轮播组件采用"原生组件封装 + React 状态管理"的架构模式,是 React Native 适配鸿蒙跨端开发的典型最佳实践。组件基于 React Native 原生 ScrollView 扩展轮播能力,而非完全自定义动画实现,既利用了原生组件的高性能渲染特性,又通过 React 状态管理实现灵活的业务逻辑控制。

tsx 复制代码
const Swipe: React.FC<SwipeProps> = ({ 
  children,
  autoplay = true,
  autoplayInterval = 3000,
  // 其他属性...
}) => {
  const [currentIndex, setCurrentIndex] = useState(initialIndex);
  const [containerWidth, setContainerWidth] = useState(0);
  const totalItems = React.Children.count(children);
  
  const scrollViewRef = useRef<any>(null);
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  
  // 核心业务逻辑...
};

在状态设计层面,组件构建了完整的状态控制闭环:currentIndex 维护当前展示项索引,containerWidth 动态获取容器宽度以适配不同设备尺寸,scrollViewRef 持有滚动视图引用实现精准的滚动控制,timerRef 管理自动播放定时器避免资源泄漏。所有状态管理均基于 React 核心 Hooks 实现,未依赖任何鸿蒙平台特定 API,保证了组件在纯 React Native 项目、鸿蒙 React Native 适配层、ArkTS 混合开发等不同集成模式下的兼容性。


自动播放与循环控制:

自动播放是轮播组件的核心特性,其实现的关键在于定时器的生命周期管理,这在鸿蒙多终端场景下尤为重要------不同性能的设备(如入门级智慧屏、高性能平板)对定时器的处理机制存在差异,不规范的定时器管理易导致内存泄漏、动画卡顿等问题。

tsx 复制代码
useEffect(() => {
  if (autoplay && totalItems > 1) {
    timerRef.current = setInterval(() => {
      goToNext();
    }, autoplayInterval);
  }

  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  };
}, [autoplay, autoplayInterval, totalItems]);

组件通过 useEffect 钩子实现定时器的创建与销毁,依赖数组精准包含 autoplayautoplayIntervaltotalItems 三个核心参数,确保参数变化时定时器能重新初始化。更关键的是,组件在 useEffect 的返回函数中强制清理定时器,这是 React Native 跨端开发的关键规范------在鸿蒙系统中,组件卸载时未清理的定时器会导致 JS 线程与原生线程的资源占用持续升高,尤其在智慧屏这类长生命周期应用中,该问题会直接影响应用稳定性。

循环播放逻辑则通过纯数学计算实现跨端一致性:

tsx 复制代码
const goToNext = () => {
  if (loop) {
    const nextIndex = (currentIndex + 1) % totalItems;
    goToIndex(nextIndex);
  } else if (currentIndex < totalItems - 1) {
    goToIndex(currentIndex + 1);
  }
};

取模运算 (currentIndex + 1) % totalItems 实现无缝循环,这种与平台无关的计算逻辑,在鸿蒙手机、平板、智慧屏等不同设备上能保持完全一致的循环体验,避免了依赖平台 API 导致的交互差异。

滑动交互:

轮播组件的滑动核心基于 React Native 原生 ScrollView 实现,其属性配置充分考虑了鸿蒙跨端适配需求:

tsx 复制代码
<ScrollView
  ref={scrollViewRef}
  horizontal
  pagingEnabled
  showsHorizontalScrollIndicator={false}
  onScroll={handleScroll}
  scrollEventThrottle={16}
  bounces={false}
>
  {React.Children.map(children, (child, index) => (
    <View key={index} style={styles.swipeItem}>
      {child}
    </View>
  ))}
</ScrollView>

关键属性的设计体现了跨端优化思路:

  • pagingEnabled={true}:启用分页滚动,这是实现"一页一滑"轮播效果的核心,在鸿蒙系统中会被转换为原生的分页滚动逻辑,相比自定义动画具有更高的流畅性和兼容性;
  • scrollEventThrottle={16}:将滚动事件触发频率设置为 16ms(约 60fps),在保证状态同步实时性的同时,避免过高的事件触发频率给鸿蒙低性能设备带来的性能压力;
  • bounces={false}:禁用弹性滚动效果,统一不同鸿蒙设备的滑动体验------部分鸿蒙设备默认的弹性效果与 React Native 标准表现不一致,禁用后可消除终端间的体验差异;
  • showsHorizontalScrollIndicator={false}:隐藏原生滚动指示器,避免不同终端指示器样式不一致的问题。

滑动事件的处理逻辑则保证了状态与视图的精准同步:

tsx 复制代码
const handleScroll = (event: any) => {
  const { contentOffset, layoutMeasurement } = event.nativeEvent;
  const index = Math.round(contentOffset.x / layoutMeasurement.width);
  
  if (index !== currentIndex) {
    setCurrentIndex(index);
    if (onIndexChanged) {
      onIndexChanged(index);
    }
  }
};

通过 contentOffset.x / layoutMeasurement.width 计算当前页码,结合 Math.round 取整,确保滑动结束后状态精准更新。这种基于原生滚动事件的计算方式,相比纯 JS 动画实现,在鸿蒙系统中具有更低的性能损耗,尤其适配智慧屏等对性能敏感的设备。

动态布局

轮播组件的布局适配核心在于动态获取容器宽度,这是解决鸿蒙不同屏幕尺寸设备适配问题的关键:

tsx 复制代码
const handleLayout = (event: any) => {
  const { width } = event.nativeEvent.layout;
  setContainerWidth(width);
};

const goToIndex = (index: number) => {
  if (index >= 0 && index < totalItems) {
    setCurrentIndex(index);
    if (scrollViewRef.current) {
      scrollViewRef.current.scrollTo({
        x: index * containerWidth,
        animated: true
      });
    }
  }
};

组件通过 onLayout 事件实时获取容器宽度,而非硬编码尺寸,保证了在鸿蒙手机(窄屏)、平板(宽屏)、智慧屏(超大屏)等不同设备上的自适应能力。scrollTo 方法结合动态宽度计算滚动位置,避免了固定宽度导致的滚动偏移问题,是 React Native 适配鸿蒙多终端布局的核心技巧。

分页与指示器:

分页指示器(Pagination)和操作指示器(Indicator)的实现兼顾了视觉与交互的跨端一致性:

tsx 复制代码
const renderPagination = () => {
  if (!showPagination) return null;
  
  return (
    <View style={styles.paginationContainer}>
      {Array.from({ length: totalItems }).map((_, index) => (
        <TouchableOpacity
          key={index}
          style={[
            styles.paginationDot,
            currentIndex === index && styles.paginationDotActive
          ]}
          onPress={() => goToIndex(index)}
        />
      ))}
    </View>
  );
};

分页指示器采用纯 View 组件实现,避免了图片/SVG 资源在不同鸿蒙设备上的适配问题;TouchableOpacity 作为 React Native 跨平台基础交互组件,在鸿蒙系统中会被转换为原生可点击视图,保证了点击反馈的一致性。分页位置支持 top/bottom 配置,可适配不同终端的UI设计规范------智慧屏通常将分页指示器置于顶部,手机端则多置于底部。

操作指示器则提供了明确的手动控制能力,尤其适配鸿蒙智慧屏的遥控器操作场景:

tsx 复制代码
<View style={styles.indicatorContainer}>
  <TouchableOpacity style={styles.indicatorButton} onPress={goToPrev}>
    <Image source={{ uri: SWIPE_ICONS.arrowLeft }} style={styles.indicatorIcon} />
  </TouchableOpacity>
  <Text style={styles.indicatorText}>{currentIndex + 1} / {totalItems}</Text>
  <TouchableOpacity style={styles.indicatorButton} onPress={goToNext}>
    <Image source={{ uri: SWIPE_ICONS.arrowRight }} style={styles.indicatorIcon} />
  </TouchableOpacity>
</View>

明确的页码提示和方向按钮,解决了智慧屏等非触控设备依赖遥控器操作的交互痛点,实现了多终端交互体验的全覆盖。


SwipeComponentApp 主组件中,展示了轮播组件与业务逻辑的集成方式,体现了 React Native 鸿蒙跨端开发的最佳实践:

  1. 数据驱动渲染:轮播内容通过数组配置化渲染,每个轮播项的样式、内容、背景色均可独立配置,便于根据鸿蒙不同设备的特性(如智慧屏的超大尺寸、手机的竖屏比例)调整展示内容;
  2. 状态回调解耦 :通过 onIndexChanged 回调获取当前轮播索引,实现业务层与组件层的状态解耦,便于对接鸿蒙分布式数据管理能力,实现多设备间轮播状态的同步;
  3. 样式标准化:所有样式均通过 StyleSheet 定义,采用 dp 单位而非像素单位,保证在不同屏幕密度的鸿蒙设备上视觉大小一致,避免了因像素密度差异导致的UI变形。

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

原生能力

基于 React Native 原生 ScrollView 实现核心轮播逻辑,而非自定义 JS 动画,原生组件在鸿蒙系统中会被转换为对应的 ArkUI 组件,相比纯 JS 实现具有更低的性能损耗和更高的兼容性。

动态适配

通过 onLayout 动态获取容器尺寸,结合索引计算滚动位置,适配鸿蒙全场景设备的屏幕尺寸差异,避免了固定尺寸导致的适配漏洞。

定时器

严格遵循 React 生命周期规范管理定时器,避免资源泄漏;采用 URI 形式加载图片资源,便于对接鸿蒙分布式资源管理体系,实现多设备资源共享。

交互体验

统一不同终端的交互规则,滑动、点击、自动播放的核心逻辑在手机、平板、智慧屏上保持一致,仅通过样式调整适配不同设备的展示规范。

总结

这套 React Native 轮播组件不仅实现了自动播放、循环滚动、分页控制等核心功能,更构建了一套完整的鸿蒙跨端适配体系。核心要点可总结为:

  • 架构层面:原生组件封装 + React 状态管理,兼顾跨端性能与逻辑灵活性;
  • 交互层面:统一滑动、点击、自动播放逻辑,适配鸿蒙多终端操作习惯;
  • 布局层面:动态尺寸计算,适配不同屏幕尺寸的鸿蒙设备;
  • 资源层面:规范定时器与资源管理,对接鸿蒙分布式能力体系。

在实际的鸿蒙跨端项目中,可基于该组件进一步扩展垂直轮播、手势缩放、跨设备内容推送等能力,充分发挥 React Native "一次开发,多端部署"的技术优势,构建真正适配鸿蒙全场景生态的轮播组件。


本文将深入分析一个功能完整的 React Native 轮播组件实现,该组件采用了现代化的函数式组件架构,支持自动播放、循环滚动、指示器显示等多种功能,同时兼顾了 React Native 与 HarmonyOS 的跨端兼容性。

接口设计

组件首先通过 TypeScript 接口定义了核心配置选项:

typescript 复制代码
interface SwipeProps {
  children: React.ReactNode[];
  autoplay?: boolean;
  autoplayInterval?: number;
  showIndicators?: boolean;
  showPagination?: boolean;
  paginationPosition?: 'bottom' | 'top';
  loop?: boolean;
  initialIndex?: number;
  onIndexChanged?: (index: number) => void;
}

这种类型定义方式体现了良好的 TypeScript 实践,通过可选属性和字面量类型,提供了清晰的配置选项和类型约束,确保了组件使用时的类型安全。

核心状态管理

轮播组件使用了多个状态来管理其行为:

typescript 复制代码
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [offsetX, setOffsetX] = useState(new Animated.Value(0));
const [containerWidth, setContainerWidth] = useState(0);
  1. currentIndex:当前显示的幻灯片索引,是轮播组件的核心状态
  2. offsetX:滚动偏移量,用于动画效果,虽然在当前实现中未直接使用,但为后续扩展预留了空间
  3. containerWidth:容器宽度,用于计算滚动位置和幻灯片尺寸

同时,组件使用了 useRef 来管理 DOM 引用和定时器:

typescript 复制代码
const scrollViewRef = useRef<any>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);

自动播放

轮播组件的自动播放功能通过 useEffect 和定时器实现:

typescript 复制代码
useEffect(() => {
  if (autoplay && totalItems > 1) {
    timerRef.current = setInterval(() => {
      goToNext();
    }, autoplayInterval);
  }

  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  };
}, [autoplay, autoplayInterval, totalItems]);

这个实现的技术要点:

  1. 仅当 autoplay 为 true 且幻灯片数量大于 1 时才启用自动播放,避免了不必要的性能消耗
  2. 使用 setInterval 定期调用 goToNext 函数切换到下一张幻灯片
  3. useEffect 的清理函数中清除定时器,确保在组件卸载或依赖项变化时不会内存泄漏
  4. 依赖项数组包含了 autoplayautoplayIntervaltotalItems,确保当这些配置变化时,定时器会重新设置

幻灯片切换

组件实现了完整的幻灯片切换逻辑:

  1. goToNext:切换到下一张幻灯片,支持循环
  2. goToPrev:切换到上一张幻灯片,支持循环
  3. goToIndex:直接跳转到指定索引的幻灯片
  4. handleScroll:处理用户手动滚动事件,更新当前索引
typescript 复制代码
const goToNext = () => {
  if (loop) {
    const nextIndex = (currentIndex + 1) % totalItems;
    goToIndex(nextIndex);
  } else if (currentIndex < totalItems - 1) {
    goToIndex(currentIndex + 1);
  }
};

const goToPrev = () => {
  if (loop) {
    const prevIndex = currentIndex === 0 ? totalItems - 1 : currentIndex - 1;
    goToIndex(prevIndex);
  } else if (currentIndex > 0) {
    goToIndex(currentIndex - 1);
  }
};

const goToIndex = (index: number) => {
  if (index >= 0 && index < totalItems) {
    setCurrentIndex(index);
    if (onIndexChanged) {
      onIndexChanged(index);
    }
    
    if (scrollViewRef.current) {
      scrollViewRef.current.scrollTo({
        x: index * containerWidth,
        animated: true
      });
    }
  }
};

滚动事件

组件通过 handleScroll 函数处理用户的手动滚动事件:

typescript 复制代码
const handleScroll = (event: any) => {
  const { contentOffset, layoutMeasurement } = event.nativeEvent;
  const index = Math.round(contentOffset.x / layoutMeasurement.width);
  
  if (index !== currentIndex) {
    setCurrentIndex(index);
    if (onIndexChanged) {
      onIndexChanged(index);
    }
  }
};

这个实现的技术要点:

  1. 通过 contentOffset.xlayoutMeasurement.width 计算当前幻灯片的索引
  2. 使用 Math.round 确保索引计算的准确性,避免因滚动位置微小差异导致的索引跳动
  3. 仅当索引发生变化时才更新状态,避免不必要的重渲染
  4. 调用 onIndexChanged 回调通知父组件索引变化,实现组件间的通信

组件通过 handleLayout 函数获取容器宽度:

typescript 复制代码
const handleLayout = (event: any) => {
  const { width } = event.nativeEvent.layout;
  setContainerWidth(width);
};

这个宽度用于计算幻灯片的位置和滚动目标,确保了轮播组件的响应式布局,能够适应不同尺寸的屏幕。

分页指示器

组件实现了可定制的分页指示器:

typescript 复制代码
const renderPagination = () => {
  if (!showPagination) return null;
  
  return (
    <View style={styles.paginationContainer}>
      {Array.from({ length: totalItems }).map((_, index) => (
        <TouchableOpacity
          key={index}
          style={[
            styles.paginationDot,
            currentIndex === index && styles.paginationDotActive
          ]}
          onPress={() => goToIndex(index)}
        />
      ))}
    </View>
  );
};

这个实现的技术要点:

  1. 根据 showPagination 控制是否显示分页指示器
  2. 根据幻灯片数量动态生成分页点
  3. 根据当前索引高亮显示对应的分页点
  4. 支持点击分页点直接跳转到对应幻灯片

主应用

主应用组件展示了轮播组件的使用示例:

typescript 复制代码
const SwipeComponentApp = () => {
  const [currentSwiperIndex, setCurrentSwiperIndex] = useState(0);
  
  const bannerItems = [
    {
      id: 1,
      title: '夏日清凉特惠',
      subtitle: '享受夏日清凉,全场商品7折起',
      image: ' `https://picsum.photos/id/10/800/400` ',
      bgColor: '#e0f2fe'
    },
    // 更多幻灯片数据...
  ];

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>轮播组件</Text>
        <Text style={styles.headerSubtitle}>循环播放图片或内容</Text>
      </View>
      
      <View style={styles.content}>
        <View style={styles.bannerSection}>
          <Swipe
            autoplay={true}
            autoplayInterval={4000}
            showIndicators={true}
            showPagination={true}
            onIndexChanged={setCurrentSwiperIndex}
          >
            {/* 幻灯片内容 */}
          </Swipe>
        </View>
      </View>
    </View>
  );
};

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

  1. 组件兼容性 :使用的 ViewTextTouchableOpacityScrollViewImage 等组件在 React Native 和 HarmonyOS 中都有对应实现
  2. API 兼容性 :使用的 useStateuseEffectuseRef 等 Hooks 在两个平台上都可用
  3. 样式兼容性 :使用的 StyleSheet API 在两个平台上的使用方式基本一致,flexbox 布局在两个平台上都得到了良好支持
  4. 事件处理:使用的滚动事件、触摸事件等在两个平台上都有对应的实现
  5. 动画 API :使用的 Animated API 在两个平台上都有实现,虽然可能存在一些细微差异,但核心功能是一致的

跨端

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

  1. 滚动性能:不同平台的滚动性能可能存在差异,需要进行性能测试和优化
  2. 动画效果:不同平台的动画 API 可能存在差异,需要确保动画效果一致
  3. 定时器处理:不同平台的定时器实现可能存在差异,需要确保定时器的正确创建和清除
  4. 触摸反馈:不同平台的触摸反馈机制可能存在差异,需要确保交互体验一致
  5. 图片加载:不同平台的图片加载机制可能存在差异,需要确保图片的正确加载和显示

组件设计

  1. 可配置性:轮播组件提供了丰富的配置选项,如自动播放、循环、指示器、分页等,满足不同场景的需求
  2. 模块化设计:组件结构清晰,逻辑分明,易于理解和维护
  3. 组合式设计 :通过 children 属性支持任意内容的轮播,提高了组件的灵活性和可复用性
  4. 响应式布局 :通过 handleLayout 获取容器宽度,实现了响应式的轮播布局
  5. 可扩展性:组件设计考虑了未来的扩展,如垂直轮播、自定义动画等

状态管理策略

  1. 精细化状态:使用多个状态变量管理不同的状态,如当前索引、偏移量、容器宽度等
  2. 引用管理 :使用 useRef 管理 DOM 引用和定时器,避免了不必要的状态更新
  3. 副作用管理 :使用 useEffect 管理定时器等副作用,确保资源的正确创建和释放
  4. 单一数据源 :使用 currentIndex 作为单一数据源,确保了状态的一致性

轮播组件的核心是通过 ScrollView 实现的:

typescript 复制代码
<ScrollView
  ref={scrollViewRef}
  horizontal
  pagingEnabled
  showsHorizontalScrollIndicator={false}
  onScroll={handleScroll}
  scrollEventThrottle={16}
  bounces={false}
>
  {React.Children.map(children, (child, index) => (
    <View key={index} style={styles.swipeItem}>
      {child}
    </View>
  ))}
</ScrollView>

技术要点:

  1. horizontal:设置为水平滚动
  2. pagingEnabled:启用分页效果,确保每次滚动停留在完整的幻灯片上
  3. showsHorizontalScrollIndicator:隐藏水平滚动指示器,提高视觉效果
  4. scrollEventThrottle:控制滚动事件的触发频率,平衡性能和交互体验
  5. bounces:禁用弹性效果,确保滚动行为更精确
  6. React.Children.map:遍历子元素,为每个子元素添加包装容器

自动播放实现

自动播放功能通过 setInterval 实现:

typescript 复制代码
if (autoplay && totalItems > 1) {
  timerRef.current = setInterval(() => {
    goToNext();
  }, autoplayInterval);
}

技术要点:

  1. 仅当 autoplay 为 true 且幻灯片数量大于 1 时才启用自动播放
  2. 使用 timerRef 存储定时器引用,以便在组件卸载或配置变化时清除
  3. 调用 goToNext 函数实现自动切换

组件的技术亮点包括:

  1. 丰富的功能:支持自动播放、循环滚动、指示器、分页等多种功能
  2. 灵活的配置:提供了丰富的配置选项,满足不同场景的需求
  3. 优秀的性能:通过多种优化策略,确保了组件的性能和流畅度
  4. 跨端兼容性:在设计时充分考虑了 React Native 和 HarmonyOS 的跨端兼容性
  5. 良好的可维护性:代码结构清晰,逻辑分明,易于理解和维护

该组件可以广泛应用于广告轮播、产品展示、教程引导等场景,为用户提供直观、流畅的内容浏览体验。通过本文的技术解读,希望能够为 React Native 跨端开发提供有益的参考,帮助开发者构建更高质量、更具用户友好性的移动应用。

在未来的开发中,轮播组件可以进一步扩展功能,如垂直轮播、自定义动画、手势控制等,同时不断优化性能和跨端兼容性,为用户提供更加出色的体验。


真实演示案例代码:

js 复制代码
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, Animated, Image, TouchableOpacity } from 'react-native';

// Base64 Icons for Swipe component
const SWIPE_ICONS = {
  arrowLeft: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDE1TDExIDExTDE1IDciIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
  arrowRight: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDE1TDE0IDExTDEwIDciIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
  play: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDl2Nm00LTYvNCAzLTMgMyIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
  pause: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDd2MTBNMTQgN3YxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
  heart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDIxLjVMMSAxMS4yQzEuNjIgOS41MiAzLjQgOC4yNSA1LjQyIDcuNzFDNy40NSA3LjE3IDkuNjMgNy40IDExLjQ1IDguM0wxMiA4LjU4TDEyLjU1IDguM0MxNC4zNyA3LjQgMTYuNTUgNy4xNyAxOC41OCA3LjcxdDIuODEgMi40M0wxMjIuNSAyMS41WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  share: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDdDNy4yIDcgNy4yIDIxIDE4IDIxIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTggN0M3LjIgNyA3LjIgMjEgMTggMjEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA3QzcuMiA3IDcuMiAyMSAxOCAyMSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  close: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDE4TDYgNiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNNiAxOEwxOCA2IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=',
  dot: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDEyQzEyIDEyIDEyIDEyIDEyIDEyIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'
};

// Swipe Component
interface SwipeProps {
  children: React.ReactNode[];
  autoplay?: boolean;
  autoplayInterval?: number;
  showIndicators?: boolean;
  showPagination?: boolean;
  paginationPosition?: 'bottom' | 'top';
  loop?: boolean;
  initialIndex?: number;
  onIndexChanged?: (index: number) => void;
}

const Swipe: React.FC<SwipeProps> = ({ 
  children,
  autoplay = true,
  autoplayInterval = 3000,
  showIndicators = true,
  showPagination = true,
  paginationPosition = 'bottom',
  loop = true,
  initialIndex = 0,
  onIndexChanged
}) => {
  const [currentIndex, setCurrentIndex] = useState(initialIndex);
  const [offsetX, setOffsetX] = useState(new Animated.Value(0));
  const [containerWidth, setContainerWidth] = useState(0);
  const totalItems = React.Children.count(children);
  
  const scrollViewRef = useRef<any>(null);
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  // Auto play functionality
  useEffect(() => {
    if (autoplay && totalItems > 1) {
      timerRef.current = setInterval(() => {
        goToNext();
      }, autoplayInterval);
    }

    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, [autoplay, autoplayInterval, totalItems]);

  const goToNext = () => {
    if (loop) {
      const nextIndex = (currentIndex + 1) % totalItems;
      goToIndex(nextIndex);
    } else if (currentIndex < totalItems - 1) {
      goToIndex(currentIndex + 1);
    }
  };

  const goToPrev = () => {
    if (loop) {
      const prevIndex = currentIndex === 0 ? totalItems - 1 : currentIndex - 1;
      goToIndex(prevIndex);
    } else if (currentIndex > 0) {
      goToIndex(currentIndex - 1);
    }
  };

  const goToIndex = (index: number) => {
    if (index >= 0 && index < totalItems) {
      setCurrentIndex(index);
      if (onIndexChanged) {
        onIndexChanged(index);
      }
      
      if (scrollViewRef.current) {
        scrollViewRef.current.scrollTo({
          x: index * containerWidth,
          animated: true
        });
      }
    }
  };

  const handleScroll = (event: any) => {
    const { contentOffset, layoutMeasurement } = event.nativeEvent;
    const index = Math.round(contentOffset.x / layoutMeasurement.width);
    
    if (index !== currentIndex) {
      setCurrentIndex(index);
      if (onIndexChanged) {
        onIndexChanged(index);
      }
    }
  };

  const handleLayout = (event: any) => {
    const { width } = event.nativeEvent.layout;
    setContainerWidth(width);
  };

  const renderPagination = () => {
    if (!showPagination) return null;
    
    return (
      <View style={styles.paginationContainer}>
        {Array.from({ length: totalItems }).map((_, index) => (
          <TouchableOpacity
            key={index}
            style={[
              styles.paginationDot,
              currentIndex === index && styles.paginationDotActive
            ]}
            onPress={() => goToIndex(index)}
          />
        ))}
      </View>
    );
  };

  return (
    <View style={styles.swipeContainer}>
      <View onLayout={handleLayout} style={styles.swipeLayout}>
        <ScrollView
          ref={scrollViewRef}
          horizontal
          pagingEnabled
          showsHorizontalScrollIndicator={false}
          onScroll={handleScroll}
          scrollEventThrottle={16}
          bounces={false}
        >
          {React.Children.map(children, (child, index) => (
            <View key={index} style={styles.swipeItem}>
              {child}
            </View>
          ))}
        </ScrollView>
      </View>
      
      {paginationPosition === 'top' && renderPagination()}
      
      {showIndicators && totalItems > 1 && (
        <View style={styles.indicatorContainer}>
          <TouchableOpacity style={styles.indicatorButton} onPress={goToPrev}>
            <Image source={{ uri: SWIPE_ICONS.arrowLeft }} style={styles.indicatorIcon} />
          </TouchableOpacity>
          <Text style={styles.indicatorText}>{currentIndex + 1} / {totalItems}</Text>
          <TouchableOpacity style={styles.indicatorButton} onPress={goToNext}>
            <Image source={{ uri: SWIPE_ICONS.arrowRight }} style={styles.indicatorIcon} />
          </TouchableOpacity>
        </View>
      )}
      
      {paginationPosition === 'bottom' && renderPagination()}
    </View>
  );
};

// Main App Component
const SwipeComponentApp = () => {
  const [currentSwiperIndex, setCurrentSwiperIndex] = useState(0);
  
  const bannerItems = [
    {
      id: 1,
      title: '夏日清凉特惠',
      subtitle: '享受夏日清凉,全场商品7折起',
      image: 'https://picsum.photos/id/10/800/400',
      bgColor: '#e0f2fe'
    },
    {
      id: 2,
      title: '新品首发',
      subtitle: '最新科技产品,抢先体验',
      image: 'https://picsum.photos/id/11/800/400',
      bgColor: '#fce7f3'
    },
    {
      id: 3,
      title: '会员专享',
      subtitle: '尊享会员特权,专属优惠不断',
      image: 'https://picsum.photos/id/12/800/400',
      bgColor: '#f0f9ff'
    },
    {
      id: 4,
      title: '限时抢购',
      subtitle: '超值好物,限时低价',
      image: 'https://picsum.photos/id/13/800/400',
      bgColor: '#f0fdf4'
    }
  ];

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>轮播组件</Text>
        <Text style={styles.headerSubtitle}>循环播放图片或内容</Text>
      </View>
      
      <View style={styles.content}>
        <View style={styles.bannerSection}>
          <Swipe
            autoplay={true}
            autoplayInterval={4000}
            showIndicators={true}
            showPagination={true}
            onIndexChanged={setCurrentSwiperIndex}
          >
            {bannerItems.map((item, index) => (
              <View key={item.id} style={[styles.bannerItem, { backgroundColor: item.bgColor }]}>
                <Image source={{ uri: item.image }} style={styles.bannerImage} />
                <View style={styles.bannerContent}>
                  <Text style={styles.bannerTitle}>{item.title}</Text>
                  <Text style={styles.bannerSubtitle}>{item.subtitle}</Text>
                  <TouchableOpacity style={styles.bannerButton}>
                    <Text style={styles.bannerButtonText}>立即购买</Text>
                  </TouchableOpacity>
                </View>
              </View>
            ))}
          </Swipe>
        </View>
        
        <View style={styles.infoSection}>
          <Text style={styles.infoTitle}>轮播内容</Text>
          <Text style={styles.infoDescription}>
            当前显示: {bannerItems[currentSwiperIndex]?.title}
          </Text>
        </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>
      
      <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: '#f8fafc',
  },
  header: {
    backgroundColor: '#1e293b',
    paddingTop: 30,
    paddingBottom: 25,
    paddingHorizontal: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#334155',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#f1f5f9',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#94a3b8',
    textAlign: 'center',
  },
  content: {
    flex: 1,
    padding: 20,
  },
  bannerSection: {
    marginBottom: 25,
  },
  swipeContainer: {
    borderRadius: 12,
    overflow: 'hidden',
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  swipeLayout: {
    height: 200,
  },
  swipeItem: {
    width: width - 40,
    height: 200,
  },
  bannerItem: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    position: 'relative',
  },
  bannerImage: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    width: undefined,
    height: undefined,
  },
  bannerContent: {
    zIndex: 1,
    padding: 20,
    alignItems: 'center',
  },
  bannerTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
    textAlign: 'center',
  },
  bannerSubtitle: {
    fontSize: 16,
    color: '#64748b',
    marginBottom: 16,
    textAlign: 'center',
  },
  bannerButton: {
    backgroundColor: '#3b82f6',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 20,
  },
  bannerButtonText: {
    color: '#ffffff',
    fontWeight: '500',
  },
  indicatorContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 15,
    backgroundColor: 'rgba(0, 0, 0, 0.2)',
  },
  indicatorButton: {
    width: 30,
    height: 30,
    borderRadius: 15,
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  indicatorIcon: {
    width: 16,
    height: 16,
    tintColor: '#ffffff',
  },
  indicatorText: {
    color: '#ffffff',
    fontSize: 14,
    fontWeight: '500',
  },
  paginationContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    padding: 10,
    backgroundColor: 'rgba(0, 0, 0, 0.2)',
  },
  paginationDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    marginHorizontal: 4,
  },
  paginationDotActive: {
    backgroundColor: '#ffffff',
    width: 12,
    height: 12,
  },
  infoSection: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 20,
    marginBottom: 25,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.08,
    shadowRadius: 2,
  },
  infoTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#0f172a',
    marginBottom: 10,
  },
  infoDescription: {
    fontSize: 16,
    color: '#64748b',
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 20,
    marginBottom: 25,
    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',
    borderRadius: 12,
    padding: 20,
    marginBottom: 30,
    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: '#1e293b',
  },
  footerText: {
    color: '#94a3b8',
    fontSize: 14,
  },
});

export default SwipeComponentApp;

打包

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

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

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

本文介绍了基于React Native开发的跨平台轮播组件在鸿蒙生态中的实现方案。该组件采用"原生组件封装+React状态管理"架构,通过ScrollView扩展轮播功能,兼顾性能与灵活性。关键实现包括:1) 基于useEffect的自动播放定时器管理,确保多终端稳定性;2) 纯数学计算的循环播放逻辑,保证跨端一致性;3) 动态布局适配不同设备尺寸;4) 原生滚动事件处理优化性能;5) 纯View实现的分页指示器确保视觉统一。组件设计充分考虑了鸿蒙手机、平板、智慧屏等设备的交互差异,通过平台无关的JS逻辑实现高性能跨端适配,为鸿蒙全场景应用开发提供了可靠解决方案。

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

相关推荐
sdff113962 小时前
【HarmonyOS】flutter of HarmonyOS 实战项目+智慧家居控制面板全解
flutter·华为·harmonyos
程序哥聊面试3 小时前
第一课:React的Hooks
前端·javascript·react.js
东东5163 小时前
基于SSM的宠物医院预约挂号系统的设计与实现vue
java·前端·javascript·vue.js·毕设
特立独行的猫a3 小时前
腾讯Kuikly多端框架(KMP)实战:轮播图的完整实现
android·harmonyos·轮播图·jetpack compose·kuikly
黑贝是条狗3 小时前
mormor2与vue搭建一个博客系统
前端·javascript·vue.js
GISer_Jing3 小时前
Taro 5.0 深度:跨端开发的架构革新与全阶实践指南
前端·react.js·taro
GISer_Jing3 小时前
Taro 5.0 小白快速上手指南:从0到1实现跨端开发
前端·react.js·taro
熊猫钓鱼>_>3 小时前
【开源鸿蒙跨平台开发先锋训练营】React Native 性能巅峰:HarmonyOS极致优化实战手册
react native·react.js·华为·开源·harmonyos·鸿蒙·openharmony
前端不太难3 小时前
大型鸿蒙 App,先过“三层解耦”这一关
华为·状态模式·harmonyos