React Native for OpenHarmony 实战:Swiper 滑动组件详解

React Native for OpenHarmony 实战:Swiper 滑动组件详解

摘要

本文深入探讨React Native中Swiper滑动组件在OpenHarmony平台上的实战应用。作为移动应用开发中常用的轮播图和滑动切换组件,Swiper在跨平台开发中面临诸多挑战。文章从Swiper组件原理入手,详细解析了react-native-swiper库在OpenHarmony环境下的适配要点,通过7个实战代码示例展示了基础用法、性能优化和平台特有问题的解决方案。特别针对OpenHarmony特有的图形渲染机制和触摸事件处理差异提供了详细指导,并附有完整的性能对比数据和常见问题解决方案表格。阅读本文,你将掌握在OpenHarmony设备上高效使用Swiper组件的全套技能,避免踩坑,提升开发效率。

引言

在移动应用开发中,Swiper(滑动组件/轮播图)是极为常见的UI元素,广泛应用于首页广告位、产品展示、引导页等场景。作为React Native开发者,我们经常需要实现这种流畅的滑动切换效果,而在OpenHarmony平台上实现这一功能却面临着独特的挑战。

OpenHarmony作为国产操作系统,其图形渲染机制、触摸事件处理与Android/iOS存在差异,导致许多在标准React Native环境中运行良好的组件在OpenHarmony上表现异常。特别是像Swiper这样高度依赖触摸交互和动画效果的组件,更容易出现滑动不流畅、动画卡顿、手势冲突等问题。

在我过去一年的OpenHarmony跨平台开发实践中,Swiper组件的适配是我遇到的最具挑战性的任务之一。记得第一次在OpenHarmony 3.2设备上测试Swiper时,滑动效果异常卡顿,甚至出现了触摸事件丢失的情况,而同样的代码在Android设备上却运行流畅。经过深入排查和多次迭代,我终于找到了问题根源并实现了稳定高效的Swiper组件。

本文将基于我5年React Native开发经验和近1年OpenHarmony平台适配经验,全面解析Swiper组件在OpenHarmony平台上的使用技巧。无论你是刚接触OpenHarmony的React Native开发者,还是正在解决Swiper适配问题的老手,相信都能从本文中获得有价值的信息。

Swiper 组件介绍

Swiper 的定义与核心功能

Swiper(滑动组件)是一种允许用户通过水平或垂直滑动来切换内容的UI组件,通常用于实现轮播图、引导页、图片浏览等场景。在React Native生态系统中,虽然官方核心库没有提供原生Swiper组件,但社区提供了多个高质量的第三方实现,其中react-native-swiper是最为流行的选择。

Swiper组件的核心功能包括:

  • 滑动切换:用户通过手指滑动实现内容切换
  • 自动轮播:定时自动切换到下一页
  • 分页指示器:显示当前页码和总页数
  • 触摸暂停:用户触摸时暂停自动轮播
  • 自定义动画:支持不同的切换动画效果

技术实现原理

react-native-swiper组件主要基于React Native的ScrollViewFlatList实现,其核心原理如下:

  1. 布局结构:将所有子视图水平排列在一个容器中,容器宽度为单个子视图宽度乘以子视图数量
  2. 滚动控制 :通过ScrollViewscrollTo方法实现平滑滚动到指定位置
  3. 触摸事件处理:监听触摸开始、移动和结束事件,计算滑动距离和方向
  4. 动画实现 :使用Animated API实现过渡动画效果
  5. 状态管理:跟踪当前页码、自动轮播定时器等状态

在OpenHarmony平台上,由于图形渲染引擎和触摸事件处理机制与Android/iOS存在差异,这些核心实现细节都需要进行针对性调整。

Swiper 在 OpenHarmony 上的应用场景

在OpenHarmony应用开发中,Swiper组件有以下典型应用场景:

  • 首页轮播广告:电商、新闻类应用的首页焦点图
  • 产品展示:商品详情页的图片轮播
  • 应用引导页:新用户首次启动应用时的介绍页面
  • 图片浏览:相册应用中的图片查看功能
  • 卡片式布局:信息卡片的左右滑动切换

这些场景在OpenHarmony设备上都有实际应用需求,但实现时需要考虑OpenHarmony平台的特性。

Swiper 组件架构图

Swiper 组件
核心容器
分页指示器
自动轮播控制器
ScrollView/FlatList
子视图容器
触摸事件处理器
滚动控制
动画处理
子视图1
子视图2
...
子视图N
当前页码
总页数
样式配置
定时器
触摸暂停
恢复播放

Swiper组件架构图展示了Swiper的核心组成部分及其相互关系。在OpenHarmony平台上,各部分都需要进行针对性适配,特别是触摸事件处理和动画实现部分。

React Native 与 OpenHarmony 平台适配要点

OpenHarmony 对 React Native 的支持现状

OpenHarmony 3.2及以上版本通过ArkUI提供了对React Native的良好支持,但与标准React Native环境相比仍存在一些差异:

  1. 图形渲染引擎:OpenHarmony使用自己的渲染引擎,与Android的Skia或iOS的Core Animation不同
  2. 触摸事件处理:OpenHarmony的触摸事件模型与Android/iOS有细微差别
  3. 动画系统 :OpenHarmony对React Native的Animated API支持有限
  4. 布局计算:Flexbox布局在OpenHarmony上的计算结果可能略有差异

Swiper 组件适配的关键挑战

在将Swiper组件适配到OpenHarmony平台时,我遇到了以下关键挑战:

  1. 触摸事件丢失问题:在某些OpenHarmony设备上,快速滑动时会出现触摸事件丢失,导致滑动不连续
  2. 滚动性能问题:OpenHarmony的滚动性能不如Android,特别是在低端设备上
  3. 动画卡顿:复杂的切换动画在OpenHarmony上容易出现卡顿
  4. 布局计算差异:子视图宽度计算在不同设备上不一致
  5. 自动轮播定时器精度:OpenHarmony的定时器精度较低,影响自动轮播的平滑度

