1. 程式人生 > >React Native與Android的互動

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可以實際需要新增。