@hadss/react_native_moving_photo
v1.0.0-rc.1
Published
Shared MovingPhotoView with OpenHarmony feature
Readme
@hadss/react_native_moving_photo
介绍
为 React Native 提供动态照片显示功能,当设备需要播放动态照片文件时,可以调用MovingPhoto模块。该库封装了MovingPhotoView组件,开发者可通过MovingPhotoView展示动态照片。
工程目录
.
├─harmony
| ├─moving_photo
│ │ └─src
│ │ ├─main
│ │ │ └─cpp
│ │ │ └─CMakeLists.txt
│ │ │ └─RNMovingPhotoPackage.h
│ │ │ └─ets
│ │ │ ├─RNMovingPhoto.ets // ArkTS实现
│ │ │ ├─RNMovingPhotoPackage.ts
| └─build-profile.json5 // 编译配置文件(多环境Harmony/OpenHarmony)
├─src
│ ├─index.ts // 导出的组件
│ ├─MovingPhotoView.tsx
│ └─fabric
| └─MovingPhotoNativeComponent.ts // ArkTs组件
│ └─types
| └─event.ts // 组件的响应事件
| └─photo.ts // 组件的props安装与使用
进入到工程目录并输入以下命令:
npm
npm install @hadss/react_native_moving_photoyarn
yarn add @hadss/react_native_moving_photo下面的代码展示了这个库的基本使用场景:
MovingPhotoView使用示例
import MovingPhotoView from '@hadss/react_native_moving_photo';
import React, { useCallback, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput } from 'react-native';
const TAG = 'MovingPhotoTest';
const getResizeMode = (index: number) => {
if (index === 1) {
return 'contain';
} else if (index === 2) {
return 'center';
} else if (index === 3) {
return 'stretch';
} else {
return 'cover';
}
};
const MovingPhotoPage = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [isMuted, setIsMuted] = useState(true);
const [autoPlay, setIsAutoPlay] = useState(false);
const [repeatPlay, setIsRepeatPlay] = useState(false);
const [enableAnalyzer, setIsEnableAnalyzer] = useState(false);
const [autoPlayPeriod, setAutoPlayPeriod] = useState<{
startTime: number;
endTime: number;
}>();
const [resizeMode, setResizeMode] = useState<'contain' | 'cover' | 'stretch' | 'center'>('cover');
const [resizeIndex, setResizeIndex] = useState(1);
const [source, setText] = useState('file://media/Photo/41/IMG_1758766363_031/IMG_20250925_101103.jpg');
const photoRef = React.useRef<any>();
const startPlayback = useCallback(() => {
photoRef.current && photoRef.current.startPlayback();
}, []);
const stopPlayback = useCallback(() => {
photoRef.current && photoRef.current.stopPlayback();
}, []);
const refreshMovingPhoto = useCallback(() => {
photoRef.current && photoRef.current.refreshMovingPhoto();
}, []);
const onComplete = useCallback(() => {
console.log(TAG, 'onComplete');
}, []);
const onStart = useCallback(() => {
console.log(TAG, 'onStart');
}, []);
const onPause = useCallback(() => {
console.log(TAG, 'onPause');
}, []);
const onStop = useCallback(() => {
console.log(TAG, 'onStop');
}, []);
const onFinish = useCallback(() => {
console.log(TAG, 'onFinish');
}, []);
const onError = useCallback(() => {
console.log(TAG, 'onError');
}, []);
const setMute = useCallback(() => {
setIsMuted(!isMuted);
}, [isMuted]);
const setAutoPlay = useCallback(() => {
setIsAutoPlay(!autoPlay);
}, [autoPlay]);
const setRepeatPlay = useCallback(() => {
setIsRepeatPlay(!repeatPlay);
}, [repeatPlay]);
const setPeriod = useCallback(() => {
setAutoPlayPeriod({ startTime: 300, endTime: 1000 });
}, []);
const setEnableAnalyzer = useCallback(() => {
setIsEnableAnalyzer(!enableAnalyzer);
}, [enableAnalyzer]);
const setImageFill = useCallback(() => {
setResizeIndex(resizeIndex + 1);
setResizeMode(getResizeMode(resizeIndex % 4));
}, [resizeIndex]);
const dataList = [
{
id: 0,
text: '设置静音: ' + isMuted,
onPress: setMute,
},
{
id: 1,
text: '设置自动播放: ' + autoPlay,
onPress: setAutoPlay,
},
{
id: 2,
text: '设置重复播放: ' + repeatPlay,
onPress: setRepeatPlay,
},
{
id: 3,
text: '设置自动播放区间: ' + JSON.stringify(autoPlayPeriod),
onPress: setPeriod,
},
{
id: 4,
text: '设置支持AI: ' + enableAnalyzer,
onPress: setEnableAnalyzer,
},
{
id: 5,
text: '设置显示模式: ' + resizeMode,
onPress: setImageFill,
},
{
id: 6,
text: '点击播放',
onPress: startPlayback,
},
{
id: 7,
text: '点击停止',
onPress: stopPlayback,
},
{
id: 8,
text: '点击刷新',
onPress: refreshMovingPhoto,
},
];
const selectColor = (index: number) => {
return {
bgColor: activeIndex === index ? 'rgb(10, 89, 247)' : 'rgba(0, 0, 0, 0.05)',
textColor: activeIndex === index ? 'white' : 'rgb(10, 89, 247)',
};
};
const touchItem = (text: string, index: number, onPress: Function) => {
return (
<TouchableOpacity
key={index + '--' + text}
style={[{ backgroundColor: selectColor(index).bgColor }, styles.button]}
onPress={() => {
onPress();
setActiveIndex(index);
}}>
<Text style={[{ color: selectColor(index).textColor }, styles.btnText]}>{text}</Text>
</TouchableOpacity>
);
};
return (
<View style={styles.container}>
<View style={styles.title}>
<Text style={styles.titleText}>MovingPhoto Test</Text>
</View>
<ScrollView scrollEnabled={true} style={styles.scrollView} showsVerticalScrollIndicator={false}>
<TextInput
style={styles.textinput}
multiline={true}
onChangeText={(text) => setText(text)} // 当文本变化时更新状态
value={source} // 设置输入框的当前值
placeholder="file://media/Photo/41/IMG_1758766363_031/IMG_20250925_101103.jpg" // 占位符文本
/>
{dataList.map((item) => {
return touchItem(item.text, item.id, item.onPress);
})}
<MovingPhotoView
ref={(ref) => {
photoRef.current = ref;
}}
source={source}
autoPlayPeriod={autoPlayPeriod}
isMuted={isMuted}
isAutoPlay={autoPlay}
isRepeatPlay={repeatPlay}
enableAnalyzer={enableAnalyzer}
resizeMode={resizeMode ?? 'cover'}
style={styles.photoStyle}
onComplete={onComplete}
onFinish={onFinish}
onPause={onPause}
onStart={onStart}
onStop={onStop}
onError={onError}
/>
</ScrollView>
</View>
);
};
export default MovingPhotoPage;
const styles = StyleSheet.create({
title: {
flexDirection: 'row',
justifyContent: 'flex-start',
width: '100%',
marginBottom: 16,
},
titleText: {
fontSize: 30,
},
textinput: {
backgroundColor: 'white',
width: '100%',
minHeight: 60,
marginBottom: 16,
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 8,
color: 'rgba(0, 0, 0, 0.6)',
},
photoStyle: {
width: '100%',
height: 400,
marginBottom: 10,
},
container: {
backgroundColor: '#F1F3F5',
paddingHorizontal: 16,
paddingTop: 72,
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
height: '100%',
},
scrollView: {
width: '100%',
height: '100%',
},
button: {
width: '100%',
height: 40,
borderRadius: 20,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
},
btnText: {
fontSize: 16,
fontWeight: '500',
},
});
Link
目前OpenHarmony暂不支持AutoLink,所以Link步骤需要手动配置。
首先需要使用DevEco Studio打开项目里的OpenHarmony工程,在工程根目录的oh-package.json5添加overrides字段:
{
"overrides": {
"@rnoh/react-native-openharmony" : "./react_native_openharmony"
}
}引鸿蒙端代码
目前有两种方法:
通过har包引入(在IDE完善相关功能后该方法会被遗弃,目前首选此方法)。
说明: har包位于三方库安装路径的
harmony文件夹下。a. 打开
entry/oh-package.json5,添加以下依赖:"dependencies":{ "@rnoh/react-native-openharmony": "file:../react_native_openharmony", "@hadss/react_native_moving_photo": "file:../../node_modules/@hadss/react_native_moving_photo/harmony/moving_photo.har", }b. 配置CMakeLists和引入RNOHGeneratedPackage:
打开
entry/src/main/cpp/CMakeLists.txt,添加:project(rnapp) cmake_minimum_required(VERSION 3.4.1) set(CMAKE_SKIP_BUILD_RPATH TRUE) set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp") set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated") set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments") set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie") add_compile_definitions(WITH_HITRACE_SYSTRACE) set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use + file(GLOB GENERATED_CPP_FILES "./generated/*.cpp") + add_subdirectory("${OH_MODULE_DIR}/@hadss/react_native_moving_photo/src/main/cpp" ./moving_photo) add_subdirectory("${RNOH_CPP_DIR}" ./rn) add_library(rnoh_app SHARED + ${GENERATED_CPP_FILES} "./PackageProvider.cpp" "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" ) target_link_libraries(rnoh_app PUBLIC rnoh) + target_link_libraries(rnoh_app PUBLIC moving_photo)c. 打开
entry/src/main/cpp/PackageProvider.cpp,添加:#include "RNOH/PackageProvider.h" + #include "generated/RNOHGeneratedPackage.h" using namespace rnoh; std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) { return { + std::make_shared<RNOHGeneratedPackage>(ctx) }; }d. 在ArkTs侧引入RNMovingPhotoPackage:
打开
entry/src/main/ets/RNPackagesFactory.ts,添加:+ import { RNMovingPhotoPackage } from '@hadss/react_native_moving_photo/ts';; export function createRNPackages(ctx: RNPackageContext): RNPackage[] { return [ + new RNMovingPhotoPackage(ctx),, ]; }在 ArkTS 侧引入 RNMovingPhoto
... + import { RNMovingPhoto } from '@hadss/react_native_moving_photo'; @Builder export function buildCustomRNComponent(ctx: ComponentBuilderContext) { ... + if (ctx.componentName === RNMovingPhoto.NAME) { + RNMovingPhoto({ + ctx: ctx.rnComponentContext, + tag: ctx.tag + }) + } ... } ...在
entry/src/main/ets/pages/index.ets或entry/src/main/ets/rn/LoadBundle.ets找到常量arkTsComponentNames在其数组里添加组件名const arkTsComponentNames: Array<string> = [ SampleView.NAME, GeneratedSampleView.NAME, PropsDisplayer.NAME, + RNMovingPhoto.NAME ];
如果不是通过Picker组件选取的图片,通过图片URI链接直接访问的图片,需要动态申请权限, 应用配置文件module.json5 配置新增:
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:xxx",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
]在src/main/ets/pages/FeaturesListPage.ets中添加申请权限的函数,同时在aboutToAppear中调用该函数
async requestPermissions(): Promise<void> {
let atManager = abilityAccessCtrl.createAtManager();
try {
await atManager.requestPermissionsFromUser(
this.getUIContext().getHostContext(),
['ohos.permission.READ_IMAGEVIDEO'] // 权限数组
);
} catch (err) {
console.error(`权限申请失败: ${err}`);
}
}+ import { abilityAccessCtrl } from '@kit.AbilityKit';
aboutToAppear() {
emitter.on({ eventId: Constants.EVENT_ID_1 }, (eventData) => {
if (eventData.data) {
const param: string = eventData.data['param'].split(', ');
animateTo({ duration: 700 }, () => {
this.navPathStack.pushPath({ name: 'FeaturesPage', param: new PageParam(param[0], param[1], param[2]) }, false);
+ if (param[1] === 'MovingPhotoPage') {
+ this.requestPermissions();
+ }
});
}
});
} e. 运行:
点击右上角的`sync`按钮
或者在终端执行:
```bash
cd entry
ohpm install
```
然后编译、运行即可。说明 若项目启动时报错:can not find record '&@rnoh/react-native-openharmony/generated/ts&X.X.X'。需在entry\oh_modules@rnoh\react-native-openharmony\ts.ts文件中添加export * from './generated/ts',并删除.cxx文件夹、build文件夹,然后执行sync操作同步代码。
直接链接源码。
如需使用直接链接源码,请参考直接链接源码说明。
API
说明: "Platform"列表示支持的平台,All表示支持所有平台。
API
MovingPhotoView | Name | Description | Type | 是否必传 | Platform | | ------------------- | ------------------------|----------------------------| -------- | --------- | | source | 图片资源路径 |string | 是 | OpenHarmony| | isAutoPlay | 是否自动播放 |boolean |否| OpenHarmony| | isMuted | 是否静音播放 | boolean |否| OpenHarmony| | isRepeatPlay | 是否重复播放,repeatPlay与autoPlay及长按播放互斥,repeatPlay设置时,autoPlay和长按播放均不生效| boolean | 否|OpenHarmony| | autoPlayPeriod | 自动播放区间,在调用此方法前,需将isAutoPlay设置为true,设置自动播放,否则指定的视频区间(startTime, endTime)无法生效。| AutoPlayPeriod |否| OpenHarmony| | enableAnalyzer | 设置该图片是否支持AI分析,当前支持主体识别、文字识别和对象查找等功能 | boolean |否| OpenHarmony| | resizeMode | 动态照片显示模式,默认值:Cover。 | resizeMode |否| OpenHarmony| | onComplete | 动态照片加载完成图片时| function |否| OpenHarmony| | onFinish | 播放结束时 | function |否| OpenHarmony| | onPause | 播放暂停时触发该事件。 | function | 否|OpenHarmony| | onStart | 播放时触发该事件。| function |否| OpenHarmony| | onStop | 播放停止时触发该事件(当stopPlayback()方法被调用后触发)。| function |否| OpenHarmony| | onError | 播放失败时触发该事件。| function |否| OpenHarmony| | startPlayback | 开始播放| function |否| OpenHarmony| | stopPlayback | 停止播放| function |否| OpenHarmony| | refreshMovingPhoto | 强制刷新动态照片组件加载的视频和图片资源,会打断组件当前的行为,使用时要谨慎。| function |否| OpenHarmony|
AutoPlayPeriod | Name | Description | Type |是否必传| Platform | | ---------- | --------------------------------------------| --------- | --- |-------- | |startTime| 区间播放开始时间,单位:ms。取值范围:大于等于0。| number |是| OpenHarmony | |endTime| 区间播放结束时间,单位:ms。取值范围:大于startTime。| number | 是|OpenHarmony |
resizeMode | Name | Description | Type | Platform | | :---------- | :--------------------------------------------------| :--------- | :-------- | |center| 图片或视频显示在组件的横向和纵向居中,且保持原有尺寸。| 'center' | OpenHarmony | |contain|保持宽高比进行缩小或者放大,使得图片或视频完全显示在显示边界内,对齐方式为水平居中。| 'contain'| OpenHarmony | |cover| 保持宽高比进行缩小或者放大,使得图片或视频两边都大于或等于显示边界,对齐方式为水平居中。| 'cover'| OpenHarmony | |stretch|不保持宽高比进行放大缩小,使得图片或视频充满显示边界,对齐方式为水平居中。| 'stretch' | OpenHarmony |
约束与限制
仅支持展示图库的图片,不支持网络图片,该组件使用AVPlayer进行播放,同时开启的AVPlayer个数建议不超过3个,超过3个可能会出现视频播放卡顿现象。 本示例仅支持标准系统上运行,支持设备:Phone | PC/2in1 | Tablet | TV。 地区限制:仅支持中国境内(不包含中国香港、中国澳门、中国台湾)提供服务。 SDK版本:API18及以上。