解决方案概览

针对上述挑战,我总结了以下解决方案:

  1. 触摸事件增强 :使用PanResponder替代默认的触摸处理,提高事件捕获率
  2. 性能优化 :减少重绘区域,使用shouldComponentUpdate优化渲染
  3. 动画简化:在OpenHarmony上使用更简单的动画效果
  4. 布局计算修正 :使用onLayout事件动态计算子视图尺寸
  5. 定时器优化 :使用requestAnimationFrame替代setInterval提高定时精度

OpenHarmony 特定适配流程

OpenHarmony React Native 开发者 OpenHarmony React Native 开发者 alt [OpenHarmony平台] [其他平台] 编写Swiper组件代码 调用原生模块 图形渲染 触摸事件处理 检查平台特性 应用特定适配 返回适配结果 显示Swiper组件 返回标准结果 显示Swiper组件

Swiper组件在OpenHarmony平台上的适配流程。开发者需要在代码中添加平台检测逻辑,针对OpenHarmony应用特定优化。

Swiper 基础用法实战

环境准备与依赖安装

在开始使用Swiper组件前,我们需要确保开发环境正确配置。我使用的环境如下:

  • Node.js: 18.17.0
  • React Native: 0.72.5
  • OpenHarmony SDK: 3.2.11.9
  • react-native-swiper: 2.0.0

安装Swiper组件库:

bash 复制代码
npm install react-native-swiper
# 或
yarn add react-native-swiper

⚠️ 重要提示:在OpenHarmony环境下,需要额外安装适配层:

bash 复制代码
npm install @ohos/react-native-swiper-adapter
# 或
yarn add @ohos/react-native-swiper-adapter

这个适配层解决了OpenHarmony平台特有的触摸事件和动画问题,是确保Swiper组件正常工作的关键。

基础 Swiper 实现

下面是最基础的Swiper实现代码,展示了如何创建一个包含3张图片的轮播图:

javascript 复制代码
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import Swiper from 'react-native-swiper';

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

const BasicSwiper = () => {
  return (
    <View style={styles.container}>
      <Swiper style={styles.wrapper} showsButtons={false}>
        <View style={[styles.slide, { backgroundColor: '#9DD6EB' }]}>
          <Text style={styles.text}>Hello Swiper</Text>
        </View>
        <View style={[styles.slide, { backgroundColor: '#97CAE5' }]}>
          <Text style={styles.text}>Beautiful</Text>
        </View>
        <View style={[styles.slide, { backgroundColor: '#92BBD9' }]}>
          <Text style={styles.text}>And simple</Text>
        </View>
      </Swiper>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  wrapper: {
    height: 200,
  },
  slide: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    width: width,
  },
  text: {
    color: '#fff',
    fontSize: 30,
    fontWeight: 'bold',
  },
});

export default BasicSwiper;

代码解析:

  1. 导入依赖:导入React、基础组件和Swiper组件
  2. 尺寸获取 :使用Dimensions获取屏幕宽度,确保Swiper宽度适配
  3. Swiper配置
    • style={styles.wrapper}:设置Swiper容器样式
    • showsButtons={false}:隐藏默认的左右导航按钮
  4. 子视图定义:定义3个不同背景色的子视图作为轮播内容
  5. 样式定义
    • slide样式确保每个子视图占满整个Swiper区域
    • 使用width: width确保子视图宽度与屏幕一致

OpenHarmony适配要点:

  • 在OpenHarmony上,必须显式设置子视图宽度为屏幕宽度,否则可能出现布局错乱
  • Dimensions.get('window')在OpenHarmony上的返回值可能与Android/iOS略有差异,建议使用onLayout事件动态获取尺寸
  • 避免使用百分比宽度,改用绝对值或Dimensions获取的值

自定义分页指示器

默认的分页指示器可能不符合设计需求,下面展示如何自定义分页指示器:

javascript 复制代码
import React from 'react';
import { View, Text, StyleSheet, Dimensions, Animated } from 'react-native';
import Swiper from 'react-native-swiper';

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

const CustomPaginationSwiper = () => {
  const renderPagination = (index, total, context) => {
    const dots = [];
    for (let i = 0; i < total; i++) {
      const isActive = index === i;
      dots.push(
        <Animated.View
          key={i}
          style={[
            styles.dot,
            isActive ? styles.activeDot : null,
            isActive ? { transform: [{ scale: context.anim ]} : null
          ]}
        />
      );
    }
    return <View style={styles.pagination}>{dots}</View>;
  };

  return (
    <View style={styles.container}>
      <Swiper
        style={styles.wrapper}
        showsButtons={false}
        dot={<View style={styles.dot} />}
        activeDot={<View style={[styles.dot, styles.activeDot]} />}
        paginationStyle={styles.paginationContainer}
        renderPagination={renderPagination}
      >
        <View style={[styles.slide, { backgroundColor: '#9DD6EB' }]} />
        <View style={[styles.slide, { backgroundColor: '#97CAE5' }]} />
        <View style={[styles.slide, { backgroundColor: '#92BBD9' }]} />
      </Swiper>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  wrapper: {
    height: 200,
  },
  slide: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    width,
  },
  paginationContainer: {
    bottom: 10,
  },
  pagination: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
  },
  dot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: 'rgba(0,0,0,0.2)',
    marginHorizontal: 3,
  },
  activeDot: {
    backgroundColor: '#007AFF',
    width: 12,
  },
});

export default CustomPaginationSwiper;

代码解析:

  1. 自定义分页函数renderPagination函数接收当前索引、总数和上下文参数
  2. 动态生成指示器:根据总页数生成相应数量的指示点
  3. 激活状态处理:当前页的指示点应用特殊样式
  4. 动画效果 :使用Animated实现激活点的缩放动画
  5. Swiper配置
    • dotactiveDot:定义默认指示点样式
    • paginationStyle:设置分页容器样式
    • renderPagination:使用自定义分页渲染函数

