npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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_photo

yarn

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"
  }
}

引鸿蒙端代码

目前有两种方法:

  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,添加:

    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),,
      ];
    }
  2. 在 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.etsentry/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操作同步代码。

  1. 直接链接源码。

    如需使用直接链接源码,请参考直接链接源码说明

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