React Native與Android的互動
在使用RN進行跨平臺開發的過程中,經常會設計到跨平臺呼叫相關的內容,而在於RN進行互動的時候,最核心的就是RN提供的Component和Module。 其中,Component是專門將Native的UI暴露出來供JS呼叫的,而Native Module則是將Native的模組暴露出來供JS呼叫的,其用途不一樣。在實戰開發中,由於RN實現的成本比較大,或者沒辦法實現,而原生是非常容易實現的,這時候就想到了自定義元件。
Component
例如,下面是一個自定義的View原生程式碼:
public class MyCustomView extends View {
private Paint mPaint;
public MyCustomView(ReactContext context) {
super(context);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(0xffff0000);
}
public void setColor(int color){
mPaint.setColor(color);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(300, 300);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
}
}
然後,建立一個ViewManager共RN呼叫,ViewManager需要繼承自SimpleViewManager或者ViewGroupManager,如果RN有預設的屬性,可以在原生端使用ReactProp註解來給原生設定屬性值。
public class MyCustomViewManager extends SimpleViewManager<MyCustomView> {
protected static final String REACT_CLASS = "MyCustomView";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected MyCustomView createViewInstance(ThemedReactContext reactContext) {
return new MyCustomView(reactContext);
}
// 設定屬性共RN呼叫,名稱需要和RN的屬性對應
@ReactProp(name = "color")
public void setColor(MyCustomView view, String color) {
view.setColor(Color.parseColor(color));
}
}
然後建立一個ReactPackage,並將我們自定義的ViewManager新增到createViewManagers中。
public class CustomReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
//自定義的ViewManager都可以加在這裡。
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new MyCustomViewManager()
);
}
}
然後將我們自定義的ReactPackage在Application中註冊。
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CustomReactPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
為了方便其他RN開發人員呼叫,我們將原生元件封裝成RN元件後再匯出。
import React, {
Component,
PropTypes,
} from 'react';
import {
requireNativeComponent,
View,
UIManager,
} from 'react-native';
const ReactNative = require('ReactNative'); // ReactNative通過import沒用
export default class MyCustomView extends Component{
constructor(props){
super(props)
}
render(){
// {...this.props} 一定需要設定,不讓你永遠也看不到
return(
<RCTMyCustomView
{...this.props}
</RCTMyCustomView>);
}
}
MyCustomView.propTypes = {
color: PropTypes.string, // 設定color屬性
...View.propTypes, // 這裡一定需要設定,不然會報錯
};
var RCTMyCustomView = requireNativeComponent('MyCustomView', MyCustomView);
需要注意的是,如果報ReactNative找不到的錯誤,請修改ReactNative的匯入方式為:
import ReactNative from 'react-native';
到此,我們就可以在需要使用的地方使用該元件了,注意給MyCustomView設定大小。
<MyCustomView
color='#00ff00'
style={{width:300, height:300}}
/>
這樣就完成了JS呼叫原生元件的功能,但是在實戰開發中,經常會設計的原生和js事件相關的傳遞,資料的傳遞可以參考我之前的文章:React Native原生模組向JS傳遞資料的幾種方式
在與原生進行事件傳遞時,如果JS要給Native傳送事件,Native需要藉助getCommandsMap()和receiveCommand()來處理JS向Native傳送事件邏輯。如果Native要給JS傳遞事件,可以使用getExportedCustomDirectEventTypeConstants()和addEventEmitters()。
private static final int CHANGE_COLOR = 1;
/**
* 可以接收的JS發過來的事件,返回來的資料是一組對應了方法名以及方法對應的一個ID(這個ID需要唯一區分)的Map。
*/
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("changeColor", CHANGE_COLOR);
}
/**
* 接收JS事件以後的處理。JS會通過一些傳送傳送相應的指令過來,Native會由receiveCommand來處理。事件過來時才會執行。
*/
@Override
public void receiveCommand(MyCustomView root, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case CHANGE_COLOR:
root.changeColor();
break;
}
}
/**
* 暴露了在JS中定義的方法,例如下面的"onChangeColor"是定義在JS中的方法。
*
* Returned map should be of the form:
* {
* "onTwirl": {
* "registrationName": "onTwirl"
* }
* }
*/
@Nullable
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put("changeColor", MapBuilder.of("registrationName", "onChangeColor"))
.build();
}
/**
* 發射入口,相當於將Native的一些事件也註冊給JS。
*/
@Override
protected void addEventEmitters(final ThemedReactContext reactContext, final MyCustomView view) {
super.addEventEmitters(reactContext, view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
.dispatchEvent(new ClickEvent(view.getId()));
}
});
}
在上面的程式碼中可以看到Native會接受一個1(CHANGE_COLOR)的指令以及會回撥一個onChangeColor的方法到JS。RN端藉助UIManager.dispatchViewManagerCommand即可將事件傳送給原生端,如果傳送給module則使用 NativeModules.UIManager.dispatchViewManagerCommand(),下面是完整程式碼:
const ReactNative = require('ReactNative');
const CUSTOM_VIEW = "custom_view";
export default class MyCustomView extends Component{
constructor(props){
super(props)
//繫結事件
this._onChange = this._onChange.bind(this);
}
// 把事件給Native
_changeColor() {
let self = this;
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(self.refs[CUSTOM_VIEW]),
1, // 傳送的commandId為1
null
);
}
_onChange() {
if (!this.props.handleClick) {
return;
}
this.props.handleClick();
}
render(){
return(
<RCTMyCustomView
ref={CUSTOM_VIEW}
{...this.props}
onChangeColor={() => this._onChange()}>
</RCTMyCustomView>);
}
}
MyCustomView.propTypes = {
handleClick: PropTypes.func,
color: PropTypes.string,
...View.propTypes,
};
var RCTMyCustomView = requireNativeComponent('MyCustomView', MyCustomView, {
nativeOnly: {onChangeColor: true}
});
注意,ReactNative的匯入方式,隨著版本的不一樣,匯入的方式也不一樣。 注意上面用到了nativeOnly,有時候有一些特殊的屬性,想從原生元件中匯出,但是又不希望它們成為對應React封裝元件的屬性,這時候就可以使用nativeOnly。下面就可以愉快的使用了:
<MyCustomView
ref='view'
color='#00ff00'
handleSizeClick={() => this._handleSizeClick()}
handleClick={() => this._handleClick()}
style={{width:300, height:300}} />
NativeModule
NativeModule是用來定義Native模組供JS呼叫的。這樣的場景會比較的多,比如Toast,在JS中沒有Toast這類東西,但是Android/IOS中卻很常見。例如,我們需要呼叫原生的Toast。
@ReactModule(name = "DemoToast")
public class DemoToastModule extends ReactContextBaseJavaModule {
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public DemoToastModule(ReactApplicationContext reactContext){
super(reactContext);
}
// Module的名稱
@Override
public String getName() {
return "DemoToast";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
/**
* 通過Callback回撥到JS
*/
@ReactMethod
public void show(String message, int duration, Callback callback) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
callback.invoke("Egos");
}
}
其中,ReactMethod標示的show是給Js呼叫的,該方法會通過callback回撥資訊給Js。 然後,我們使用NativeModules將Native module轉化成具體的JS元件,並匯出以供第三方呼叫。
import { NativeModules } from 'react-native';
RCTDemoToast = NativeModules.DemoToast;
var DemoToast = {
SHORT: RCTDemoToast.SHORT,
LONG: RCTDemoToast.LONG,
show(message, duration){
RCTDemoToast.show(message, duration, (msg) => {
var str = msg;
});
}
};
module.exports = DemoToast;
然後在第三方使用即可,如果涉及到事件相關的內容。可以使用下面的方式。例如:
componentWillMount() {
DeviceEventEmitter.addListener('testMethod', (event) => {var s = event;} );
}
當Native程式碼執行sendEvent指令時,就會在RN中執行上面程式碼。
WritableMap params = Arguments.createMap();
params.putString("xzh","Egos");
sendEvent(getReactApplicationContext(), "testMethod", params);
/**
* 也可以直接傳送事件給JS程式碼
*/
private void sendEvent(ReactContext reactContext,
String eventName, @Nullable WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
引數WritableMap可以實際需要新增。