OpenHarmony适配要点:

  • 在OpenHarmony上,Animated API的性能有限,应避免复杂的动画效果
  • 如果发现指示器位置不正确,可能需要调整paginationStylebottom
  • 在OpenHarmony 3.2上,建议将指示器动画简化为颜色变化,避免使用transform
  • 使用Dimensions获取的尺寸在OpenHarmony上可能不够精确,可考虑使用onLayout事件获取更准确的尺寸

自动轮播功能实现

自动轮播是Swiper组件的常见需求,下面展示如何实现自动轮播:

javascript 复制代码
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Dimensions, Platform } from 'react-native';
import Swiper from 'react-native-swiper';

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

const AutoPlaySwiper = () => {
  const [autoplay, setAutoplay] = useState(true);
  
  // OpenHarmony平台检测
  const isOpenHarmony = Platform.OS === 'ohos';
  
  useEffect(() => {
    // OpenHarmony上需要调整自动轮播间隔
    const interval = isOpenHarmony ? 4000 : 3000;
    
    // 在OpenHarmony上使用requestAnimationFrame替代setInterval
    let frameId;
    let startTime;
    
    const startAnimation = (timestamp) => {
      if (!startTime) startTime = timestamp;
      const progress = timestamp - startTime;
      
      if (progress >= interval) {
        // 触发下一页
        // 实际实现中需要通过ref控制Swiper
        startTime = timestamp;
      }
      
      frameId = requestAnimationFrame(startAnimation);
    };
    
    if (autoplay) {
      frameId = requestAnimationFrame(startAnimation);
    }
    
    return () => {
      if (frameId) {
        cancelAnimationFrame(frameId);
      }
    };
  }, [autoplay, isOpenHarmony]);

  return (
    <View style={styles.container}>
      <Swiper
        style={styles.wrapper}
        autoplay={autoplay}
        autoplayTimeout={isOpenHarmony ? 4 : 3} // OpenHarmony上增加间隔
        loop
        onMomentumScrollEnd={(e, state, context) => {
          console.log('当前页:', state.index);
        }}
        onScrollBeginDrag={() => {
          // 用户开始滑动,暂停自动轮播
          setAutoplay(false);
        }}
        onScrollEndDrag={() => {
          // 用户结束滑动,恢复自动轮播
          setAutoplay(true);
        }}
      >
        <View style={[styles.slide, { backgroundColor: '#9DD6EB' }]}>
          <Text style={styles.text}>自动轮播 1</Text>
        </View>
        <View style={[styles.slide, { backgroundColor: '#97CAE5' }]}>
          <Text style={styles.text}>自动轮播 2</Text>
        </View>
        <View style={[styles.slide, { backgroundColor: '#92BBD9' }]}>
          <Text style={styles.text}>自动轮播 3</Text>
        </View>
      </Swiper>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  wrapper: {
    height: 200,
  },
  slide: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    width,
  },
  text: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
});

export default AutoPlaySwiper;

代码解析:

  1. 状态管理 :使用useState管理自动轮播状态
  2. 平台检测 :通过Platform.OS检测是否为OpenHarmony平台
  3. 定时器优化
    • 在OpenHarmony上使用requestAnimationFrame替代setInterval
    • 根据平台调整自动轮播间隔(OpenHarmony上需要更长间隔)
  4. 事件处理
    • onMomentumScrollEnd:滑动结束时的回调
    • onScrollBeginDrag:用户开始滑动时暂停自动轮播
    • onScrollEndDrag:用户结束滑动时恢复自动轮播

OpenHarmony适配要点:

  • OpenHarmony的定时器精度较低,使用requestAnimationFrame可以提高平滑度
  • 在OpenHarmony上,建议将autoplayTimeout设置为4秒或更长,避免因性能问题导致轮播不流畅
  • onScrollBeginDragonScrollEndDrag在OpenHarmony上可能触发不及时,需要添加额外的防抖处理
  • OpenHarmony 3.2上,autoplay功能可能不稳定,建议在低端设备上禁用自动轮播

Swiper 进阶用法

动态数据源与性能优化

在实际应用中,Swiper通常需要展示动态数据。下面展示如何高效处理动态数据源并优化性能:

javascript 复制代码
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, Dimensions, Image, ActivityIndicator, Platform } from 'react-native';
import Swiper from 'react-native-swiper';

const { width } = Dimensions.get('window');
const ITEM_WIDTH = width;
const ITEM_HEIGHT = 200;

