npm package: @hadss/react_native_geometry_transition
Description: Shared Element Transition with OpenHarmony feature
Latest version: 1.0.8

# @hadss/react_native_geometry_transition

## 介绍

为基于 React Native 的混合开发工程提供一镜到底能力。

## 工程目录

```
.
├── harmony
│   ├── geometry_transition
│   │   ├── src
│   │   │   └── main
│   │   │       ├── cpp     // CAPI一镜到底组件实现
│   │   │       │   ├── CMakeLists.txt
│   │   │       │   ├── GeometryViewComponentDescriptor.h
│   │   │       │   ├── GeometryViewComponentInstance.cpp
│   │   │       │   ├── GeometryViewComponentInstance.h
│   │   │       │   ├── GeometryViewEventEmitter.cpp
│   │   │       │   ├── GeometryViewEventEmitter.h
│   │   │       │   ├── GeometryViewJSIBinder.h
│   │   │       │   ├── GeometryViewNode.cpp
│   │   │       │   ├── GeometryViewNode.h
│   │   │       │   ├── GeometryViewPackage.cpp
│   │   │       │   ├── GeometryViewPackage.h
│   │   │       │   ├── Index.d.ts
│   │   │       │   ├── Props.cpp
│   │   │       │   ├── Props.h
│   │   │       │   └── oh-package.json5
│   │   │       ├── ets    // ArkTS一镜到底组件实现
│   │   │       │   ├── GeometryTransitionView.ets
│   │   │       │   ├── GeometryTransitionViewPackages.ts
│   │   │       │   └── generated
│   │   │       │       ├── components
│   │   │       │       │   ├── GeometryTransitionView.ts
│   │   │       │       │   └── ts.ts
│   │   │       │       ├── index.ets
│   │   │       │       ├── ts.ts
│   │   │       │       └── turboModules
│   │   │       │           └── ts.ts
│   │   │       └── module.json5
│   │   └── ts.ts
│   └── geometry_transition.har
├── package.json
├── src
│   ├── fabric
│   │   ├── GeometryView.tsx   // CAPI一镜到底组件
│   │   └── v1
│   │       └── GeometryTransitionViewNativeComponent.tsx   // ArkTS一镜到底组件
│   └── index.tsx
```



## 安装与使用

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

<!-- tabs:start -->

**npm**

```bash
npm install @hadss/react_native_geometry_transition
```

**yarn**

```bash
yarn add @hadss/react_native_geometry_transition
```

<!-- tabs:end -->

下面的代码展示了这个库的基本使用场景：
前置条件：ArkUI的navigation控制路由跳转，NavDestination加载对应的RN bundle。

场景一：当前页面是RN页面，目标页面是原生页面

**PlayList页面**

使用了GeometryView组件，并设置了geometryViewID为'test'，点击事件调用原生navPathStack.pushPath，跳转MusicPlay页面。

```jsx
function PlayList() {
  return (
    <SafeAreaView style={styles.container}>
      <GeometryView
        style={[styles.geometryView, { top: 100, left: 100 }]} 
        geometryViewID={'test'}
        onGeometryViewClick={() => {
          SampleTurboModule.pushStringToHarmony('pages/MusicPlay', 1);
        }}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    alignItems: 'center',
    width: '100%',
    height: '100%',
  },
  geometryView: {
    position: 'absolute', 
    width: 60,
    height: 60,
    backgroundColor: 'red',
  },
});

export default PlayList;
```

**MusicPlay页面**

```jsx
function MusicPlay() {
  return (
    <SafeAreaView style={styles.container}>
      <GeometryView style={{ width: 200, height: 200, backgroundColor: 'red' }} geometryViewID={'test'} onGeometryViewClick={() => {
        SampleTurboModule.pushStringToHarmony('pop', 2)
      }} >
      </GeometryView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    alignItems: 'center',
    width: '100%',
    height: '100%'
  },
  text: {
    fontSize: 18,
    color: '#333',
  },
});

export default MusicPlay;
```

**原生侧**

PlayListPage监听RN页面跳转事件，如果事件触发，调用this.navPathStack.pushPath({ name: 'MusicPlayPage' })，跳转至原生MusicPlayPage页面。

