1. 项目概述:React Native鸿蒙跨平台手电筒应用开发
作为一名有五年跨平台开发经验的程序员,我最近尝试用React Native为鸿蒙系统开发一个模拟手电筒应用,整个过程既充满挑战又收获颇丰。这个看似简单的项目实际上涉及了React Native在鸿蒙平台的适配、原生模块调用、设备权限管理等多个关键技术点。
手电筒应用虽然功能简单,但却是学习跨平台开发的绝佳案例。它需要调用设备硬件(摄像头LED),处理用户权限,实现状态切换等基础但完整的移动开发流程。选择React Native作为开发框架,可以让我们用熟悉的JavaScript语法同时覆盖Android、iOS和新兴的鸿蒙系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目初始化
2.1 开发环境配置
首先需要安装Node.js(建议16.x以上版本)和Watchman(Mac用户需要)。然后通过npm安装React Native CLI:
bash复制npm install -g react-native-cli
对于鸿蒙平台的支持,我们需要额外安装华为提供的React Native鸿蒙适配器:
bash复制npm install @react-native-harmony/harmony
注意:目前React Native对鸿蒙的支持还处于早期阶段,建议使用React Native 0.68以上版本以获得更好的兼容性。
2.2 项目创建与鸿蒙平台添加
使用以下命令创建新项目:
bash复制npx react-native init FlashlightApp --template react-native@0.68.0
cd FlashlightApp
然后添加鸿蒙平台支持:
bash复制npx react-native-harmony add harmony
这会在项目中创建harmony目录,包含鸿蒙平台特定的代码和配置。
3. 核心功能实现
3.1 权限请求与硬件访问
手电筒功能需要访问设备的相机和闪光灯权限。在React Native中,我们可以使用PermissionsAndroid模块来处理权限请求:
javascript复制import { PermissionsAndroid } from 'react-native';
async function requestCameraPermission() {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
{
title: "手电筒需要相机权限",
message: "应用需要访问相机以使用闪光灯功能",
buttonNeutral: "稍后询问",
buttonNegative: "取消",
buttonPositive: "确定"
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn(err);
return false;
}
}
对于鸿蒙平台,权限系统略有不同,我们需要在config.json中声明需要的权限:
json复制{
"module": {
"reqPermissions": [
{
"name": "ohos.permission.CAMERA"
}
]
}
}
3.2 手电筒控制实现
由于React Native本身不提供直接控制手电筒的API,我们需要创建原生模块。这里展示如何在鸿蒙平台上实现:
首先在harmony/src/main/ets/modules创建FlashlightModule.ets:
typescript复制import { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
import camera from '@ohos.multimedia.camera';
import { BusinessError } from '@ohos.base';
export class FlashlightModule extends TurboModule {
private cameraManager: camera.CameraManager | null = null;
constructor(ctx: TurboModuleContext) {
super(ctx);
try {
this.cameraManager = camera.getCameraManager(ctx.uiAbilityContext);
} catch (error) {
console.error('Failed to get camera manager:', (error as BusinessError).message);
}
}
async turnOn() {
if (!this.cameraManager) return false;
try {
const cameras = this.cameraManager.getSupportedCameras();
if (cameras.length === 0) return false;
const flash = this.cameraManager.getSupportedFlashModes(cameras[0]);
if (flash.includes(camera.FlashMode.FLASH_MODE_TORCH)) {
await this.cameraManager.setFlashMode(cameras[0], camera.FlashMode.FLASH_MODE_TORCH);
return true;
}
} catch (error) {
console.error('Failed to turn on flashlight:', (error as BusinessError).message);
}
return false;
}
async turnOff() {
if (!this.cameraManager) return false;
try {
const cameras = this.cameraManager.getSupportedCameras();
if (cameras.length === 0) return false;
await this.cameraManager.setFlashMode(cameras[0], camera.FlashMode.FLASH_MODE_OFF);
return true;
} catch (error) {
console.error('Failed to turn off flashlight:', (error as BusinessError).message);
}
return false;
}
}
然后在JavaScript端创建对应的模块接口:
javascript复制import { NativeModules } from 'react-native';
const { FlashlightModule } = NativeModules;
export const turnFlashlightOn = async () => {
try {
return await FlashlightModule.turnOn();
} catch (e) {
console.error(e);
return false;
}
};
export const turnFlashlightOff = async () => {
try {
return await FlashlightModule.turnOff();
} catch (e) {
console.error(e);
return false;
}
};
4. 用户界面设计与状态管理
4.1 基础UI组件
手电筒应用的UI相对简单,主要包含一个开关按钮和状态指示:
javascript复制import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { turnFlashlightOn, turnFlashlightOff } from './flashlightService';
const FlashlightApp = () => {
const [isOn, setIsOn] = useState(false);
const [permissionGranted, setPermissionGranted] = useState(false);
const toggleFlashlight = async () => {
if (!permissionGranted) {
const granted = await requestCameraPermission();
setPermissionGranted(granted);
if (!granted) return;
}
if (isOn) {
const success = await turnFlashlightOff();
if (success) setIsOn(false);
} else {
const success = await turnFlashlightOn();
if (success) setIsOn(true);
}
};
return (
<View style={styles.container}>
<Text style={styles.statusText}>
{isOn ? '手电筒已开启' : '手电筒已关闭'}
</Text>
<TouchableOpacity
style={[styles.button, isOn ? styles.buttonOn : styles.buttonOff]}
onPress={toggleFlashlight}
>
<Text style={styles.buttonText}>
{isOn ? '关闭' : '打开'}
</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
button: {
width: 150,
height: 150,
borderRadius: 75,
justifyContent: 'center',
alignItems: 'center',
marginTop: 30,
},
buttonOn: {
backgroundColor: '#ffeb3b',
shadowColor: '#ffeb3b',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 20,
elevation: 10,
},
buttonOff: {
backgroundColor: '#333',
},
buttonText: {
fontSize: 24,
color: '#fff',
fontWeight: 'bold',
},
statusText: {
fontSize: 20,
marginBottom: 20,
color: '#333',
},
});
export default FlashlightApp;
4.2 添加动画效果
为了提升用户体验,我们可以添加一些简单的动画效果:
javascript复制import { Animated, Easing } from 'react-native';
// 在组件内部
const [scaleAnim] = useState(new Animated.Value(1));
const animateButton = () => {
Animated.sequence([
Animated.timing(scaleAnim, {
toValue: 0.9,
duration: 100,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(scaleAnim, {
toValue: 1.1,
duration: 100,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(scaleAnim, {
toValue: 1,
duration: 100,
easing: Easing.ease,
useNativeDriver: true,
}),
]).start();
};
const toggleFlashlight = async () => {
animateButton();
// 原有逻辑...
};
// 修改TouchableOpacity为Animated.View
<Animated.View
style={[
styles.button,
isOn ? styles.buttonOn : styles.buttonOff,
{ transform: [{ scale: scaleAnim }] }
]}
>
<TouchableOpacity onPress={toggleFlashlight}>
<Text style={styles.buttonText}>
{isOn ? '关闭' : '打开'}
</Text>
</TouchableOpacity>
</Animated.View>
5. 多平台适配与测试
5.1 Android平台适配
对于Android平台,我们需要创建对应的原生模块。在android/app/src/main/java/com/flashlightapp创建FlashlightModule.java:
java复制package com.flashlightapp;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.content.Context;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class FlashlightModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
private CameraManager cameraManager;
private String cameraId;
FlashlightModule(ReactApplicationContext context) {
super(context);
this.reactContext = context;
cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
for (String id : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Boolean hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
if (hasFlash != null && hasFlash) {
cameraId = id;
break;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@NonNull
@Override
public String getName() {
return "FlashlightModule";
}
@ReactMethod
public void turnOn(Promise promise) {
try {
if (cameraId != null) {
cameraManager.setTorchMode(cameraId, true);
promise.resolve(true);
} else {
promise.resolve(false);
}
} catch (CameraAccessException e) {
promise.reject("FLASHLIGHT_ERROR", e.getMessage());
}
}
@ReactMethod
public void turnOff(Promise promise) {
try {
if (cameraId != null) {
cameraManager.setTorchMode(cameraId, false);
promise.resolve(true);
} else {
promise.resolve(false);
}
} (CameraAccessException e) {
promise.reject("FLASHLIGHT_ERROR", e.getMessage());
}
}
}
然后在同目录下创建FlashlightPackage.java:
java复制package com.flashlightapp;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FlashlightPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new FlashlightModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
最后在MainApplication.java中注册这个包:
java复制@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new FlashlightPackage());
return packages;
}
5.2 iOS平台适配
对于iOS平台,同样需要原生模块实现。在ios目录下创建FlashlightModule.h:
objectivec复制#import <React/RCTBridgeModule.h>
@interface FlashlightModule : NSObject <RCTBridgeModule>
@end
和FlashlightModule.m:
objectivec复制#import "FlashlightModule.h"
#import <AVFoundation/AVFoundation.h>
@implementation FlashlightModule
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(turnOn:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch]) {
[device lockForConfiguration:nil];
[device setTorchMode:AVCaptureTorchModeOn];
[device unlockForConfiguration];
resolve(@(YES));
} else {
resolve(@(NO));
}
}
RCT_EXPORT_METHOD(turnOff:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch]) {
[device lockForConfiguration:nil];
[device setTorchMode:AVCaptureTorchModeOff];
[device unlockForConfiguration];
resolve(@(YES));
} else {
resolve(@(NO));
}
}
@end
6. 调试与优化
6.1 常见问题解决
-
鸿蒙平台闪退问题:
- 检查config.json中的权限声明
- 确保所有原生方法都有错误处理
- 查看DevEco Studio的日志输出
-
Android权限被拒绝:
- 检查AndroidManifest.xml中的权限声明
- 确保在请求权限前检查是否已经拥有权限
- 处理用户拒绝权限的情况
-
iOS模拟器不支持手电筒:
- 添加模拟器检测代码,返回适当提示
- 在实际设备上测试功能
6.2 性能优化
-
减少原生模块调用:
- 合并开关操作为一个方法,通过参数控制
- 缓存设备状态,避免重复检查
-
内存管理:
- 在组件卸载时确保关闭手电筒
- 处理应用进入后台时的情况
-
用户体验优化:
- 添加加载状态,避免用户多次点击
- 提供适当的错误反馈
7. 项目扩展思路
这个基础项目可以进一步扩展:
-
添加SOS闪光功能:
- 实现摩尔斯电码的闪光模式
- 可配置的闪光频率和模式
-
屏幕手电筒:
- 当设备没有闪光灯时,使用全白屏幕作为替代
- 可调节屏幕亮度
-
颜色手电筒:
- 使用不同颜色的光
- 适用于特定场景(如摄影、暗房工作)
-
智能控制:
- 根据环境光线自动开关
- 定时关闭功能
-
多语言支持:
- 添加i18n国际化支持
- 根据系统语言自动切换
这个React Native鸿蒙跨平台手电筒项目虽然简单,但涵盖了从环境搭建、原生模块开发、UI设计到多平台适配的完整开发流程。通过这个项目,我深刻体会到React Native在跨平台开发中的优势,也了解了鸿蒙平台的特殊性。在实际开发中,最大的挑战是鸿蒙平台的文档和社区资源相对较少,需要更多自己摸索和尝试。