const DynamicDataSwiper = () => {
  const [banners, setBanners] = useState([]);
  const [loading, setLoading] = useState(true);
  const swiperRef = useRef(null);
  const isMounted = useRef(true);
  
  // 模拟数据获取
  useEffect(() => {
    const fetchData = async () => {
      try {
        // 模拟API请求
        const mockData = [
          { id: 1, image: 'https://example.com/banner1.jpg', title: 'Banner 1' },
          { id: 2, image: 'https://example.com/banner2.jpg', title: 'Banner 2' },
          { id: 3, image: 'https://example.com/banner3.jpg', title: 'Banner 3' },
        ];
        
        if (isMounted.current) {
          setBanners(mockData);
          setLoading(false);
        }
      } catch (error) {
        console.error('获取数据失败:', error);
        if (isMounted.current) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted.current = false;
    };
  }, []);

  // OpenHarmony性能优化:减少重绘
  const shouldComponentUpdate = (nextProps, nextState) => {
    // 仅在数据变化时更新
    return banners.length !== nextState.banners.length || 
           loading !== nextState.loading;
  };

  // OpenHarmony特定优化:预加载图片
  const preloadImages = () => {
    if (Platform.OS === 'ohos' && banners.length > 0) {
      // OpenHarmony上需要提前加载图片
      banners.forEach(banner => {
        Image.prefetch(banner.image).catch(err => 
          console.warn('图片预加载失败:', banner.image, err)
        );
      });
    }
  };

  useEffect(() => {
    if (!loading && banners.length > 0) {
      preloadImages();
    }
  }, [loading, banners]);

  if (loading) {
    return (
      <View style={[styles.container, styles.loadingContainer]}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (banners.length === 0) {
    return (
      <View style={[styles.container, styles.emptyContainer]}>
        <Text style={styles.emptyText}>暂无轮播内容</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Swiper
        ref={swiperRef}
        style={styles.wrapper}
        loop
        autoplay
        autoplayTimeout={Platform.OS === 'ohos' ? 4.5 : 3}
        removeClippedSubviews={Platform.OS === 'ohos'} // OpenHarmony上启用裁剪
        showsPagination={true}
        dotStyle={styles.dot}
        activeDotStyle={[styles.dot, styles.activeDot]}
        paginationStyle={styles.paginationStyle}
        onIndexChanged={(index) => {
          console.log('当前索引:', index);
        }}
      >
        {banners.map((banner, index) => (
          <View key={banner.id} style={styles.slide}>
            <Image 
              source={{ uri: banner.image }} 
              style={styles.image}
              resizeMode="cover"
              onLoad={() => console.log(`图片 ${index + 1} 加载完成`)}
              onError={(error) => console.error(`图片 ${index + 1} 加载失败`, error)}
            />
            <Text style={styles.title}>{banner.title}</Text>
          </View>
        ))}
      </Swiper>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
  },
  loadingContainer: {
    height: ITEM_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
  },
  emptyContainer: {
    height: ITEM_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
  },
  emptyText: {
    color: '#666',
    fontSize: 16,
  },
  wrapper: {
    height: ITEM_HEIGHT,
  },
  slide: {
    width: ITEM_WIDTH,
    height: ITEM_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5F5F5',
  },
  image: {
    width: '100%',
    height: '100%',
    position: 'absolute',
  },
  title: {
    position: 'absolute',
    bottom: 20,
    left: 0,
    right: 0,
    backgroundColor: 'rgba(0,0,0,0.3)',
    color: 'white',
    fontSize: 16,
    padding: 8,
    textAlign: 'center',
  },
  dot: {
    backgroundColor: 'rgba(0,0,0,0.2)',
    width: 6,
    height: 6,
    borderRadius: 3,
    marginLeft: 3,
    marginRight: 3,
    marginTop: 3,
    marginBottom: 3,
  },
  activeDot: {
    backgroundColor: '#007AFF',
  },
  paginationStyle: {
    bottom: 10,
  },
});

export default React.memo(DynamicDataSwiper, shouldComponentUpdate);

代码解析:

  1. 数据管理
    • 使用useState管理轮播数据和加载状态
    • 使用useRef保存Swiper引用和组件挂载状态
  2. 数据获取
    • 模拟API请求获取轮播数据
    • 使用isMounted避免在组件卸载后更新状态
  3. 性能优化
    • removeClippedSubviews:在OpenHarmony上启用视图裁剪,减少渲染负担
    • React.memo:使用高阶组件避免不必要的重渲染
    • shouldComponentUpdate:自定义更新逻辑,仅在数据变化时更新
  4. 图片处理
    • OpenHarmony上使用Image.prefetch预加载图片
    • 添加图片加载完成和失败的回调

OpenHarmony适配要点:

  • OpenHarmony上必须设置removeClippedSubviews={true},否则低端设备可能出现严重卡顿
  • 图片资源在OpenHarmony上加载较慢,预加载是必要的性能优化手段
  • 使用React.memo可以显著减少OpenHarmony上的重绘次数
  • OpenHarmony 3.2上,autoplayTimeout应设置为4.5秒以上,避免因性能问题导致轮播不流畅
  • OpenHarmony对Image组件的支持有限,建议使用绝对URL而非本地资源

手势冲突解决方案

在复杂页面中,Swiper可能与其他手势(如下拉刷新、侧滑菜单)产生冲突。下面展示如何解决手势冲突:

javascript 复制代码
import React, { useRef, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, PanResponder, Platform } from 'react-native';
import Swiper from 'react-native-swiper';

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

const GestureConflictSwiper = () => {
  const swiperRef = useRef(null);
  const [isSwiping, setIsSwiping] = useState(false);
  const [parentScrollEnabled, setParentScrollEnabled] = useState(true);
  
  // OpenHarmony特定手势处理
  const isOH = Platform.OS === 'ohos';
  const gestureStartX = useRef(0);
  const gestureStartTime = useRef(0);
  
  // 创建PanResponder处理手势冲突
  const swiperPanResponder = useRef(
    PanResponder.create({
      // 决定是否成为响应者
      onStartShouldSetPanResponder: (evt, gestureState) => {
        gestureStartX.current = gestureState.x0;
        gestureStartTime.current = Date.now();
        return true;
      },
      // 决定是否成为响应者(触摸移动时)
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        const dx = Math.abs(gestureState.dx);
        const dy = Math.abs(gestureState.dy);
        
        // OpenHarmony特定处理:增加水平滑动阈值
        const horizontalThreshold = isOH ? 10 : 5;
        
        // 如果水平移动大于垂直移动且超过阈值,则Swiper接管手势
        if (dx > horizontalThreshold && dx > dy) {
          setIsSwiping(true);
          setParentScrollEnabled(false);
          return true;
        }
        
        setIsSwiping(false);
        setParentScrollEnabled(true);
        return false;
      },
      // 手势释放
      onPanResponderRelease: (evt, gestureState) => {
        setIsSwiping(false);
        setParentScrollEnabled(true);
        
        // OpenHarmony特定处理:检查快速滑动
        if (isOH) {
          const duration = Date.now() - gestureStartTime.current;
          const dx = Math.abs(gestureState.dx);
          
          // 快速滑动且距离足够,强制切换页面
          if (duration < 300 && dx > 30 && swiperRef.current) {
            const direction = gestureState.dx > 0 ? -1 : 1;
            swiperRef.current.scrollBy(direction);
          }
        }
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <View 
        style={styles.swiperContainer}
        {...swiperPanResponder.panHandlers}
      >
        <Swiper
          ref={swiperRef}
          style={styles.wrapper}
          loop
          autoplay
          autoplayTimeout={isOH ? 4 : 3}
          bounces={false}
          horizontal
          showsPagination
          dotStyle={styles.dot}
          activeDotStyle={[styles.dot, styles.activeDot]}
          onIndexChanged={(index) => {
            console.log('Swiper索引:', index);
          }}
        >
          <View style={[styles.slide, { backgroundColor: '#9DD6EB' }]}>
            <Text style={styles.text}>手势冲突解决 1</Text>
          </View>
          <View style={[styles.slide, { backgroundColor: '#97CAE5' }]}>
            <Text style={styles.text}>手势冲突解决 2</Text>
          </View>
          <View style={[styles.slide, { backgroundColor: '#92BBD9' }]}>
            <Text style={styles.text}>手势冲突解决 3</Text>
          </View>
        </Swiper>
      </View>
      
      <View style={styles.infoContainer}>
        <Text style={styles.infoText}>
          {isSwiping 
            ? '当前Swiper正在处理手势' 
            : '手势由父容器处理'}
        </Text>
        <Text style={styles.infoText}>
          父容器滚动状态: {parentScrollEnabled ? '启用' : '禁用'}
        </Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  swiperContainer: {
    height: 200,
    overflow: 'hidden',
  },
  wrapper: {
    backgroundColor: '#F5F5F5',
  },
  slide: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    width,
  },
  text: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  dot: {
    backgroundColor: 'rgba(0,0,0,0.2)',
    width: 6,
    height: 6,
    borderRadius: 3,
    marginLeft: 3,
    marginRight: 3,
  },
  activeDot: {
    backgroundColor: '#007AFF',
  },
  infoContainer: {
    padding: 15,
    borderTopWidth: 1,
    borderTopColor: '#eee',
  },
  infoText: {
    fontSize: 16,
    lineHeight: 24,
  },
});

export default GestureConflictSwiper;

代码解析:

  1. 手势状态管理
    • isSwiping:跟踪当前是否在Swiper滑动中
    • parentScrollEnabled:控制父容器是否可以滚动
  2. PanResponder实现
    • onStartShouldSetPanResponder:记录手势起始位置和时间
    • onMoveShouldSetPanResponder:根据滑动方向决定谁处理手势
    • onPanResponderRelease:手势释放时重置状态
  3. OpenHarmony特定处理
    • 增加水平滑动阈值(10px vs 标准5px)
    • 添加快速滑动检测,确保在OpenHarmony上滑动切换可靠

OpenHarmony适配要点:

  • OpenHarmony上需要增加水平滑动阈值,因为其触摸事件精度较低
  • OpenHarmony 3.2上,快速滑动时可能无法触发页面切换,需要手动调用scrollBy
  • bounces={false}在OpenHarmony上是必要的,否则会出现异常回弹效果
  • OpenHarmony对PanResponder的支持有限,应避免复杂的逻辑判断
  • 在OpenHarmony上,手势冲突问题更为明显,需要更精细的手势判断逻辑

高级动画效果实现

虽然OpenHarmony对复杂动画支持有限,但我们仍可以实现一些基础的动画效果:

javascript 复制代码
import React, { useRef } from 'react';
import { View, Text, StyleSheet, Dimensions, Animated, Easing, Platform } from 'react-native';
import Swiper from 'react-native-swiper';

const { width } = Dimensions.get('window');
const ITEM_WIDTH = width;
const ITEM_HEIGHT = 250;

const AdvancedAnimationSwiper = () => {
  const fadeAnim = useRef(new Animated.Value(0)).current;
  const scaleAnim = useRef(new Animated.Value(0.9)).current;
  const isOH = Platform.OS === 'ohos';
  
  // OpenHarmony上简化动画配置
  const animationConfig = isOH 
    ? { duration: 300, easing: Easing.ease } 
    : { duration: 500, easing: Easing.out(Easing.quad) };
  
  const animateIn = () => {
    Animated.parallel([
      Animated.timing(fadeAnim, {
        toValue: 1,
        ...animationConfig,
        useNativeDriver: true,
      }),
      Animated.spring(scaleAnim, {
        toValue: 1,
        friction: isOH ? 7 : 5, // OpenHarmony上增加摩擦系数
        useNativeDriver: true,
      }),
    ]).start();
  };
  
  const animateOut = () => {
    Animated.parallel([
      Animated.timing(fadeAnim, {
        toValue: 0,
        ...animationConfig,
        useNativeDriver: true,
      }),
      Animated.spring(scaleAnim, {
        toValue: 0.9,
        friction: isOH ? 7 : 5,
        useNativeDriver: true,
      }),
    ]).start();
  };
  
  const renderCard = (title, color, index) => {
    return (
      <Animated.View
        key={index}
        style={[
          styles.card,
          {
            backgroundColor: color,
            opacity: fadeAnim,
            transform: [{ scale: scaleAnim }],
          }
        ]}
      >
        <Text style={styles.cardTitle}>{title}</Text>
        <Text style={styles.cardDescription}>高级动画效果</Text>
      </Animated.View>
    );
  };

  return (
    <View style={styles.container}>
      <Swiper
        style={styles.wrapper}
        loop
        autoplay
        autoplayTimeout={isOH ? 4.5 : 3}
        onIndexChanged={(index) => {
          // 重置动画
          fadeAnim.setValue(0);
          scaleAnim.setValue(0.9);
          // 启动进入动画
          animateIn();
        }}
        onScrollBeginDrag={() => {
          // 启动退出动画
          animateOut();
        }}
        bounces={false}
        removeClippedSubviews={isOH}
      >
        {renderCard('卡片 1', '#9DD6EB', 0)}
        {renderCard('卡片 2', '#97CAE5', 1)}
        {renderCard('卡片 3', '#92BBD9', 2)}
      </Swiper>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  wrapper: {
    height: ITEM_HEIGHT,
  },
  card: {
    width: ITEM_WIDTH - 40,
    height: ITEM_HEIGHT - 60,
    margin: 20,
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#fff',
    marginBottom: 10,
  },
  cardDescription: {
    fontSize: 18,
    color: 'rgba(255,255,255,0.8)',
  },
});

export default AdvancedAnimationSwiper;

代码解析:

  1. 动画变量
    • fadeAnim:控制卡片透明度
    • scaleAnim:控制卡片缩放比例
  2. 平台特定动画配置
    • OpenHarmony上使用更简单的动画曲线和更短的持续时间
    • 调整摩擦系数以适应OpenHarmony的物理引擎
  3. 动画函数
    • animateIn:页面进入时的动画
    • animateOut:页面离开时的动画
  4. 事件绑定
    • onIndexChanged:页面切换完成时触发进入动画
    • onScrollBeginDrag:开始滑动时触发退出动画

OpenHarmony适配要点:

  • OpenHarmony上必须设置useNativeDriver: true,否则动画会非常卡顿
  • OpenHarmony对Easing的支持有限,应避免使用复杂的缓动函数
  • OpenHarmony 3.2上,动画持续时间应控制在300ms以内,避免卡顿
  • OpenHarmony对阴影效果支持不佳,应简化或移除shadow*样式
  • OpenHarmony上应避免同时运行多个复杂动画,会导致严重性能问题

实战案例:电商应用首页轮播

在电商应用中,首页轮播是核心功能之一。下面展示一个完整的电商轮播实现案例:

javascript 复制代码
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, Dimensions, Image, TouchableOpacity, Platform } from 'react-native';
import Swiper from 'react-native-swiper';
import Icon from 'react-native-vector-icons/Ionicons';

const { width } = Dimensions.get('window');
const BANNER_HEIGHT = 200;
const isOH = Platform.OS === 'ohos';

const ECommerceBanner = ({ banners, onBannerPress }) => {
  const swiperRef = useRef(null);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [loadingStates, setLoadingStates] = useState({});
  
  // OpenHarmony特定优化:预加载图片
  useEffect(() => {
    if (isOH && banners && banners.length > 0) {
      banners.forEach(banner => {
        if (!loadingStates[banner.id]) {
          setLoadingStates(prev => ({ ...prev, [banner.id]: 'loading' }));
          Image.prefetch(banner.image)
            .then(() => setLoadingStates(prev => ({ ...prev, [banner.id]: 'success' })))
            .catch(() => setLoadingStates(prev => ({ ...prev, [banner.id]: 'error' })));
        }
      });
    }
  }, [banners]);
  
  // 处理轮播索引变化
  const handleIndexChanged = (index) => {
    setCurrentIndex(index);
  };
  
  // 处理轮播点击
  const handleBannerPress = (banner, index) => {
    if (onBannerPress) {
      onBannerPress(banner, index);
    }
  };
  
  // 渲染单个轮播项
  const renderBannerItem = (banner, index) => {
    const isLoading = loadingStates[banner.id] === 'loading';
    const isError = loadingStates[banner.id] === 'error';
    
    return (
      <TouchableOpacity 
        key={banner.id} 
        activeOpacity={0.9}
        style={styles.bannerItem}
        onPress={() => handleBannerPress(banner, index)}
      >
        {isError ? (
          <View style={styles.errorContainer}>
            <Icon name="alert-circle" size={24} color="#FF3B30" />
            <Text style={styles.errorText}>图片加载失败</Text>
          </View>
        ) : (
          <>
            {isLoading && (
              <View style={styles.loadingContainer}>
                <ActivityIndicator size="small" color="#007AFF" />
              </View>
            )}
            <Image
              source={{ uri: banner.image }}
              style={styles.bannerImage}
              resizeMode="cover"
            />
            {banner.title && (
              <View style={styles.titleOverlay}>
                <Text style={styles.titleText} numberOfLines={1}>
                  {banner.title}
                </Text>
              </View>
            )}
          </>
        )}
      </TouchableOpacity>
    );
  };
  
  // 渲染分页指示器
  const renderPagination = (index, total) => {
    const dots = [];
    for (let i = 0; i < total; i++) {
      dots.push(
        <View
          key={i}
          style={[
            styles.paginationDot,
            i === index ? styles.activePaginationDot : null
          ]}
        />
      );
    }
    return (
      <View style={styles.paginationContainer}>
        {dots}
        <View style={styles.pageInfo}>
          <Text style={styles.pageInfoText}>
            {index + 1}/{total}
          </Text>
        </View>
      </View>
    );
  };

  if (!banners || banners.length === 0) {
    return (
      <View style={[styles.container, styles.emptyContainer]}>
        <Text style={styles.emptyText}>暂无轮播广告</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Swiper
        ref={swiperRef}
        style={styles.wrapper}
        loop
        autoplay
        autoplayTimeout={isOH ? 4.5 : 3}
        removeClippedSubviews={isOH}
        onIndexChanged={handleIndexChanged}
        dot={<View style={styles.paginationDot} />}
        activeDot={<View style={[styles.paginationDot, styles.activePaginationDot]} />}
        paginationStyle={styles.paginationStyle}
        renderPagination={renderPagination}
        bounces={false}
      >
        {banners.map((banner, index) => renderBannerItem(banner, index))}
      </Swiper>
      
      {/* 底部操作按钮 */}
      <View style={styles.bottomControls}>
        <TouchableOpacity style={styles.controlButton}>
          <Icon name="arrow-back" size={20} color="#007AFF" />
        </TouchableOpacity>
        <View style={styles.indicator}>
          <Text style={styles.indicatorText}>
            {currentIndex + 1}/{banners.length}
          </Text>
        </View>
        <TouchableOpacity 
          style={styles.controlButton}
          onPress={() => swiperRef.current?.scrollBy(1)}
        >
          <Icon name="arrow-forward" size={20} color="#007AFF" />
        </TouchableOpacity>
      </View>
    </View>
  );
};

// 电商应用首页使用示例
const HomeScreen = () => {
  const [banners, setBanners] = useState([]);
  
  useEffect(() => {
    // 模拟获取轮播数据
    setTimeout(() => {
      setBanners([
        { id: 1, image: 'https://example.com/banner1.jpg', title: '夏季大促,低至5折', action: 'PROMO_1' },
        { id: 2, image: 'https://example.com/banner2.jpg', title: '新品首发,立即抢购', action: 'NEW_ARRIVAL' },
        { id: 3, image: 'https://example.com/banner3.jpg', title: '会员专享,限时优惠', action: 'MEMBER_DEAL' },
      ]);
    }, 500);
  }, []);
  
  const handleBannerPress = (banner, index) => {
    console.log(`点击了轮播图 ${index + 1}`, banner);
    // 这里处理点击事件,如跳转到对应页面
  };

  return (
    <View style={styles.homeContainer}>
      <Text style={styles.header}>电商应用首页</Text>
      <ECommerceBanner banners={banners} onBannerPress={handleBannerPress} />
      {/* 其他首页内容 */}
    </View>
  );
};

const styles = StyleSheet.create({
  homeContainer: {
    flex: 1,
    backgroundColor: '#F8F9FA',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    padding: 15,
    color: '#333',
  },
  container: {
    position: 'relative',
  },
  wrapper: {
    height: BANNER_HEIGHT,
  },
  bannerItem: {
    width: width,
    height: BANNER_HEIGHT,
    backgroundColor: '#F5F5F5',
  },
  bannerImage: {
    width: '100%',
    height: '100%',
  },
  titleOverlay: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    padding: 8,
  },
  titleText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
  paginationContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  paginationDot: {
    width: 6,
    height: 6,
    borderRadius: 3,
    backgroundColor: 'rgba(255,255,255,0.5)',
    marginHorizontal: 3,
  },
  activePaginationDot: {
    backgroundColor: 'white',
    width: 8,
  },
  pageInfo: {
    marginLeft: 8,
    backgroundColor: 'rgba(0,0,0,0.3)',
    paddingHorizontal: 6,
    paddingVertical: 2,
    borderRadius: 10,
  },
  pageInfoText: {
    color: 'white',
    fontSize: 12,
  },
  paginationStyle: {
    bottom: 10,
  },
  bottomControls: {
    position: 'absolute',
    bottom: 10,
    left: 0,
    right: 0,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,0.2)',
    borderRadius: 20,
    height: 30,
    marginHorizontal: 20,
  },
  controlButton: {
    padding: 5,
  },
  indicator: {
    marginHorizontal: 10,
    backgroundColor: 'rgba(255,255,255,0.3)',
    paddingHorizontal: 8,
    paddingVertical: 2,
    borderRadius: 10,
  },
  indicatorText: {
    color: 'white',
    fontSize: 14,
  },
  emptyContainer: {
    height: BANNER_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F8F9FA',
  },
  emptyText: {
    color: '#666',
    fontSize: 16,
  },
  loadingContainer: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(255,255,255,0.8)',
  },
  errorContainer: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F8F9FA',
  },
  errorText: {
    marginTop: 5,
    color: '#FF3B30',
  },
});

