npm package: @hadss/react_native_moving_photo
Description: Shared MovingPhotoView with OpenHarmony feature
Latest version: 1.0.0-rc.1

# @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
```

## 安装与使用

进入到工程目录并输入以下命令：

<!-- tabs:start -->

**npm**

```bash
npm install @hadss/react_native_moving_photo
```

**yarn**

```bash
yarn add @hadss/react_native_moving_photo
```
下面的代码展示了这个库的基本使用场景：

**MovingPhotoView使用示例** 
```jsx
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字段：

```json
{
  "overrides": {
    "@rnoh/react-native-openharmony" : "./react_native_openharmony"
  }
}
```

### 引鸿蒙端代码

目前有两种方法：

1. **通过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`，添加：

    ```diff
    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`，添加：

    ```diff
    #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`，添加：

    ```diff
    + import { RNMovingPhotoPackage } from '@hadss/react_native_moving_photo/ts';;
    
    export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
      return [
    +   new RNMovingPhotoPackage(ctx),,
      ];
    }
    ```
2. **在 ArkTS 侧引入 RNMovingPhoto**

   ```diff
     ...
   +  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` 在其数组里添加组件名

   ```diff
   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}`);
    }
  }
  ``` 
  ```diff
  + 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操作同步代码。

1. **直接链接源码。**

    如需使用直接链接源码，请参考[直接链接源码说明](https://gitee.com/react-native-oh-library/usage-docs/blob/master/zh-cn/link-source-code.md)。

##  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及以上。