【入门到精通】鸿蒙next开发:RN OpenHarmony特征&多设备适配最佳实践

往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)


RN OpenHarmony特征&多设备适配最佳实践

1. 概述

现有的伙伴应用使用RN框架开发的历史页面多且冗杂。为达到HarmonyOS系统一多体验,所有页面若均使用ArkUI框架重新开发耗时长、成本高,不可行。针对此问题,本文将主要提供一套RN多设备响应式组件及方案:

1)一套基于RN的鸿蒙特征动画组件库,在RN页面实现鸿蒙特征动画UI效果;

2)一套基于RN的一多高阶组件库,在RN页面实现折叠屏悬停避让分栏的UI效果;

首先介绍组件及效果说明,再分别结合组件效果提供对应场景的开发案例,最终提供示例代码指导实际开发。

组件库安装

参考@hadss/react_native_adaptive_layout 和 @hadss/react_native_geometry_transition

2. 多设备断点

2.1 断点设计原理

RN断点是基于鸿蒙多设备封装的一套断点机制,通过设置断点,让开发者可以结合窗口宽度去实现不同的页面布局效果。

断点以应用窗口宽度为基准,将应用窗口在宽度维度上分成了几个不同的区间即不同的断点,默认提供的断点区间如下所示。

断点名称

取值范围(px

xs

[0, 320)

sm

[320, 600)

md

[600, 840)

lg

[840, 1440)

xl

[1440, +∞)

2.2 多设备适配指导

在实际开发过程中,可以使用@hadss/react_native_adaptive_layout的setBreakpoints自定义断点的区间,也可以使用上述默认的断点区间。使用useBreakpointValue时,只需将屏幕断点所对应的参数传入useBreakpointValue,当屏幕断点发生变化时,该hook会根据当前断点的类型返回所对应的数据。

具体示例如下所示:

自定义断点区间

// 自定义断点区间,可选 
  useEffect(() => { 
    setBreakpoints({ 
      base: 320, 
      md: 768, 
      lg: 1024, 
    }); 
  });

使用断点hook并获取不同断点下的属性值

 const color = useBreakpointValue({ 
    base: 'red', 
    xs: 'blue', 
    sm: 'green', 
    md: 'yellow', 
    lg: 'purple', 
    xl: 'orange', 
  }); 
 
  return ( 
    <Text style={{ color }}>Responsive Color Text</Text> 
  );

3. RN鸿蒙特征动画组件

3.1 组件使用说明

@hadss/react_native_geometry_transition鸿蒙特征动画组件GeometryView,基于ArkUI的geometryTransition接口,实现了鸿蒙特征动画效果,详细介绍可参考:使用geometryTransition共享元素转场

组件导入方式

​import GeometryView from '@hadss/react_native_geometry_transition/src/';

组件API

名称

类型

必填

说明

geometryViewID

string

设置转场动效ID

onGeometryViewClick

DirectEventHandler

点击回调函数

3.2 场景案例

点击歌单页当前播放音乐控件,跳转音乐播放页,触发一镜到底的转场效果。

33.gif

3.2.1 关键代码片段

1、将歌单页中的当前播放音乐控件设置共享元素ID,监听点击事件并发送消息至ArkUI侧。(SampleTurboModule为自定义TurboModule,执行调用ArkUI侧发送消息方法)

 return ( 
        <GeometryView 
            style={styles.container} 
            geometryViewID={'test'}   // 设置共享元素ID 
            onGeometryViewClick={() => { 
                SampleTurboModule.pushStringToHarmony('pages/MusicPlay', 1); 
            }}> 
            <Image source={require('../../../../asset/cover.png')} style={[styles.albumCover, { marginLeft: itemLeft }]} /> 
            <View style={styles.songInfo}> 
                <Text style={styles.songTitle}>{song.title}</Text> 
                <Text style={styles.songArtist}>{song.artist}</Text> 
            </View> 
            <View style={{ flex: 1 }} /> 
            {controlIcons} 
        </GeometryView> 
    );

2、ArkUI侧监听跳转事件,在animateTo闭包内执行页面跳转逻辑

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

3、音乐播放页设置与歌单页一致的共享元素ID

@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) 
      } else if (this.instance) { 
        BaseRN({ 
          rnInstance: this.instance, 
          moduleName: this.moduleName, 
          bundlePath: this.bundlePath, 
        }).align(Alignment.Top) 
      } 
    } 
    .geometryTransition('test')  // 设置共享元素ID 
    .hideTitleBar(true) 
  } 
}

4. RN折叠屏适配组件

4.1 组件使用说明

RN折叠屏适配组件FoldSplitContainer,包含primary、secondary、extra三个区域,实现折叠屏二分栏、三分栏在展开态、悬停态以及折叠屏悬停状态下折痕区避让,详情参考:@hadss/react_native_adaptive_layout

组件导入方式