export default HomeScreen;

代码解析:

  1. 组件拆分
    • ECommerceBanner:独立的轮播组件,可复用
    • HomeScreen:电商首页使用示例
  2. 功能特点
    • 图片预加载(针对OpenHarmony优化)
    • 图片加载状态管理(加载中、成功、失败)
    • 自定义分页指示器(带页码显示)
    • 底部控制按钮(上一页/下一页)
    • 点击事件处理
  3. OpenHarmony优化
    • 增加轮播间隔至4.5秒
    • 启用removeClippedSubviews
    • 使用更简单的动画和样式
    • 图片预加载机制

OpenHarmony适配要点:

  • OpenHarmony上必须使用removeClippedSubviews,否则滚动性能会严重下降
  • OpenHarmony 3.2上,图片加载失败处理尤为重要,因为网络请求成功率较低
  • OpenHarmony对阴影和复杂渐变支持不佳,应简化UI设计
  • OpenHarmony上建议移除不必要的动画效果,保持界面简洁流畅
  • OpenHarmony设备性能差异大,需要针对低端设备做额外优化

电商轮播组件架构图

OpenHarmony优化
状态同步
预加载完成
手势冲突处理
导航跳转
电商轮播组件
数据层
UI层
交互层
数据获取
状态管理
图片预加载
轮播容器
轮播项
分页指示器
底部控制
滑动事件
点击事件
自动轮播
页面路由

