React-Native之Android:封裝原生UI組件

參考官方文檔修改而來:http://facebook.github.io/react-native/docs/native-components-android.html#content
中文版:http://reactnative.cn/docs/0.23/native-component-android.html#content
官方文檔以ImageView為樣例,這里以android原生最常見的TextView的做一下簡單封裝示例,官方組件中已有關于原生TextView的更完整的封裝,需要的可以去看。
這里封裝的前提是已經有一個可以正常運行的RN工程。
建議添加Java代碼時用AS來打開android工程,添加JS代碼時用IDEA。這樣都會有智能提示,比較方便。
封裝Android原生視圖的基本步驟:
1.創建一個ViewManager的子類,并實現必需方法。
2.創建自己的ReactPackage,并將1中創建的ViewManager的子類添加到其中;再將自己的ReactPackage添加到工程里的ReactActivity。
3.在1中創建的ViewManager子類中導出視圖的屬性設置器:使用@ReactProp
(或@ReactPropGroup)注解。
4.實現JS模塊。
5.在JS里使用封裝的原生UI。
6.注冊原生事件
下面為每一步作詳細解釋并輔以代碼。
1.創建一個ViewManager的子類,并實現必需方法。

public class MyTextViewManager extends SimpleViewManager<TextView> {
    @Override
    public String getName() {
        return "MyTextView";
    }
    @Override
    protected TextView createViewInstance(ThemedReactContext reactContext) {
        TextView textView = new TextView(reactContext);
        return textView;
}

必需的方法為getName和createViewInstance兩個方法。看名字就知道一個是返回創建的view名字,一個是創建view實例。
這里選擇的是SimpleViewManager,還有個BaseViewManager.兩者的區別就類似我們在android中經常用的SimpleAdapter和BaseAdapter一樣。
2.逐步注冊ViewManger。
先創建自己的ReactPackage,并將1中創建的ViewManager的子類添加到其中。

public class MyReactPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Arrays.<NativeModule>asList(
                new MyToastAndroid(reactContext)
        );
    }
    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new PieChartViewManager(),
                new MyTextViewManager()
        );
    }
}

當實現ReactPackage時,需要實現這三個方法,學過前面的的導入原生模塊部分的同學應該很熟悉了。封裝的原生模塊放在createNativeModules里,如上面的MyToastAndroid; 封裝的原生UI組件放在createViewManagers里。需要注意的是剩下的最后一個方法createJSModules里默認是返回null,要改成返回空集合,否則編譯時會報錯。
然后將MyReactPackage添加到ReactActivity中

public class MainActivity extends ReactActivity {
    @Override
    protected String getMainComponentName() {
        return "RN0405";
    }
    @Override
    protected boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
    }
    @Override
    protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
                new MyReactPackage()
        );
    }
}

這個MainActivity是init RN工程時自動生成的。將上面的MyReactPackage添加到getPackages方法里即可。
3.在MyTextViewManager中添加需要導出的view屬性。

public class MyTextViewManager extends SimpleViewManager<TextView> {
    @Override
    public String getName() {
        return "MyTextView";
    }
    @Override
    protected TextView createViewInstance(ThemedReactContext reactContext) {
        TextView textView = new TextView(reactContext);
        return textView;
    }
    @ReactProp(name="text")
    public void setText(TextView view,String text){
        view.setText(text);
    }
    @ReactProp(name="textSize")
    public void setTextSize(TextView view,float fontSize){
        view.setTextSize(fontSize);
    }
    @ReactProp(name="textColor",defaultInt = Color.BLACK)
    public void setTextColor(TextView view,int textColor){
        view.setTextColor(textColor);
    }
    @ReactProp(name="isAlpha",defaultBoolean = false)
    public void setTextAlpha(TextView view,boolean isAlpha){
        if(isAlpha){
            view.setAlpha(0.5f);
        }else{
        }
    }
}