```ts
@Component
export default struct PlayListPage {
  private instance: RNInstance = LoadManager.instance;
  private bundlePath = 'bundle/playlist.harmony.bundle'
  private moduleName = 'PlayList'
  @StorageLink('isMetroAvailable') isMetroAvailable: boolean = false
  @Consume('navPathStack') navPathStack: NavPathStack

  aboutToAppear() {
    emitter.on({ eventId: 1 }, () => {
      animateTo({duration: 700, curve: Curve.Friction}, () => {
        this.navPathStack.pushPath({ name: 'MusicPlayPage' })
      });
    });
  }

  aboutToDisappear() {
    emitter.off(1);
  }

  build() {
    NavDestination() {
      if (this.isMetroAvailable) {
        MetroBaseRN({
          moduleName: this.moduleName,
        }).align(Alignment.Top).margin({ top: 0 })
      } else if (this.instance) {
        BaseRN({
          rnInstance: this.instance,
          moduleName: this.moduleName,
          bundlePath: this.bundlePath,
        }).align(Alignment.Top).margin({ top: 0 })
      } else {
        Text('加载失败')
      }
    }
    .hideTitleBar(true)
  }
}
```

**MusicPlayPage加载RN侧MusicPlay页面**

```ts
@Component
export default struct MusicPlayPage {
  private instance: RNInstance = LoadManager.instance
  private bundlePath = 'bundle/musicplay.harmony.bundle'
  private moduleName = 'MusicPlay'
  @StorageLink('isMetroAvailable') isMetroAvailable: boolean = false
  @Consume('navPathStack') navPathStack: NavPathStack

  aboutToAppear() {
    emitter.on({ eventId: 2 }, () => {
      animateTo({duration: 700, curve: Curve.Friction}, () => {
        this.navPathStack.pop()
      });
    });
  }

  aboutToDisappear() {
    emitter.off(2);
  }

  build() {
    NavDestination() {
      if (this.isMetroAvailable) {
        MetroBaseRN({
          moduleName: this.moduleName,
        })
          .align(Alignment.Top).margin({ top: 20 })
      } else if (this.instance) {
        BaseRN({
          rnInstance: this.instance,
          moduleName: this.moduleName,
          bundlePath: this.bundlePath,
        }).align(Alignment.Top).margin({ top: 20 })
      } else {
        Text('加载失败')
      }
    }
    .geometryTransition('test')   // 设置同RN侧一致的id
    .hideTitleBar(true)
  }
}

```

场景二：当前页面和目标页面均是RN页面

**RN侧当前页面**

使用了GeometryTransitionView组件，并设置了geometryViewID为'test'，pageType设置为'current'，点击跳转按钮调用原生navPathStack.pushPath，跳转至目标页面。
>**Tip**： duration动画执行时间在当前页面设置不会生效，具体动画执行时间由目标页面的duration控制。

```jsx
    <View style={{ width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }}>
      <GeometryTransitionView geometryViewID='test' style={{ width: 100, height: 100, backgroundColor: 'black' }} isFollow={false} duration={700} pageType={'current'}>
      </GeometryTransitionView>
      <Button title="跳转" onPress={() => { SampleTurboModule.pushStringToHarmony('pages/TargetPage', 1); }}></Button>
    </View>
```

**原生侧当前页面**

监听路由跳转事件，跳转至目标页面。

```jsx
  aboutToAppear() {
    emitter.on({ eventId: 1 }, (eventData) => {
      this.pagePathsGoods.pushPath({ name: "TargetPage" }, false);
    });
  }
```

**RN侧目标页面**

使用了GeometryTransitionView组件，并设置了与 “当前页面“ 一致的geometryViewID：'test'，pageType设置为'target'，duration为设置从 “当前页面“ 跳转至目标页面的动画执行时间，点击返回按钮调用原生navPathStack.pop，返回 “当前页面“ 。

```jsx
    <View style={{ width: '100%', height: '50%', justifyContent: 'flex-end', alignItems: 'center', paddingBottom: '120' }}>
      <GeometryTransitionView
        style={{ width: 200, height: 200, backgroundColor: 'red' }}
        geometryViewID={'test'}
        isFollow={false}
        duration={700}
        pageType={'target'}
      >
      </GeometryTransitionView>
      <Button title="返回" onPress={() => { SampleTurboModule.pushStringToHarmony('pages/CurrentPage', 2); }}></Button>
    </View>
```

**原生侧目标页面**

监听路由返回事件，**使用动画**返回跳转至 “当前页面“ 。