电商轮播组件架构图,特别标注了针对OpenHarmony平台的优化点。图片预加载和手势冲突处理是OpenHarmony适配的关键环节。

常见问题与解决方案

Swiper 组件 API 对比表

功能/属性 OpenHarmony 适配建议 Android/iOS 标准用法 OpenHarmony 问题现象
autoplay 设置为true,但autoplayTimeout增加到4-5秒 通常3秒即可 轮播间隔不稳定,可能跳过页面
removeClippedSubviews 必须设置为true 可选,通常默认true 低端设备严重卡顿,内存占用高
bounces 必须设置为false 可选,默认true 异常回弹效果,影响用户体验
onScrollBeginDrag 添加防抖处理(300ms) 直接使用 触发不及时,可能导致事件丢失
动画效果 简化动画,避免复杂效果 可使用丰富动画 卡顿严重,可能完全不显示
图片加载 必须预加载,添加错误处理 可直接使用 加载失败率高,无回退机制
手势阈值 水平阈值提高到10px 通常5px即可 手势识别不准确,滑动不流畅

OpenHarmony 与其他平台差异对比

特性 OpenHarmony Android iOS 适配建议
触摸事件精度 ⚠️ 较低,快速滑动易丢失 ✅ 高 ✅ 高 增加手势阈值,添加快速滑动补偿
滚动性能 ⚠️ 中低端设备较差 ✅ 良好 ✅ 优秀 简化UI,减少重绘,使用removeClippedSubviews
动画支持 ⚠️ 有限,复杂动画卡顿 ✅ 良好 ✅ 优秀 简化动画,避免同时运行多个动画
定时器精度 ⚠️ 较低 ✅ 正常 ✅ 正常 增加自动轮播间隔,使用requestAnimationFrame
图片加载 ⚠️ 较慢,失败率高 ✅ 正常 ✅ 正常 必须预加载,添加错误处理机制
内存管理 ⚠️ 较严格,易触发OOM ✅ 合理 ✅ 合理 及时释放资源,避免大图直接加载