官方文檔里說的很清楚,也強調了注意事項,在此不贅述,摘錄如下:
要導出給JavaScript使用的屬性,需要申明帶有@ReactProp(或@ReactPropGroup)注解的設置方法。方法的第一個參數是要修改屬性的視圖實例,第二個參數是要設置的屬性值。方法的返回值類型必須為void,而且訪問控制必須被聲明為public。JavaScript所得知的屬性類型會由該方法第二個參數的類型來自動決定。支持的類型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。@ReactProp注解必須包含一個字符串類型的參數name。這個參數指定了對應屬性在JavaScript端的名字。除了name,@ReactProp注解還接受這些可選的參數:defaultBoolean, defaultInt, defaultFloat。這些參數必須是對應的基礎類型的值(也就是boolean, int, float
),這些值會被傳遞給setter方法,以免JavaScript端某些情況下在組件中移除了對應的屬性。注意這個"默認"值只對基本類型生效,對于其他的類型而言,當對應的屬性刪除時,null會作為默認值提供給方法。使用@ReactPropGroup來注解的設置方法和@ReactProp不同。請參見@ReactPropGroup注解類源代碼中的文檔來獲取更多詳情。
重要! 在ReactJS里,修改一個屬性會引發一次對設置方法的調用。有一種修改情況是,移除掉之前設置的屬性。在這種情況下設置方法也一樣會被調用,并且“默認”值會被作為參數提供(對于基礎類型來說可以通過defaultBoolean、defaultFloat等@ReactProp的屬性提供,而對于復雜類型來說參數則會設置為null)
這里注意一點是關于view的基本屬性,如長寬是不需要在此導出的。至于需要導出哪些屬性是要根據要封裝的原生view的實際情況來做決定的,如果不知道如何導某些類型的屬性,建議去看看官方已經封裝好的原生組件(在MainReactPackage里可以看到官方已經封裝了的原生組件)。
至此,java端算是做好了。
4.實現對應的JS模塊。
創建MyTextView.js 文件,里面初始的代碼如下

var {requireNativeComponent,PropTypes}=require('react-native');
var myTextView ={
    name:'MyTextViewLOL',
    propTypes:{
        text:PropTypes.string,
        textSize:PropTypes.number,
        textColor:PropTypes.number,
        isAlpha:PropTypes.bool,
    }
}
module.exports =requireNativeComponent('MyTextView',myTextView);

第一行代碼是引入兩個模塊,在ES6里的寫法是用import代替的。
第二行的myTextView 是定義了myTextView對象,里面用propTypes預定義了我們剛才在MyTextViewManager里導出的四個屬性。
最后一行代碼是這個JS文件導出的模塊,requireNativeComponent的第一個參數是剛才在MyTextViewManager的getName的返回值,第二個參數就是上面我們定義的myTextView,這樣就將native端和JS端的兩個對象綁定在一起了。
5.在JS里使用封裝的原生UI。
index.android.js

import React, {
  AppRegistry,
  Component,
  StyleSheet,
  Text,
  View,
    Dimensions
} from 'react-native';
const dimensions = Dimensions.get('window');
var MyTextView = require('./MyTextView');
class RN0405 extends Component {
    constructor(){
        super();
        this.state={
            text:"hahahahahaghahaha"
        }
    }
  render() {
    return (
        <View style={styles.container}>
            <View style={styles.outView}>
            <MyTextView
                style={styles.myTextView}
                text={this.state.text}
                textSize={15}
                isAlpha={false}
            />
            </View>
        </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    width:dimensions.width,
      alignItems:'center',
    flex: 1,
    backgroundColor: '#F5FCFF',
  },
    outView:{
        borderWidth:1,
    },
    myTextView:{
        width:300,
        height:100,
    },
});
AppRegistry.registerComponent('RN0405', () => RN0405);

其實主要代碼沒幾行,主要是

var MyTextView = require('./MyTextView');
<MyTextView
                style={styles.myTextView}
                text={this.state.text}
                textSize={15}
                isAlpha={false}
            />

要注意的是在styles.myTextView記得設置寬高,否則就算其他做對了,但也不會顯示。
到這里所有步驟走完一遍了,來react-native run-android一下吧。(這里是在Mac上,windows上要先react-antive start)
等運行完了會發現是紅屏錯誤(沒有設置的IP和端口的自己去設置好),提示是沒有預定義testID,這里就要回到步驟4了:剛才只是把我們在MyTextViewManager導出的四個屬性預定義了,然而現在看起來還不夠,還有一些view的默認存在屬性是需要在JS端也預定義的。本著哪里出錯改哪里的解決思路,把testID添加上,再運行,接著還提示有屬性沒添加,那就繼續添加吧。等到哪一次運行不提示還有屬性沒添加的時候就算添加完了。完整的步驟4文件是這樣的。

var {requireNativeComponent,PropTypes}=require('react-native');
var myTextView ={
    name:'MyTextViewLOL',
    propTypes:{
        text:PropTypes.string,
        textSize:PropTypes.number,
        textColor:PropTypes.number,
        isAlpha:PropTypes.bool,

        testID:PropTypes.string,
        accessibilityComponentType:PropTypes.string,
        accessibilityLabel:PropTypes.string,
        accessibilityLiveRegion:PropTypes.string,
        renderToHardwareTextureAndroid:PropTypes.bool,
        importantForAccessibility:PropTypes.string,
        onLayout:PropTypes.bool,
    }
}
module.exports =requireNativeComponent('MyTextView',myTextView);

6.注冊原生事件。
到現在,已經基本可以看到將一個原生View封裝成RN的view并且可以以組件形式正常使用了,如果你的需求只是在界面上展示這樣一個view,甚至可以有點擊等觸發功能,那么可以滿足了。但是如果想產生native端和JS端的互動,亦即原生事件和JS端事件相互綁定和觸發,那么還需要進行注冊事件,這樣也才能形成兩端完整的互動。
首先在native端進行事件注冊。
在MyTextViewManager中,修改createViewInstance方法。

@Override
 protected TextView createViewInstance(final ThemedReactContext reactContext) {
 final TextView textView = new TextView(reactContext);
 textView.setOnTouchListener(new View.OnTouchListener() {
 @Override
 public boolean onTouch(View v, MotionEvent event) {
 if(event.getAction()==MotionEvent.ACTION_DOWN){
 WritableMap nativeEvent= Arguments.createMap();
 nativeEvent.putString("message", "MyMessage");
 reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
 textView.getId(), "topChange",nativeEvent
 );
 return true;
 }else{
 return false;
 }
 }
 });
 return textView;
 }

這是在創建的view實例中,當發生本地事件時進行事件注冊。這個事件名topChange
在JavaScript端映射到onChange
回調屬性上,這個映射關系是固定的被官方寫在UIManagerModuleConstants.java
文件里了,當自己有其他事件需求時需要去這個類里查詢。
這里關鍵是要理解有RCTEventEmitter的這行代碼,并且要寫對地方。有些同學參考官方文檔沒有搞出來,應該就是沒有把這行代碼寫在本地事件發生的時候,例如上面的onTouch里。emitter的中文意思是發出者,發射體。
然后就是JS端的事件綁定了。
修改上面的MyTextView.js文件,完整代碼如下

var {requireNativeComponent,PropTypes}=require('react-native');
var myTextView ={
 name:'MyTextViewLOL',
 propTypes:{
 text:PropTypes.string,
 textSize:PropTypes.number,
 textColor:PropTypes.number,
 isAlpha:PropTypes.bool,

 testID:PropTypes.string,
 accessibilityComponentType:PropTypes.string,
 accessibilityLabel:PropTypes.string,
 accessibilityLiveRegion:PropTypes.string,
 renderToHardwareTextureAndroid:PropTypes.bool,
 importantForAccessibility:PropTypes.string,
 onLayout:PropTypes.bool,
 }
}
var RCTMyView=requireNativeComponent('MyTextView',myTextView);
import React,{
 Component
} from 'react-native';
class MyView extends Component{
 constructor(){
 super();
 this._onChange=this._onChange.bind(this);
 }
 _onChange(event:Event){
 if(!this.props.onChangeMessage){
 return;
 }
 if(event.nativeEvent.message==='MyMessage'){
 this.props.onChangeMessage();
 return;
 }
 }
 render(){
 return <RCTMyView
 {...this.props}
 onChange={this._onChange} />
 }
}
MyView.propTypes={
 onChangeMessage:React.PropTypes.func,
}
module.exports =MyView;

這里用MyView對myTextView進行了一次封裝。注意到在MyView里為onChange綁定了_onChange方法,在這個方法里我們會調用一個預定義為函數的onChangeMessage。而之前已經在native端將topChange綁定了原生的onTouch事件,topChange又會映射到JS端的onChange屬性,這樣最后當原生的onTouch事件發生時,就會調用JS端定義的onChangeMessage函數,就實現了兩端事件的互動。其實onTouch事件對應到onChange屬性后就已經實現了事件綁定,寫onChangeMessage是為了示例展示而已。
這里提一下上面的event.nativeEvent.message==='MyMessage'
這一對是在MyTextViewManger里寫的,在練習的時候發現當WritableMap里K 為"type",v隨意時,這里用 event.type==='MyMessage' 也可以,但用其他諸如message時就不可以。知道為啥的同學可以告訴下我。
最后在JS里使用已經封裝好的View。
只需為上面的index.android.js文件添加幾行代碼即可。
在RNWIN0410里添加一個函數

_onButtonPress(){
        alert("haha");
        this.setState({
            text:"bind event successful!"
        });
    }
為<MyTextView />添加一個屬性onChangeMessage={()=>this._onButtonPress()}

好了,重新react-native run-android,看到haha后點擊下應該就會出現彈窗和發生文字更改了。
上面就是我參考官方文檔實現的封裝原生組件過程,還是比較簡單的,限于經驗有限,若有錯誤還望指點。還是鼓勵大家多去看官方文檔和組件代碼,會學到很多東西,至于有時模仿官方代碼反而錯誤百出的時候要注意官方的封裝是系統的層層封裝,不像這個示例一樣只是一個類而已。
最近RN版本更新到0.23.1了,發現對windows的兼容性好很多了。在家里在windows10下試著init了下發現竟然成功了!我這是前幾個月時按官方教程一步步來配置的環境,只是當時一直出問題就放棄了。現在竟然創建工程成功了,并且可以正常跑在我的錘子上了!
這是不是說明RN的1.0正式版本就快要推出了呢?
文末提供下調出RN調試菜單的代碼,省的老是搖一搖讓人以為我們上班不務正業呢

@ReactMethod
public void showDevMenu(){    
getCurrentActivity().runOnUiThread(new Runnable() {       
 @Override        
public void run() {         
 ((ReactActivity)getCurrentActivity()).onKeyUp(KeyEvent.KEYCODE_MENU,null);        
} 
});
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評論 25 708
  • 繼上一篇文章的React Native 與原生之間的通信(iOS),我們知道RN與原生通信主要通過屬性、原生模塊、...
    朱_源浩閱讀 19,322評論 43 60
  • 學習ReactNative有一段時間了,也加了好多群,看見群里每天那么多人在那兒討論和學習ReactNative,...
    光無影閱讀 7,521評論 28 41
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,251評論 4 61
  • 剛剛下載了軟件。還不太會用,帶著將信將疑的態度,試寫一篇隨筆,希望只有自己才能看得見,需要這樣的安全感~ 轉眼寒假...
    小懶貓呀呀閱讀 268評論 0 0