多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

React Native鸿蒙开发入门:电风扇旋转Demo实战解析

React Native鸿蒙开发入门:电风扇旋转Demo实战解析 最近在折腾React Native开发鸿蒙应用拿一个会转的电风扇练手。这个项目算很基础的入门Demo但覆盖面一点都不窄界面布局、组件拆分、状态管理、循环动画、设备联调全都在一个几十行的代码里串起来了。很多人第一次接触鸿蒙跨平台开发习惯性地去看官方文档然后被一堆环境变量、签名配置劝退。我觉得不如先做一个这种带反馈的小玩意儿把流程跑通再回头看文档就顺了。这篇文章就按我的实际动手顺序来写包括建工程、写组件、调动画、上真机以及中间遇到的几个坑。1. 项目概述与开发环境准备1.1 为什么选电风扇当入门项目电风扇这个例子很适合做跨平台入门因为它只有一个核心动作旋转。做React Native项目也一样难点往往不在地图、相机这些重型能力而在“UI组件如何组织、状态如何流转、动画如何驱动”。电风扇能把这三件事全部涵盖又不会引入地图SDK、蓝牙协议这些额外复杂度对新手非常友好。我在团队里接触React Native比较多日常写的多是列表页和表单页真正涉及自定义动画的确实不多。鸿蒙适配层出现之后我第一反应不是去写“Hello World”而是想验证两件事第一现有RN代码能不能少改就跑到鸿蒙设备上第二动画这类依赖原生渲染能力的功能在鸿蒙上到底能不能流畅跑。电风扇Demo就是为这两个问题准备的。从结果看适配层做得比我想象中完整。RN的View、Text、TouchableOpacity这些基础组件都有对应映射StyleSheet的排版体系也能直接用。这意味着你过去积累的React Native组件代码迁移成本主要在构建配置而不是重写UI。对刚接触鸿蒙开发的同学来说先跑通一个小Demo能最快建立信心。1.2 开发环境准备先确认电脑上有Node.js。我用的是18的LTS版本如果你还在用12或14大概率会在装依赖、启动构建脚本的时候遇到各种奇怪的报错。建议直接用nvm管一个长期支持版本省得到处踩雷。装包我用npm如果你习惯yarn差别不大但注意别混用锁文件不然CI和本机行为不一致。创建工程直接用React Native官方CLInpx react-native-community/cli init ElectricFanDemo cd ElectricFanDemoCLI会帮你生成一个标准的RN项目里面有android、ios目录。鸿蒙适配层做的事情就是在现有RN项目上新增一个鸿蒙工程目录并提供一套运行时桥接。不同团队的适配层包名和初始化命令略有差异以你使用的开源库文档为准。大致流程是# 这里的“鸿蒙适配包”需要替换成实际使用的包名 npm install rn-harmony-adapter # 执行适配层提供的初始化命令 npx rn-harmony init初始化完成之后项目里会多出一个类似harmony的目录里面是鸿蒙侧的工程结构。React Native的JavaScript代码不用动原生侧由适配层负责把RN节点渲染成鸿蒙的ArkUI组件。这一步是整个跨平台方案的灵魂JS写业务原生管渲染桥接层管通信。我习惯把代码分层放不全都塞在App.js里。目录结构大概这样src/ components/ Fan.js screens/ HomeScreen.js utils/ speed.js App.js虽然这个Demo规模不大但为后续扩展留一点空间是值得的。后面加手势调风速、摇头动画直接在对应组件里改就行不用动主入口。运行到鸿蒙设备前要先在设备上开启开发者模式把“USB调试”打开。模拟器也可以只是性能会比真机差一些动画卡顿时分不清是适配层的问题还是环境的问题。我第一次就直接上了真机后来的经验是先跑模拟器验证构建链路再上真机看动画表现两个阶段各有各的价值。2. 模拟电风扇的界面设计与组件拆解2.1 页面结构与组件划分电风扇的界面拆开看就是三个部分标题区、风扇展示区、控制区。展示区是一个圆形外框里面放着旋转的叶片圆心位置有一个中心轴盖控制区是开关按钮加上几档风速按钮。虽然看着简单但把组件边界画清楚后面对接状态会非常省事。一个容易犯的错误是把风扇外框和叶片放在同一个视图里然后整个视图做旋转。结果就是外框也在转像陀螺不像风扇。正确做法是把“外框”和“叶片组”拆开外框固定不动只有叶片组发生旋转。这也是组件拆分的价值谁需要变谁保持不变代码结构一清二楚。我的组件树结构是这样的SafeAreaView根容器Header标题文字FanArea定位容器负责摆放外框和叶片Fan自定义组件内部包含外框、叶片组、中心轴Controls操作区电源开关风速档位按钮控制区的按钮我会在App层持有状态isOn和speed然后通过props传给Fan组件。这样做的好处是风扇组件可以成为一个纯展示组件给他“是否开启”“几档速度”它自己决定动画怎么跑外界不需要知道Animated的内部细节。这是React组件划分里很常见也很实用的思路。2.2 布局与基础样式风扇外框采用一个正方形加圆角背景色偏浅灰边框深一点模拟金属圈。叶片组使用绝对定位铺满整个外框内部所有叶片都放在同一个旋转节点里。以下是核心样式const SIZE 220; const styles StyleSheet.create({ fanWrap: { width: SIZE, height: SIZE, borderRadius: SIZE / 2, borderWidth: 6, borderColor: #d8dee9, backgroundColor: #f7f9fc, alignItems: center, justifyContent: center, overflow: hidden, }, bladeGroup: { position: absolute, top: 0, left: 0, width: SIZE, height: SIZE, alignItems: center, justifyContent: center, }, bladeArm: { position: absolute, width: 20, height: 20, left: SIZE / 2 - 10, top: SIZE / 2 - 10, alignItems: center, justifyContent: center, }, blade: { width: 18, height: 70, borderRadius: 9, backgroundColor: #576b7d, opacity: 0.85, transform: [{ translateY: -45 }], }, });关于叶片位置的细节三片叶片不能简单地都塞在同一个位置然后旋转那样它们会叠在一起看起来只有一片。我的做法是先生成三个bladeArm每个都是一个20x20的正方形定位在风扇中心然后让其中的叶片通过translateY向上偏移45。再把每个bladeArm整体旋转0度、120度、240度三片叶片就均匀分布在圆心了。这个技巧很多人第一次写会绕进去记住一点旋转的是定位臂不是叶片本身。中心轴盖可以直接叠在叶片组上面用绝对定位放一个小圆盖住叶片的交点。颜色用红棕色加上白边视觉上更像实体风扇的轴心。这里的层级顺序很关键叶片组在下轴盖在上否则轴盖会被旋转的叶片盖住。2.3 生成三片叶片叶片数量我用常量BLADE_COUNT 3循环生成。这样以后想改成四片、五片只需要改一个数字。下面这段是Fan组件里生成叶片的逻辑const BLADE_COUNT 3; function renderBlades() { const blades []; for (let i 0; i BLADE_COUNT; i) { const angle (i * 360) / BLADE_COUNT; blades.push( View key{i} style{[styles.bladeArm, { transform: [{ rotate: ${angle}deg }] }]} View style{styles.blade} / /View ); } return blades; }angle用的是度React Native的rotate默认只认deg不要漏掉单位。很多人写出rotate: 120运行起来动画完全不转就是单位问题。叶片本身是圆角矩形透明度稍微调低一点让背景色透出来转起来的时候会有一点残影效果很像真实风扇的叶片。叶片之间的角度不必涉及三角函数因为叶片是围绕同一个旋转中心分布的均分角度就行。这个思路也可以沿用到其他仪表盘、雷达图、环形菜单的组件上算是入门阶段很实用的一个模板。3. 旋转动画与控制逻辑实现3.1 动画方案选型React Native里的动画方案不少常见的有Animated和Reanimated。这个Demo我选择Animated原因很简单它内置在RN核心库里不需要额外安装原生依赖对鸿蒙适配层来说接入更稳。Reanimated功能更强但它的原生部分要和鸿蒙适配层做集成对入门项目有点重。Animated做循环旋转动画的核心API是Animated.loop。我们可以让一个Animated.Value从0变到1再用interpolate把它映射成0deg到360deg最后放到transform里const rotation useRef(new Animated.Value(0)).current; const rotate rotation.interpolate({ inputRange: [0, 1], outputRange: [0deg, 360deg], });值只是0到1具体转多少度由interpolate决定。这样我们不用关心角度累计因为loop每转完一圈会重置到0映射依然是0度到360度视觉上就是连续旋转。如果用toValue: 360配合持续增长的角度值反而容易在动画重启时出现跳变。设置动画的时候要用Easing.linear也就是线性缓动。风扇旋转是匀速的如果默认的ease会让它忽快忽慢看起来像是被风吹的一顿一顿很不自然。useNativeDriver也要注意。在标准RN环境里原生驱动可以避免动画每帧通过JS线程计算性能更好。鸿蒙适配层是否完全支持要看版本。如果动画跑起来不动先别急着怀疑代码把useNativeDriver改成false试试。我在某次调试中就遇到过类似情况后来加了个简单开关来切换这个参数方便定位问题。3.2 开关与档位的状态设计这个Demo的状态就两个isOn表示是否开启speed表示当前档位。我设定有3档对应的每圈毫秒数为1200、800、450。档位越高跑完一圈的时间越短看起来转速越快。状态放在App根组件里通过props传给Fan。代码大致如下const [isOn, setIsOn] useState(false); const [speed, setSpeed] useState(1);开关按钮的逻辑很简单点击取反。切换档位时如果当前风扇没开点击档位按钮不做任何反应避免用户以为风扇转起来了其实并没有。如果开着直接setSpeed(level)让动画的duration随档位变化。档位变化时要特别注意动画的重启方式。直接在useEffect依赖里把speed加进去清理函数里先stop()再start()。如果只是改duration而不停旧动画有可能出现两个动画同时控制同一个Animated.Value结果就是旋转一顿一顿甚至转速错乱。这个坑我在第一次实现时踩过后来在清理函数里统一stop()问题立刻消失。3.3 用useEffect驱动动画生命周期我把动画的启动和停止放在useEffect里依赖isOn和speed。这样状态一变动画能自动跟着变不需要在按钮点击回调里手动操作动画对象。这是React声明式思维和命令式动画API结合的一个典型写法。const durationMap { 1: 1200, 2: 800, 3: 450 }; useEffect(() { if (!isOn) { animRef.current?.stop(); rotation.setValue(0); return; } animRef.current?.stop(); animRef.current Animated.loop( Animated.timing(rotation, { toValue: 1, duration: durationMap[speed], easing: Easing.linear, useNativeDriver: true, }) ); animRef.current.start(); return () animRef.current?.stop(); }, [isOn, speed, rotation]);这里有一个细节animRef在useRef里存动画对象而不是直接用局部变量。因为每次effect重跑都会生成新的动画实例局部变量在异步回调里很容易引用到旧的实例导致stop的是上一个动画新的动画还在跑。用animRef保存当前动画清理和重启才能准确。rotation.setValue(0)是我个人习惯。关掉风扇后让叶片回到初始位置下一轮开启时从0度开始逻辑更整洁。如果你希望关掉时停在当前角度、开机再继续转可以去掉这一行。但注意Animated.loop的重置行为可能让重启瞬间产生轻微跳变这点要在意。3.4 控制按钮的实现控制按钮我用TouchableOpacity因为它自带按压透明度反馈比写死View加onPress要好看得多。按钮布局分成一行电源键、一行档位键。电源键的名字暗示状态风扇关闭时显示“开启”开启时显示“关闭”。档位键显示“1档”“2档”“3档”当前选中的档位按钮背景加深方便一眼看到当前转速。TouchableOpacity style{[styles.btn, speed level styles.activeBtn]} onPress{() changeSpeed(level)} Text style{styles.btnText}{level} 档/Text /TouchableOpacity点击事件绑定在changeSpeed上函数内部判断isOn。按钮虽然视觉上可以点击但状态受控不会误操作。这种“视觉可点、逻辑受限”的处理方式在交互设计里很常见用户不需要学习规则点了没反应或有一档被禁用他们自然能理解当前状态。完整的App代码我就不逐行贴了核心是App持有状态Fan组件接收状态并驱动动画控制区按钮修改状态。整个数据流是单向的找bug很容易状态变了排查props动画不转排查useEffect。4. 在鸿蒙设备上运行与调试4.1 构建与部署流程使用鸿蒙适配层之后新增的编译目标会把RN的JS代码、资源文件和原生工程打包成一个可安装的应用包。实际操作时适配层一般会提供一个构建脚本类似npm run harmony这个命令会同步JS代码到鸿蒙工程、执行原生构建、最后产出hap包。第一次构建会比较慢因为要把鸿蒙侧的依赖下载、编译后面增量构建会快很多。我建议第一次部署用模拟器省去插线、授权、驱动识别这些环节。模拟器如果卡先看CPU占用是不是过高把其他重型应用关掉。我在模拟器里第一次跑起来风扇是转了但动画明显有卡顿当时以为是适配层性能不行换到真机后再测发现非常流畅。所以动画类的性能判断一定要以真机为准。真机部署前需要确认鸿蒙设备已开启开发者模式并允许安装调试包。有些设备安装时会提示应用来源选择允许即可。如果构建脚本支持签名配置建议在调试阶段用默认调试签名发布前再替换正式签名。4.2 常见问题速查表我把实际碰到的典型问题整理成一张表先快速对照详细的踩坑记录在下文现象常见原因解决办法构建后启动白屏JS Bundle没有正确打包进hap重新执行构建脚本确认JS Bundle路径动画完全不动useNativeDriver不被当前适配层支持改为false再试叶片在转但位置偏移叶片定位臂没有放在风扇中心检查bladeArm的left/top计算点击按钮没反应事件绑定在子组件丢失或状态被覆盖检查onPress和状态提升真机识别不到设备USB调试未开启或驱动缺失重新插拔、确认开发者模式编译内存不足原生构建默认内存上限太低设置JVM堆大小或关掉其他应用这些坑中白屏是我遇到最耗时的。解决思路很简单先从React Native的通用日志里看JS端有没有报错再在原生侧看打包是否成功。如果两端都没有日志几乎可以确定是JS Bundle没有打出来。做一个入门Demo时这个排查过程比写代码更值得体验它能帮你理解RN代码是怎么被原生容器加载的。4.3 调试中的几个小技巧日志是跨平台联调的第一工具。我在Fan组件的useEffect里加过一行console.log打印当前档位和转速目标。看起来很简单但在调试“档位切换后动画停不下来”时日志能直接告诉我effect到底有没有被重新触发。另一个技巧是使用React Native的DevMenu。正常情况下它能提供reload、打开调试器等功能。在鸿蒙适配层上不同版本的DevMenu入口可能藏在shake或特定的系统菜单里多做一步确认别以为没有。调试时的reload功能对快速验证代码改动很有用不用每次重新构建。还有一个性能判断技巧在动画期间不要频繁打印日志或做setState。JS线程一旦被塞满原生动画也会受影响。我在调试时曾为了方便看状态每秒打印一次当前角度结果风扇转速肉眼可见地掉下来了。关掉日志之后立刻恢复流畅。这个现象很多人没注意过但它很重要。5. 优化与扩展从一个风扇到更多可能性5.1 减少重渲染当前Demo很小不做性能优化也能跑。但如果以后要加入更多动画和交互有几件顺手就能做的事把Fan组件用React.memo包一层避免App状态变化时Fan组件反复渲染按钮回调用useCallback固定引用动画相关的Animated.Value不要通过state管理始终放在useRef里。这些不是炫技而是动画场景下很实用的基本素养。动画期间最忌讳的是每帧setState。比如把旋转角度存到state里然后通过setState更新这会让整个组件树在每一帧都参与diff和render。用Animated的方案JS只启动一次动画后续渲染全部交给原生侧差距是数量级的。对入门者来说养成这个意识比记住API更重要。5.2 加一个手势调速想更进阶一点可以在控制区放一个滑块或者做上下滑动调速。手势层用React Native的PanResponder或者响应式滚动组件都行。核心逻辑还是改speed这个状态动画部分不用动。这就是状态驱动UI的好处所有交互最终汇成一个数值UI层只负责根据数值表现。我之前试过用手势滑动调速滑快滑慢会改变档位手感上有了明显提升。后来发现调试时更容易暴露问题手势事件频繁改变speed如果动画重启逻辑没写好就会卡顿。所以这个扩展既是功能增强也是压力测试。5.3 加入摇头与音效除了转电风扇还有个标志性动作就是摇头。实现思路不难给叶片组再加一个左右摆动的基础动画用另一个Animated.Value控制从-20度到20度再往返。关键是摇头动画和旋转动画要跑在同一个变换节点上组合成rotate和rotateY之类的变换。加上音效和触感反馈会让demo看起来更完整但这部分依赖的原生能力在鸿蒙适配层里会有差异化支持做之前先查一下包是否可用。从电风扇这个项目延伸出去你可以把同样的动画能力用到加载动画、扫码动画、时钟表盘、雷达扫描这些场景。核心只有一个拆出不变的容器再把可变的部分用状态和动画组合出来。掌握这个思路比会写某个按钮要值钱得多。我个人实际操作下来的体会是跨平台开发最容易忽视的不是框架差异而是“如何把动画和状态管理干净地拆开”。鸿蒙适配层现在能跑通这种基础Demo不代表所有RN生态都零成本迁移但至少对于常规页面和轻动画确实可以做到一次开发、多处运行。写这个Demo的一天里我最多的时间花在查构建日志和动画卡顿上反而是代码逻辑本身很顺利。如果你也想试试鸿蒙开发真的建议从这种小项目入手先让风扇转起来再想它能为你解决什么问题。
返回列表