常见问题解决方案

问题现象 可能原因 解决方案 验证方式
滑动不流畅,卡顿 1. 未启用removeClippedSubviews 2. UI过于复杂 3. 动画效果过多 1. 设置removeClippedSubviews={true} 2. 简化子视图UI 3. 移除或简化动画 在OpenHarmony 3.2设备上测试滑动流畅度
自动轮播跳过页面 1. autoplayTimeout太短 2. 定时器精度问题 1. 增加autoplayTimeout至4.5秒 2. 使用requestAnimationFrame替代setInterval 观察连续轮播10次是否跳页
触摸事件丢失 1. 手势阈值太低 2. 快速滑动处理不足 1. 增加水平滑动阈值至10px 2. 添加快速滑动检测和补偿 快速滑动测试,检查是否能正常切换页面
图片加载失败 1. 未预加载 2. 网络请求超时 1. 使用Image.prefetch预加载 2. 添加加载失败UI和重试机制 模拟弱网环境测试图片加载
手势与其他组件冲突 1. 手势识别不准确 2. 未正确处理手势竞争 1. 使用PanResponder精细控制 2. 根据滑动方向决定手势归属 在包含下拉刷新的页面中测试
分页指示器位置错误 1. 尺寸计算不准确 2. 样式兼容性问题 1. 使用onLayout动态获取尺寸 2. 避免使用百分比布局 在不同尺寸OpenHarmony设备上测试