```jsx
  aboutToAppear() {
    emitter.on({ eventId: 2 }, (eventData) => {
      if (eventData.data) {
        const path: string = eventData.data['param'];
        if (path === "pages/CurrentPage") {
          animateTo({ duration: 700 }, () => {
            this.pagePathsGoods.pop(false);
          })
        }
      }
    });
  }
```

## 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`，添加以下依赖：

   ```json
   "dependencies": {
       "@rnoh/react-native-openharmony": "file:../react_native_openharmony",
       "@hadss/react_native_geometry_transition": "file:../../node_modules/@hadss/react_native_geometry_transition/harmony/geometry_transition.har",
     }
   ```

   b.配置CMakeLists和引入GeometryViewPackage：

   打开`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
   + add_subdirectory("${OH_MODULE_DIR}/@hadss/react_native_geometry_transition/src/main/cpp" ./geometry_transition)
   
   add_subdirectory("${RNOH_CPP_DIR}" ./rn)
   
   add_library(rnoh_app SHARED
       "./PackageProvider.cpp"
       "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
   )
   
   target_link_libraries(rnoh_app PUBLIC rnoh)
   + target_link_libraries(rnoh_app PUBLIC geometry_transition)
   ```

   c.打开`entry/src/main/cpp/PackageProvider.cpp`，添加：

   ```diff
   #include "RNOH/PackageProvider.h"
   + #include "GeometryViewPackage.h"
   
   using namespace rnoh;
   
   std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
       return {
   +        std::make_shared<GeometryViewPackage>(ctx)
       };
   }
   ```

   d.运行：

   点击右上角的`sync`按钮

   或者在终端执行：

   ```bash
   cd entry
   ohpm install
   ```

   然后编译、运行即可。

2. **在 ArkTS 侧引入 GeometryTransitionView**

   ```diff
     ...
   +  import { GeometryTransitionView } from '@hadss/react_native_geometry_transition';
   
    @Builder
    export function buildCustomRNComponent(ctx: ComponentBuilderContext) {
     ...
   + if (ctx.componentName === GeometryTransitionView.NAME) {
   +   GeometryTransitionView({
   +     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,
   + GeometryTransitionView.NAME
     ];
   ```

3. **在 ArkTS 侧引入 GeometryTransitionViewPackage**

   ```diff
     ...
   + import { GeometryTransitionViewPackage } from '@hadss/react_native_geometry_transition/ts';
   
   export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
     return [
       new SamplePackage(ctx),
   +   new GeometryTransitionViewPackage(ctx)
     ];
   }
   ```

   
**方法二:**

**直接链接源码**

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

## API

>**说明：**
>"Platform"列表示支持的平台，All表示支持所有平台。

**GeometryView**

| Name                | Description           | Type     | Platform    |
| ------------------- | --------------------- | -------- | ----------- |
| geometryViewID      | 共享元素ID.           | string   | OpenHarmony |
| onGeometryViewClick | 点击GeometryView回调. | callback | OpenHarmony |

**GeometryTransitionView**

| Name           | Description                                                  | Type                  | Platform    |
| -------------- | ------------------------------------------------------------ | --------------------- | ----------- |
| geometryViewID | 用于设置绑定关系，id置空字符串清除绑定关系避免参与共享行为，id可更换重新建立绑定关系。同一个id只能有两个组件绑定且是in/out不同类型角色，不能多个组件绑定同一个id。 | string                | OpenHarmony |
| isFollow       | 组件内共享元素转场动画参数。true代表跟随做共享动画，false代表不跟随做共享动画，默认值：false。 | boolean?              | OpenHarmony |
| duration       | 动画执行时间，默认值：1000ms。                               | number?               | OpenHarmony |
| pageType       | 页面标志。                                                   | 'current' \| 'target' | OpenHarmony |

**约束与限制：**

1、仅限用于原生 Navigation 路由跳转场景；

2、在使用GeometryTransitionView时，如果跳转页面前需要重置geometryViewID，需使用以下方式：
```typescript
UIManager.dispatchViewManagerCommand(
  findNodeHandle(nativeRef.current),
  'changeGeometryTransitionID',
  ['geometryTransitionID'],   // 共享元素ID
);
```

并且通过监听geometryTransitionIDChanged事件进行页面跳转，避免target页面先触发build导致一镜到底动效失效问题。
```typescript
DeviceEventEmitter.addListener('geometryTransitionIDChanged', () => {
  console.log('geometryTransitionIDChanged');
});
```