import { FoldSplitContainer } from '@hadss/react_native_adaptive_layout/src';

FoldSplitContainer组件

Name

Description

Type

Platform

primary

主要区域回调函数。

function

OpenHarmony

secondary

次要区域回调函数。

function

OpenHarmony

extra

扩展区域回调函数,不传入的情况,没有对应区域。

function

OpenHarmony

expandedLayoutOptions

展开态布局信息。

ExpandedRegionLayoutOptions

OpenHarmony

hoverModeLayoutOptions

悬停态布局信息。

HoverModeRegionLayoutOptions

OpenHarmony

foldedLayoutOptions

折叠态布局信息。

FoldedRegionLayoutOptions

OpenHarmony

onHoverStatusChange

折叠屏进入或退出悬停模式时触发的回调函数。

onHoverStatusChangeHandler

OpenHarmony

ExpandedRegionLayoutOptions

Name

Description

Type

Platform

isExtraRegionPerpendicular

扩展区域是否从上到下贯穿整个组件,当且仅当extra有效时此字段才生效。默认值:true。

boolean

OpenHarmony

verticalSplitRatio

主要区域与次要区域之间的高度比例。默认值:PresetSplitRatio.LAYOUT_1V1。

number

OpenHarmony

horizontalSplitRatio

主要区域与扩展区域之间的宽度比例,当且仅当extra有效时此字段才生效。默认值:PresetSplitRatio.LAYOUT_3V2。

number

OpenHarmony

extraRegionPosition

扩展区域的位置信息,当且仅当isExtraRegionPerpendicular = false有效时此字段才生效。默认值:ExtraRegionPosition.top。

ExtraRegionPosition

OpenHarmony

HoverModeRegionLayoutOptions

Name

Description

Type

Platform

showExtraRegion

可折叠屏幕在半折叠状态下是否显示扩展区域。默认值:false。

boolean

OpenHarmony

horizontalSplitRatio

主要区域与扩展区域之间的宽度比例,当且仅当extra有效时此字段才生效。默认值:PresetSplitRatio.LAYOUT_3V2。

number

OpenHarmony

extraRegionPosition

扩展区域的位置信息,当且仅当showExtraRegion时此字段才生效。默认值:ExtraRegionPosition.top。

ExtraRegionPosition

OpenHarmony

FoldedRegionLayoutOptions

Name

Description

Type

Platform

verticalSplitRatio

主要区域与次要区域之间的高度比例。默认值:PresetSplitRatio.LAYOUT_1V1。

number

OpenHarmony

onHoverStatusChangeHandler

Name

Description

Type

Platform

callback

折叠屏进入或退出悬停模式时触发的回调函数。

(status: HoverModeStatus) => void

OpenHarmony

HoverModeStatus

Name

Description

Type

Platform

foldStatus

设备的折叠状态。

FoldStatus

OpenHarmony

isHoverMode

app当前是否处于悬停态。

boolean

OpenHarmony

ExtraRegionPosition

Name

Description

Value

top

扩展区域在组件上半区域。

1

bottom

扩展区域在组件下半区域。

2

PresetSplitRatio

Name

Description

Value

LAYOUT_1V1

1:1比例。

1/1

LAYOUT_3V2

3:2比例。

3/2

LAYOUT_2V3

2:3比例。

2/3

推荐使用方式

const primaryRender = () => ( 
    <View> 
      <Text> 此区域为primary 
    </View> 
  ); 
  const secondRender = () => ( 
    <View> 
      <Text> 此区域为second 
    </View> 
  ); 
  const extraRender = () => ( 
    <View> 
      <Text> 此区域为extra 
    </View> 
  ); 
   
  const expandedLayoutOptions: ExpandedRegionLayoutOptions = { 
    isExtraRegionPerpendicular: true, 
    verticalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
    horizontalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
  }; 
  const foldedRegionLayoutOptions: FoldedRegionLayoutOptions = { 
    verticalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
  }; 
  const hoverModeLayoutOptions: HoverModeRegionLayoutOptions = { 
    horizontalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
    showExtraRegion: true, 
  }; 
  <FoldSplitContainer 
    primary={primaryRender()} 
    secondary={secondRender()} 
    extra={extraRender()} 
    expandedLayoutOptions={expandedLayoutOptions} 
    foldedLayoutOptions={foldedRegionLayoutOptions} 
    hoverModeLayoutOptions={hoverModeLayoutOptions} 
  />

4.2 场景案例

4.2.1 实现效果

fold

expanded

hover

34.png

35.png

36.png

4.2.2 关键代码片段

这段代码是用React Native编写的,用于构建一个音乐播放器界面:

1、secondRender函数:

  • 这个函数返回一个渲染组件,该组件包含多个View和Image组件,用于显示音乐的封面、标题、艺术家名、播放进度条、控制按钮(如上一曲、下一曲、播放/暂停、重复等)和一些额外的图标。

  • View组件用于布局,Image组件用于显示图标或音乐封面。
  • Slider组件用于显示播放进度条,用户可以通过拖动来改变播放位置。
  • TouchableOpacity组件用于创建可点击的图标。

