扫码加入react-native技术交流群。定期会分享react native 技术文章,移动技术干货,精彩文章技术推送。欢迎各位大牛, React Native技术爱好者加入交流!
篇幅预计阅读时间10分钟,结尾有福利~~
前些天群里有朋友说想了解如何在React Native项目中使用封装原生View组件,之前有写过一篇 Android原生层与React Native 交互的文章,感兴趣的朋友可以看一下。今天要分享的如何封装原生View组件在React Native项目中使用,实现的思路和交互的内容有部分相同,也是基于定义交互模块的基础上作为View的扩展,相信看了Android原生层与React Native 交互的文章,再了解封装原生View就很轻松了~
一、概述
在React Native中,我们已经接触了很多种丰富的组件了,例如 ScrollView、FlatList、SectionList、Button、Text、Image等等...常用的组件已经可以帮助我们实现并满足日常开发中所遇到的功能需求。但是产品经理突发奇想还是会提出各种“新鲜”的功能,一些复杂的界面实现,在RN层面变得异常棘手,所以需要我们从原生层去组建View,在RN中完成渲染。本篇内容会以实际开发的案例来引导大家学会如何在Android层封装原生UI组件。整体流程如下:
1. 创建ViewManager的子类。
2. 实现createViewInstance方法,返回UI实例。
3. 使用@ReactProp(或@ReactPropGroup)注解,注册UI组件的属性。
4. 创建视图管理包,注册ViewManager到应用程序。
5. 实现JavaScript模块。
6. android层向js层发送消息。
7. js层向android层发送消息。
二、创建
1. 创建UI模块
在封装NativeModule (原生模块) 的时候,定义了ReactContextBaseJavaModule的子类,并实现了getName、@ReactMethod注解的供RN调用的通信方法等。创建原生UI组件需要我们定义SimpleViewManager的子类,并实现getName、createViewInstance、@ReactProp注解的方法。
(1)getName:返回原生UI模块的唯一标识名称。
(2)createViewInstance:创建UI视图实例。
(3)@ReactProp:标注要注册的属性。
例如,我们要创建一个图片组件,在Android中图片组件是ImageView,那么代码如下:
public class ImageViewManager extends SimpleViewManager<ImageView>{ private ThemedReactContext mContext; private static final String GIFVIEW_MANAGER_NAME = "ImageView"; @Override public String getName() { return GIFVIEW_MANAGER_NAME; } /** * 此处创建View实例,并返回 * @param reactContext * @return */ @Override protected ImageView createViewInstance(ThemedReactContext reactContext) { this.mContext = reactContext; ImageView imageView = new ImageView(reactContext); return imageView; } @ReactProp(name = "imageName") public void setImageSrc(final ImageView image, String url) { // 加载url对应的图片 Bitmap bitmap = loadBitmap(url) image.setImageBitmap(); } }
上面代码分为三部分,分别是getName方法返回对应UI模块名称,createViewInstance返回UI实例,setImageSrc设置显示的图片。@ReactProp注解需要提供属性的名称,例如imageName,在RN中就可以通过该名称指定图片。
[注]:SimpleViewManager继承自BaseViewManager,BaseViewManager继承自ViewManager
2. 注册UI模块
和定义原生交互模块一样,需要我们定义ReactPackage的子类,即包管理类,并将其添加。
package com.xxx.gifview; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Created by songlcy on 2018/4/16. */ public class ImageViewPackage implements ReactPackage{ @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<Class<? extends JavaScriptModule>> createJSModules() { return Collections.emptyList(); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Arrays.<ViewManager>asList(new ImageViewManager()); } }
在创建交互模块时,我们是把ReactContextBaseJavaModule的子类,即模块类注册到了createNativeModules方法中,而UI模块是需要注册到createViewManager方法中。
3. 将包管理类添加到Application的getPackages:
@Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new ImageViewPackage(), ); }
以上步骤,我们就在原生层完成了对原生UI的封装创建。接下来,就可以在RN中获取,并作为Component来渲染到视图上了。
4. RN层调用原生UI组件
import React, {Component} from 'react'; import { requireNativeComponent } from 'react-native'; let RCTGIFImageView = requireNativeComponent('GIFImageView', RCTGIFView); class RCTGIFView extends Component { render() { return <RCTGIFImageView {...this.props}/> } } module.exports = RCTGIFView;
(1)导入requireNativeComponent
(2)使用requireNativeComponent加载原生UI视图,第一个参数即原生层getName返回的名称,第二个参数表示应用到哪个组件上
/** * Songlcy * 2018-04-17 */ import React, { Component } from 'react'; import { Platform, StyleSheet, Text, View } from 'react-native'; import RCTGIFView from './RCTGIFView'; export default class App extends Component { constructor(props) { super(props); this.state = { isPlaying: true, image: '图片地址' } } render() { return ( <View style={ styles.container }> <RCTGIFView style={ styles.image } imageName={ this.state.image } /> </View> ); } }
以上就是在RN中直接调用原生UI的流程,相信大家都可以很轻松的看懂。经过以上流程,将原生UI封装给RN使用,就轻松的搞定了。
三、进阶
第二部分我们码文综合描述了如何构建原生UI模块,下面介绍如何实现RN和原生模块之间的通信交互。既然是通信交互,那么肯定是双向的,即Android原生层主动或者被动方式,RN层作为主动或者被动方式。
为了模拟通信交互,在原来的代码上进行扩展,我们为原生层UI注册一个onClick单击事件,点击后,将信息传递给RN层。
1. Android原生层向RN层传递数据
(1)注册事件类型,事件名称
声明事件,需要重写SimpleViewManager的getExportedCustomDirectEventTypeConstants方法:
/** * 自定义事件 * @return */ @Nullable @Override public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of(EVENT_NAME_ONCLICK,MapBuilder.of("registrationName", EVENT_NAME_ONCLICK)); }
在注册自定义事件时,需要提供事件的名称,例如上述代码中的EVENT_NAME_ONCLICK:
private static final String EVENT_NAME_ONCLICK = "onClick";
(2)在事件中传递数据到RN层
/** * 此处创建View实例,并返回 * @param reactContext * @return */ @Override protected ImageView createViewInstance(ThemedReactContext reactContext) { this.mContext = reactContext; this.mContext.addLifecycleEventListener(this); final ImageView imageView = new ImageView(reactContext); imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 创建数据传递信使,类似于Android中的Bundle WritableMap data = Arguments.createMap(); data.putString("msg","点击了图片");// key用于在RN中获取传递的数据 mContext.getJSModule(RCTEventEmitter.class).receiveEvent( imageView.getId(), // RN层原生层根据id绑定在一起 EVENT_NAME_ONCLICK, // 事件名称 data // 传递的数据 ); } }); return imageView; }
上述代码中,为原生UI(ImageView)注册了单击事件,在单击事件中,我们做了两件事:
* 创建要传递的数据对象
* 将数据传递到RN层
将原生层注册完毕后,我们转到RN层,使用刚刚自定义的事件
import React, {Component} from 'react'; import { requireNativeComponent } from 'react-native'; let RCTGIFImageView = requireNativeComponent('GIFImageView', RCTGIFView, { nativeOnly: { onClick: true } }); class RCTGIFView extends Component { /** * 单击事件 */ onClick(event) { if(this.props.onClick) { if(!this.props.onClick){ return; } // 使用event.nativeEvent.msg获取原生层传递的数据 this.props.onClick(event.nativeEvent.msg); } } render() { return <RCTGIFImageView {...this.props} onClick={ (event)=> this.onClick(event) } /> } } module.exports = RCTGIFView;
可以看到上述代码中,在requireNativeComponent的第三个参数声明为nativeOnly。有时候你的原生组件有一些特殊的属性希望导出,但并不希望它成为公开的接口。举个例子,Switch组件可能会有一个onChange属性用来传递原始的原生事件,然后导出一个onValueChange属性,这个属性在调用的时候会带上Switch的状态作为参数之一。这样的话你可能不希望原生专用的属性出现在API之中,也就不希望把它放到propTypes里。可是如果你不放的话,又会出现一个报错。解决方案就是带上额外的nativeOnly参数。
在组件声明了事件后,只需要在使用组件的地方调用即可:
<RCTImageView style={ styles.gifImage } playStatus={ this.state.isPlaying } imageName={ this.state.gifImage } onClick={ (msg)=> alert('原生层传递的数据为:', msg) } />
2. RN层向Android原生层层传递数据
在某些需求场景下,会需要RN层主动发出命令,让原生层封装的UI模块处理一些任务。这时候就需要RN层向原生层发送交互通知。实现过程如下三步:
(1)定义交互方法名称,交互命令ID
private static final String HANDLE_METHOD_NAME = "handleTask"; // 交互方法名 private static final int HANDLE_METHOD_ID = 1; // 交互命令ID
(2)原生层重写getCommandsMap方法,接收RN层发来的通知
/** * 接收交互通知 * @return */ @Nullable @Override public Map<String, Integer> getCommandsMap() { return MapBuilder.of(HANDLE_METHOD_NAME,HANDLE_METHOD_ID); }
getCommandsMap可以接收多组交互通知,每组通知需要包括名称(RN层调用的方法名)和命令ID
(3)重写receiveCommand方法,根据RN层发送的对应通知ID,处理对应任务请求
/** * 根据命令ID,处理对应任务 * @param root * @param commandId * @param args */ @Override public void receiveCommand(ImageView root, int commandId, @Nullable ReadableArray args) { switch (commandId){ case HANDLE_METHOD_ID: if(args != null) { String name = args.getString(0);//获取第一个位置的数据 Toast.makeText(mContext, "收到RN层的任务通知,开始在原生层处理任务...", Toast.LENGTH_SHORT).show(); } break; default: break; } }
(4)RN层发送任务通知
import { UIManager, findNodeHandle } from 'react-native'; sendNotification() { //向native层发送命令 UIManager.dispatchViewManagerCommand( findNodeHandle(this.refs.RCTImageView), UIManager.RCTImageView.Commands.handleTask, // Commands后面的值与原生层定义的HANDLE_METHOD_NAME一致 [name] // 向原生层传递的参数数据,数据形如:["第一个参数","第二个参数",3] ); } <RCTImageView ref='RCTImageView' style={ styles.gifImage } playStatus={ this.state.isPlaying } imageName={ this.state.gifImage } onClick={ (msg)=> alert('原生层传递的数据为:', msg) } />
UIManager.dispatchViewManagerCommand()方法用于向原生层发送通知命令,该方法接收三个参数:
(1)组件引用,即声明的ref名称,并使用findNodeHandle处理
(2)UIManager.UI模块名.Commands.命令名,即在原生层定义的命令名称,UI模块名与getName方法返回的名称一致
(3)向原生层传递的数据
噗噗,终于要结尾了。整篇内容通过一个简单的实例来向大家讲解如何在React Native 封装android原生UI组件的开发流程,包括定义UI模块、交互逻辑等等。希望能帮助大家解决实际开发中遇到的问题,源码已经开放到GitHub,并且也基于该功能,封装了一个Gif图插件,支持播放、暂停,后期还会不断更新,感兴趣的童靴可以fork,更重要的是star哈~ 链接如下: