この記事の目的
React NativeでもNativeのUIを触りたいときってありますよね。
そこに対してもReact Nativeでサポートしているのですが、案の定公式の説明やコード例が十分でない&日本語の資料もなさそうなのでまとめてみました。
※ 公式が不十分でソースコードを読みつつの理解となるため、正しくない内容になってしまうかもしれない点をご了承ください。(そもそも正解がない)
※ Objective-C/Swift 版はこちらをどうぞ→http://qiita.com/uryyyyyyy/items/889a3e519cd993a0fb4a
※ ソースコードはこちら→https://github.com/uryyyyyyy/RNBindingSample/tree/android-native-component
ゴール
- React Nativeから、既に定義された独自クラス(Viewを継承しているもの)を表示できる
- プロパティ値などをreact側で制御できる
- イベントの発火をjs側で扱える
擬似コードで書くとこんな感じが理想。
function _onChange(data) {
//use `data` object
}
<CustomUIView
style={styles.hoge}
value={this.props.value}
onChange={_onChange}
/>
作り方
-
BaseViewManager
を継承したラッパー(Manager)を用意し、そこでJSから渡されるデータを制御する。 - Managerをpackageで包み、それをMainActivityに食わせることで有効化する。
- JS側で、上記独自UIViewをJSで扱うためのUtilityクラスを定義する。
という流れです。簡単ですね!
(なお、iOS版を書いた後にAndroidを触ってるのですが、iOS版ももっと簡単にかけたかもしれないです。。)
試しに作ってみた
公式にある例がわかりにくかったので、ここでは、EditTextとTextViewを例に上げます。
ゴール
- React NativeからTextViewとEditTextを表示できる
- EditTextで値を入力するとその変更がJSで検知できる。
- propsを動的に変えることで、TextViewの表示が切り替わる
画面イメージはこんな感じです。普通ですね。。
とりあえずロジック抜きでTextViewを呼び出してみる
まずは一番簡単な実装を見ていきます。ManagerでTextViewを呼び出します。
package com.rnbindingsample.slider;
import android.util.Log;
import android.widget.TextView;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
//①
public class MyTextViewManager extends SimpleViewManager<TextView> {
private static final String REACT_CLASS = "MyTextView";
//②
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected TextView createViewInstance(ThemedReactContext context) {
return new TextView(context);
}
//③
@ReactProp(name = "value")
public void setValue(TextView view, String value) {
view.setText(value);
}
}
①でSimpleViewManagerを呼び出しています。こいつはどうやら、Styleで適用するものをそのまま活かしつつ描画してくれるようです。特にこだわりがなければこれを使えば良さそうです。
②ではgetNameをoverrideしています。これは、JSで呼ばれる時の識別子を指します。
③Reactのpropsで渡されたvalueが変化するたびにこれが呼ばれます。言うまでもないですが、極力重い処理はすべきでありません。プロパティの書き換えのみに徹しましょう。
次にこれをpackage化します。
package com.rnbindingsample.slider;
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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class URYPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
//new MySliderManager(),
new MyTextViewManager()
//new MyEditTextManager()
);
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return new ArrayList<>();
}
}
色々ありますが基本はコピペで済みます。
MySliderManager
などが見えるところがありますが、ここだけ変更すればいいだけです。Managerの追加も簡単ですね。(MyEditTextManager
は後で出てきます。)
最後にpackageを読み込みます。
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new URYPackage()
);
}
};
既にMainReactPackageが居ると思うので、自身で定義したPackageを追加するだけです。
これでJava側は完了です。最後にJSを見ましょう。
import { requireNativeComponent } from 'react-native';
export default requireNativeComponent('MyTextView');
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import MyTextView from './MyTextView'
export default class RNBindingSample extends Component {
state = {text: "hello world"};
render() {
return (
<View>
//①
<MyTextView value={this.state.text} style={{height: 50, width: 800}} />
</View>
);
}
}
AppRegistry.registerComponent('RNBindingSample', () => RNBindingSample);
特に説明は要らないと思いますが一点だけ。
①にあるようにstyleを指定しないと何も表示されません。
これは、SimpleViewManagerを継承したクラスを使って描画する際に、どのくらい描画領域を確保していいかわからないためだと思われます。
(Styleの他にも設定方法はあるのですが、それは僕もあまり理解してないので省略。。。
一応Sliderでのサンプルを置いてありますが、v0.40.0でまたちょっとAPIが変わったようです。。。)
イベントを扱えるようにしてみる
EditTextの値が変わるたびにイベントを発火させてみます。
package com.rnbindingsample.slider;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.widget.EditText;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.util.Map;
public class MyEditTextManager extends SimpleViewManager<EditText> {
private static final String MY_EVENT_NAME = "myOnChangeEvent";
private static final String REACT_CLASS = "MyEditText";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected EditText createViewInstance(ThemedReactContext context) {
final EditText view = new EditText(context);
//①
view.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(final Editable s) {
//②
ReactContext reactContext = (ReactContext) view.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
new Event(view.getId()) {//③
@Override
public String getEventName() {
return MY_EVENT_NAME;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
WritableMap eventData = Arguments.createMap();//④
eventData.putString("value", s.toString());
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), eventData);
}
});
}
});
return view;
}
@ReactProp(name = "value")
public void setValue(EditText view, String value) {
view.setText(value);
}
@Override//⑤
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(MY_EVENT_NAME, MapBuilder.of("registrationName", "myChange"));
}
}
長いですが付いてきてください。。。
①でEditTextにイベントリスナを貼り付けます。今回は値が変わった後のイベントだけ扱います。
②でReact NativeのAPIを使ってdispatcherを取得し、イベントを投げつけます。
③でEventを書いていますが、一意のIDを持たせて判別を行うようです。また、getEventNameとありますが、これは⑤で定義しているMapperのようなもので、myOnChangeEventというイベントをdispatchしたら、js側でmyChangeという名前の関数が立ち上がるようになってます(なんでこんな面倒なんだろう??)
④では、dispatch時に渡す値を設定しています。valueの中にEditTextの文字列が入ることがわかりますね。わかりますよね。
これをPackageに登録したら、あとはJS側だけです。
<MyEditText value=""
style={{height: 100, width: 800}}
myChange={(event) => console.log(event.nativeEvent.value)} />
ここで、event.nativeEventが上記WritableMapの中身です。なのでvalueプロパティの中にはstring値が入ってるはずですね。
あとはこれらを組み合わせれば、上記のgifのようなUIを作ることができます。詳しくはリポジトリをどうぞ
まとめ
React Nativeを導入するときに、「JSで書くと、これまでのViewの資産とか使えなくてツライでしょ?」などと言われて凹んでいた貴方。こんな感じで書けますよ。オススメです。
React Nativeで優勝(?)