2、extraRender函数:

  • 这个函数用于显示歌词信息。

3、布局选项:

  • expandedLayoutOptions、foldedRegionLayoutOptions和hoverModeLayoutOptions定义了不同的布局选项,用于控制组件在展开、折叠和悬停模式下的布局。

4、返回的组件:

  • 最后,代码返回一个ImageBackground组件,该组件使用一个模糊背景图片,并在其上层叠加FoldSplitContainer组件。FoldSplitContainer组件使用前面定义的primaryRender、secondRender和extraRender函数来渲染其主要、次要和额外的内容部分。

代码示例如下:

const secondRender = () => ( 
    <View style={{flex: 1, alignItems: 'center'}}> 
      <View style={styles.message}> 
        <View> 
          <Text style={styles.title}>{title}</Text> 
          <Text style={styles.artist}>{artist}</Text> 
        </View> 
        <Image 
          source={require('../../../asset/likes.svg')} 
          style={styles.imageGrey} 
        /> 
      </View> 
      <View style={styles.slider}> 
        <Slider 
          style={{width: '100%'}} 
          minimumValue={0} 
          maximumValue={duration} 
          value={position} 
          minimumTrackTintColor="#e8e1e1" 
          maximumTrackTintColor="#784949" 
          thumbStyle={{opacity: 0}} 
          onValueChange={(val: number) => { 
            seekTo(val); 
          }} 
        /> 
      </View> 
      <View style={styles.controls}> 
        <Text style={styles.text}>{formatTime(position)}</Text> 
        <Text style={styles.text}>{formatTime(duration)}</Text> 
      </View> 
      <View style={styles.container}> 
        <Image 
          source={require('../../../asset/repeat.svg')} 
          style={styles.imageGrey} 
        /> 
        <TouchableOpacity onPress={skipToPrevious}> 
          <Image 
            source={require('../../../asset/left.svg')} 
            style={styles.image} 
          /> 
        </TouchableOpacity> 
        <TouchableOpacity onPress={togglePlayPause}> 
          <Image 
            source={ 
              playState === State.Playing 
                ? require('../../../asset/pause.svg') 
                : require('../../../asset/play.svg') 
            } 
            style={styles.imagePlay} 
          /> 
        </TouchableOpacity> 
        <TouchableOpacity onPress={skipToNext}> 
          <Image 
            source={require('../../../asset/forward_end_fill.svg')} 
            style={styles.image} 
          /> 
        </TouchableOpacity> 
        <Image 
          source={require('../../../asset/music_note_list.svg')} 
          style={styles.imageGrey} 
        /> 
      </View> 
      <View style={styles.container}> 
        <Image 
          source={require('../../../asset/share_play.svg')} 
          style={styles.imageGrey} 
        /> 
        <Image 
          source={require('../../../asset/bell.svg')} 
          style={styles.imageGrey} 
        /> 
        <Image 
          source={require('../../../asset/arrow_down_circle.svg')} 
          style={styles.imageGrey} 
        /> 
        <Image 
          source={require('../../../asset/dot.svg')} 
          style={styles.imageGrey} 
        /> 
      </View> 
    </View> 
  ); 
  const extraRender = () => ( 
    <View style={styles.extra}> 
      <Text 
        style={{ 
          marginTop: isHover ? 70 : 0, 
          marginRight: isPad ? 200 : 0, 
          fontSize: 23, 
          color: '#ffffff', 
        }}> 
        此歌曲为纯音乐,请您欣赏 
      </Text> 
    </View> 
  ); 
  const expandedLayoutOptions: ExpandedRegionLayoutOptions = { 
    isExtraRegionPerpendicular: true, 
    verticalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
    horizontalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
  }; 
  const foldedRegionLayoutOptions: FoldedRegionLayoutOptions = { 
    verticalSplitRatio: PresetSplitRatio.LAYOUT_1V1, 
  }; 
  const hoverModeLayoutOptions: HoverModeRegionLayoutOptions = { 
    horizontalSplitRatio: 0.66, 
    showExtraRegion: true, 
  }; 
  return ( 
    <ImageBackground 
      source={require('../../../asset/blur.png')} 
      style={{width: '100%', height: '100%'}}> 
      <View style={{position: 'absolute', width: '100%', alignItems: 'center'}}> 
        <FoldSplitContainer 
          primary={primaryRender()} 
          secondary={secondRender()} 
          extra={extraRender()} 
          expandedLayoutOptions={expandedLayoutOptions} 
          foldedLayoutOptions={foldedRegionLayoutOptions} 
          hoverModeLayoutOptions={hoverModeLayoutOptions} 
        /> 
    </ImageBackground> 
  );

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值