总结与展望

关键要点总结

通过本文的详细解析,我们深入探讨了React Native中Swiper组件在OpenHarmony平台上的适配与应用。关键要点包括:

  1. 平台差异认知:OpenHarmony与标准React Native环境存在显著差异,特别是在触摸事件处理、动画支持和性能表现方面。

  2. 核心适配策略

    • 必须启用removeClippedSubviews以优化滚动性能
    • 设置bounces={false}避免异常回弹效果
    • 增加autoplayTimeout至4-5秒确保轮播稳定
    • 简化UI和动画,适应OpenHarmony的渲染能力
  3. 性能优化技巧

    • 使用Image.prefetch预加载图片资源
    • 通过PanResponder精细控制手势处理
    • 针对OpenHarmony使用更简单的动画效果
    • 采用React.memo减少不必要的重渲染
  4. 问题排查方法

    • 通过日志监控事件触发情况
    • 使用性能分析工具定位瓶颈
    • 针对不同设备进行差异化适配

未来展望

随着OpenHarmony生态的不断发展,React Native在OpenHarmony上的支持也将持续改进:

  1. 官方支持增强:预计未来OpenHarmony版本将提供更完善的React Native支持,减少适配工作量。

  2. 性能优化:OpenHarmony的图形渲染引擎有望进一步优化,提升复杂动画和滚动性能。

  3. 社区生态:随着更多开发者加入,将出现更多针对OpenHarmony优化的第三方库。

  4. 开发工具:更强大的调试工具将帮助开发者更高效地解决跨平台兼容性问题。

作为开发者,我们应该:

  • 持续关注OpenHarmony和React Native的最新发展
  • 积极参与社区,分享适配经验和解决方案
  • 为开源项目贡献代码,推动生态发展
  • 在项目中采用渐进式适配策略,平衡开发效率和用户体验

给开发者的建议

  1. 先做平台检测:在关键代码处添加平台检测,针对性应用优化。

  2. 性能优先:在OpenHarmony上,应优先考虑性能而非炫酷效果。

  3. 渐进式增强:先实现基础功能,再根据设备能力添加高级特性。

  4. 充分测试:在多种OpenHarmony设备上进行测试,特别是低端设备。

  5. 保持简单:避免过度设计,简洁的UI在OpenHarmony上表现更好。

完整项目Demo地址

完整项目Demo地址:https://atomgit.com/pickstar/AtomGitDemos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

💬 互动时间:你在OpenHarmony上使用Swiper组件时遇到过哪些问题?有什么独特的解决方案?欢迎在评论区分享你的经验,让我们一起推动React Native在OpenHarmony上的更好应用!如果你觉得本文有帮助,请点赞并分享给更多需要的开发者朋友。让我们共同建设更强大的开源鸿蒙生态!

相关推荐
鸣弦artha2 小时前
Flutter框架跨平台鸿蒙开发——Build流程深度解析
开发语言·javascript·flutter
摘星编程4 小时前
React Native for OpenHarmony 实战:DisplayInfo 显示信息详解
android·react native·react.js
LongJ_Sir4 小时前
Cesium--可拖拽气泡弹窗(Vue3版)
javascript
跟着珅聪学java5 小时前
JavaScript 中定义全局变量的教程
javascript
哈哈你是真的厉害5 小时前
React Native 鸿蒙跨平台开发:FlatList 基础列表代码指南
react native·react.js·harmonyos
午安~婉5 小时前
整理知识点
前端·javascript·vue
向前V5 小时前
Flutter for OpenHarmony数独游戏App实战:底部导航栏
javascript·flutter·游戏
人道领域5 小时前
JavaWeb从入门到进阶(javaScript)
开发语言·javascript·ecmascript
军军君015 小时前
Three.js基础功能学习十二:常量与核心
前端·javascript·学习·3d·threejs·three·三维