如何创建一个android的react-native组件(二)

来源:互联网 发布:钻展数据分析 编辑:程序博客网 时间:2024/06/10 00:21

接着上一篇文章,这一篇我把自己上传到npm上的react-native-segmented-android开发步骤和大家分享。
下载react-native组件命令:

$ npm install react-native-segmented-android --save

这是效果图:

Segmented.png
这次要实现的是View组件,所以要通过继承SimpleViewManager 来实现,步骤和(一)基本保持一致。

开始

Step 1 - 新建react-native工程 ReactNativeSegmentedAndroid

$ react-native init ReactNativeSegmentedAndroid

Step 2 - 将新建的工程导入android studio然后新建空library(以react-native-segmented-android为library的名称)

Step 3 - 新建空library(以react-native-segmented-android为library的名称)
在library目录下的build.gradle中添加react-native的依赖

// file: android/react-native-segmented-android/build.gradle...dependencies {    ...    compile 'info.hoang8f:android-segmented:1.0.6'    compile 'com.facebook.react:react-native:0.16.+' }

Step 4 - 创建AndroidSegmented类继承SegmentedGroup

public class AndroidSegmented extends SegmentedGroup{    public void setSegmentOrientation(String str){        if(str.equals("horizontal")){            setOrientation(RadioGroup.HORIZONTAL);        }else if(str.equals("vertical")){            setOrientation(RadioGroup.VERTICAL);        }    }    public AndroidSegmented(ThemedReactContext context) {        super(context);        setGravity(Gravity.CENTER);        setLayoutParams(new ViewGroup.LayoutParams(                ViewGroup.LayoutParams.WRAP_CONTENT,                ViewGroup.LayoutParams.WRAP_CONTENT        ));    }    private final Runnable mLayoutRunnable = new Runnable() {        @Override        public void run() {            measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),                    MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));            layout(getLeft(), getTop(), getRight(), getBottom());        }    };    @Override    public void requestLayout() {        super.requestLayout();        post(mLayoutRunnable);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);    }}

Step 5 - 继承SimpleViewManager,注意这时就不是继承ReactContextBaseJavaModule了 ,大家可以很明显的发现setChildText()方法上多了一个‘@ReactProp(name = “childText”)’,加上了‘@ReactProp’的,segmented控件多了一个name为childText的属性,值为ReadableArray ( js端代码:childText={[‘One’,’Two’,’Three’,’Four’,”Five”]})。

public class AndroidSegmentedManager extends SimpleViewManager<AndroidSegmented> {    public static final String REACT_CLASS = "AndroidSegmented";    private static final String COLOR_REGEX = "^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$";    private Context context;    @Override    public String getName() {        return REACT_CLASS;    }    @Override    protected AndroidSegmented createViewInstance(ThemedReactContext reactContext) {        this.context = reactContext;        return new AndroidSegmented(reactContext);    }    @Override    protected void addEventEmitters(final ThemedReactContext reactContext, final AndroidSegmented view) {        view.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(RadioGroup group, int checkedId) {                reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()                        .dispatchEvent(                                new AndroidSegmentedEvent(                                        view.getId(),                                        SystemClock.uptimeMillis(),                                        checkedId));            }        });    }    @ReactProp(name = "childText")    public void setChildText(AndroidSegmented view, ReadableArray data) {        int childCount = data.size();        Log.e("TAG", "___" + childCount);        for (int i = 0; i < childCount; i++) {            RadioButton child = (RadioButton) LayoutInflater.from(context).inflate(R.layout.radio_button, null);            child.setText(data.getString(i));            view.addView(child);        }    }    @ReactProp(name = "selectedPosition")    public void setSelectedChild(AndroidSegmented view, int position) {        RadioButton radioBt= (RadioButton)(view.getChildAt(position));        radioBt.setChecked(true);    }    @ReactProp(name = "orientation")    public void setOrientation(AndroidSegmented view, String orientation) {        view.setSegmentOrientation(orientation);    }    @ReactProp(name = "tintColor")    public void setTintColor(AndroidSegmented view, ReadableArray data) {        String type0 = data.getType(0).name();        String type1 = data.getType(1).name();        if ("String".equals(type0) && "String".equals(type1)) {            String color0 = data.getString(0);            String color1 = data.getString(1);            if (color0 != null && color1 != null) {                if (color0.matches(COLOR_REGEX) && color1.matches(COLOR_REGEX)) {                    view.setTintColor(Color.parseColor(color0), Color.parseColor(color1));                } else {                    throw new JSApplicationIllegalArgumentException("Invalid arrowColor property: " + color0);                }            }        }    }}

Step 6 - 创建AndroidSegmentedEvent类继承Event

public class AndroidSegmentedEvent extends Event<AndroidSegmentedEvent> {    public static final String EVENT_NAME = "topChange";    private final int selectedPosition;    public AndroidSegmentedEvent(int viewId, long timestampMs, int selectedPosition) {        super(viewId, timestampMs);        this.selectedPosition = selectedPosition;    }    @Override    public String getEventName() {        return EVENT_NAME;    }    @Override    public void dispatch(RCTEventEmitter rctEventEmitter) {        rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());    }    @Override    public short getCoalescingKey() {        return 0;    }    private WritableMap serializeEventData() {        WritableMap eventData = Arguments.createMap();        eventData.putInt("selected", getPosition());        Log.e("AAA","position="+getPosition());        return eventData;    }    private int getPosition() {        return selectedPosition;    }}

Step 7 - 继承ReactPackage,注意createNativeModules()返回的是加入了 AndroidToastModule 的集合,createJSModules()与createViewManagers()返回的都是空集合,如果Step 4 步继承的是BaseViewManager或其子类,那么createViewManagers()中返回的就是加入了BaseViewManager的集合,其他的就是空集合,一般情况createJSModules()的返回值都是空集合。

public class AndroidSegmentedPackage 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 AndroidSegmentedManager());    }}

Step 8 - 新建AndroidSegmented.js,文件位置
‘ android/react-native-segmented-android/AndroidSegmented.js ’代码如下,然后在 ‘android/react-native-segmented-android/’下运行如下命令生成package.json文件

$ npm init         //生成package.json文件
//AndroidSegmented.js'use strict';var React = require('react-native');var { requireNativeComponent, PropTypes, View } = React;var NativeAndroidSegmented = requireNativeComponent('AndroidSegmented', AndroidSegmented);class AndroidSegmented extends React.Component {  constructor() {    super();    this._onChange = this._onChange.bind(this);  }  _onChange(event) {    if (this.props.onChange) {      this.props.onChange(event.nativeEvent);    }  }  render() {    return (      <NativeAndroidSegmented        {...this.props}         onChange={this._onChange}/>    );  }}var colorType = function (props, propName, componentName) {  var checker = function() {    var color = props[propName];    var regex = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;    if (!regex.test(color)) {      return new Error('Only accept color formats: #RRGGBB and #AARRGGBB');    }  };  return PropTypes.string(props, propName, componentName) || checker();}AndroidSegmented.propTypes = {  ...View.propTypes,  childText: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.string ])),  orientation:PropTypes.string,  tintColor:PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.string ])),  selectedPosition:PropTypes.number,  onChange: PropTypes.func,}AndroidSegmented.defaultProps = {};module.exports = AndroidSegmented;
//package.json内容{  "name": "react-native-segmented-android",  "version": "1.0.3",  "description": "a high imitation of iOS segmented Controls",  "main": "AndroidSegmented.js",  "scripts": {    "test": "react-native start"  },  "repository": {    "type": "git",    "url": "https://github.com/zzyyppqq/react-native-segmented-android.git"  },  "keywords": [    "android",    "segmented",    "react-component",    "react-native"  ],  "author": "zzyyppqq",  "license": "ISC",  "peerDependencies": {    "react-native": "^1.0.3"  }}

Step 9 - 复制AndroidSegmented.js 文件到‘/ReactNativeSegmentedAndroid/ ’ 目录下,如下是index.android.js代码,然后运行测试

'use strict';var React = require('react-native');var {  AppRegistry,  StyleSheet,  Text,  Dimensions,  ToastAndroid,  View,} = React;//var AndroidSegmented = require('react-native-segmented-android');var AndroidSegmented = require('./AndroidSegmented');var deviceWidth = Dimensions.get('window').width;var deviceHeight = Dimensions.get('window').height;var ReactNativeSegmentedExample = React.createClass({  onSelectPosition:function(event){    console.log(event);    ToastAndroid.show('segment '+event.selected, ToastAndroid.SHORT)  },  render: function() {    return (      <View>          <AndroidSegmented            tintColor={['#ff0000','#ffffff']}            style={{width:deviceWidth,height:60,backgroundColor:'#fff000',                  justifyContent: 'center',                  alignItems: 'center'}}            childText={['One','Two','Three','Four',"Five"]}            orientation='horizontal'            selectedPosition={0}            onChange={this.onSelectPosition} />            <AndroidSegmented              tintColor={['#009688','#ffffff']}              style={{width:deviceWidth,height:200,backgroundColor:'#fff000',                    justifyContent: 'center',                    alignItems: 'center'}}              childText={['One','Two','Three','Four',"Five"]}              orientation='vertical'              selectedPosition={0}              onChange={this.onSelectPosition} />        </View>    );  }});

Install

Step 1 - Install the npm package

$ npm install react-native-degment-android --save

Step 2 - Update Gradle Settings

// file: android/settings.gradle...include ':react-native-degment-android', ':app'project(':react-native-degment-android').projectDir = new File(rootProject.projectDir,'../node_modules/react-native-degment-android')

Step 3 - Update app Gradle Build

// file: android/app/build.gradle...dependencies {    ...    compile project(':react-native-degment-android')}

Step 4 - Register React Package

...import com.higo.zhangyp.segmented.AndroidSegmentedPackage; // <-- importpublic class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler {    private ReactInstanceManager mReactInstanceManager;    private ReactRootView mReactRootView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        mReactRootView = new ReactRootView(this);        mReactInstanceManager = ReactInstanceManager.builder()                .setApplication(getApplication())                .setBundleAssetName("index.android.bundle")                .setJSMainModuleName("index.android")                .addPackage(new MainReactPackage())                .addPackage(new AndroidSegmentedPackage()) // <-- Register package here                .setUseDeveloperSupport(BuildConfig.DEBUG)                .setInitialLifecycleState(LifecycleState.RESUMED)                .build();        mReactRootView.startReactApplication(mReactInstanceManager, "AwesomeProject", null);        setContentView(mReactRootView);    }...

从react-native的官方文档中我们已经知道facebook的react-native团队已经为我们实现了很多组件,例如 Image、Text、ViewPagerAndroid等,我们在index.android.js中可以直接使用这些组件,这些组件为什么能直接使用呢?

大家会很自然的想到已经封装好了呗。那在哪封装的?如何封装的?其实只要通过命令react-native init ProjectName创建过react-native工程的同学来说,在哪儿封装的一目了然。我们来看react-native工程的结构图:

B3BB8C75-7E30-4BCF-8BFD-4E7AA9A1A563.png

react-native工程中,在node_modules下有一个很特别的react-native文件夹,android的工程中的build.gradle 文件多了一个依赖,不用想肯定在这两个地方封装的,这也是react-native的关键。

dependencies {          compile 'com.facebook.react:react-native:0.16.+'    }

首先我们从入口MainActivity开始,看了我的前两篇文章,如何自定义react-native的android组件(一)和(二),要使用一个自定义组件,必须在MainActivity中加入【.addPackage(new AndroidSegmentedPackage()) 】才能使用。

...  @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        mReactRootView = new ReactRootView(this);        mReactInstanceManager = ReactInstanceManager.builder()                .setApplication(getApplication())                .setBundleAssetName("index.android.bundle")                .setJSMainModuleName("index.android")                .addPackage(new MainReactPackage())                .addPackage(new AndroidSegmentedPackage())                  .setUseDeveloperSupport(BuildConfig.DEBUG)                .setInitialLifecycleState(LifecycleState.RESUMED)                .build();        mReactRootView.startReactApplication(mReactInstanceManager, "ReactNativeSegmented", null);        setContentView(mReactRootView);    }...

那么官方的Android组件是如何实现的呢,我们肯定注意到了【.addPackage(new MainReactPackage())】和自定义的是不是很像,格式也一样,我想肯定在这里面有实现,进入MainReactPackage类中,代码如下:

//react-native 源码public class MainReactPackage implements ReactPackage {  @Override  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {    return Arrays.<NativeModule>asList(      new AsyncStorageModule(reactContext),      new FrescoModule(reactContext),      new IntentModule(reactContext),      new LocationModule(reactContext),      new NetworkingModule(reactContext),      new WebSocketModule(reactContext),      new ToastModule(reactContext));  }  @Override  public List<Class<? extends JavaScriptModule>> createJSModules() {    return Collections.emptyList();  }  @Override  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {    return Arrays.<ViewManager>asList(      new ReactDrawerLayoutManager(),      new ReactHorizontalScrollViewManager(),      new ReactImageManager(),      new ReactProgressBarViewManager(),      new ReactRawTextManager(),      new ReactScrollViewManager(),      new ReactSwitchManager(),      new ReactTextInputManager(),      new ReactTextViewManager(),      new ReactToolbarManager(),      new ReactViewManager(),      new ReactViewPagerManager(),      new ReactTextInlineImageViewManager(),      new ReactVirtualTextViewManager(),      new SwipeRefreshLayoutManager());  }}

看了MainReactPackage中的代码,果不其然,首先我们看createViewManagers()方法中的集合,看看集合子集的命名是不是很熟悉,
看看这里一共实现了多少原生控件:DrawerLayout、HorizontalScrollView、HorizontalScrollView、Image等等,还有SwipeRefreshLayout官网上还没有更新这个组件,其实这个版本已经可以使用了。

1.public class ReactDrawerLayoutManager extends ViewGroupManager<ReactDrawerLayout>2.public class ReactImageManager extends SimpleViewManager<ReactImageView>3.public class ReactProgressBarViewManager extends BaseViewManager<ProgressBarContainerView, ProgressBarShadowNode>...
//贴上一个ReactDrawerLayoutManager源码,大家看看实现public class ReactDrawerLayoutManager extends ViewGroupManager<ReactDrawerLayout> {  private static final String REACT_CLASS = "AndroidDrawerLayout";  public static final int OPEN_DRAWER = 1;  public static final int CLOSE_DRAWER = 2;  @Override  public String getName() {    return REACT_CLASS;  }  @Override  protected void addEventEmitters(ThemedReactContext reactContext, ReactDrawerLayout view) {    view.setDrawerListener(        new DrawerEventEmitter(            view,            reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()));  }  @Override  protected ReactDrawerLayout createViewInstance(ThemedReactContext context) {    return new ReactDrawerLayout(context);  }  @ReactProp(name = "drawerPosition", defaultInt = Gravity.START)  public void setDrawerPosition(ReactDrawerLayout view, int drawerPosition) {    if (Gravity.START == drawerPosition || Gravity.END == drawerPosition) {      view.setDrawerPosition(drawerPosition);    } else {      throw new JSApplicationIllegalArgumentException("Unknown drawerPosition " + drawerPosition);    }  }  @ReactProp(name = "drawerWidth", defaultFloat = Float.NaN)  public void getDrawerWidth(ReactDrawerLayout view, float width) {    int widthInPx = Float.isNaN(width) ?        ReactDrawerLayout.DEFAULT_DRAWER_WIDTH : Math.round(PixelUtil.toPixelFromDIP(width));    view.setDrawerWidth(widthInPx);  }  @Override  public boolean needsCustomLayoutForChildren() {    // Return true, since DrawerLayout will lay out it's own children.    return true;  }  @Override  public @Nullable Map<String, Integer> getCommandsMap() {    return MapBuilder.of("openDrawer", OPEN_DRAWER, "closeDrawer", CLOSE_DRAWER);  }  @Override  public void receiveCommand(      ReactDrawerLayout root,      int commandId,      @Nullable ReadableArray args) {    switch (commandId) {      case OPEN_DRAWER:        root.openDrawer();        break;      case CLOSE_DRAWER:        root.closeDrawer();        break;    }  }  @Override  public @Nullable Map getExportedViewConstants() {    return MapBuilder.of(        "DrawerPosition",        MapBuilder.of("Left", Gravity.START, "Right", Gravity.END));  }  @Override  public @Nullable Map getExportedCustomDirectEventTypeConstants() {    return MapBuilder.of(        DrawerSlideEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerSlide"),        DrawerOpenedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerOpen"),        DrawerClosedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerClose"),        DrawerStateChangedEvent.EVENT_NAME, MapBuilder.of(            "registrationName", "onDrawerStateChanged"));  }  /**   * This method is overridden because of two reasons:   * 1. A drawer must have exactly two children   * 2. The second child that is added, is the navigationView, which gets panned from the side.   */  @Override  public void addView(ReactDrawerLayout parent, View child, int index) {    if (getChildCount(parent) >= 2) {      throw new          JSApplicationIllegalArgumentException("The Drawer cannot have more than two children");    }    if (index != 0 && index != 1) {      throw new JSApplicationIllegalArgumentException(          "The only valid indices for drawer's child are 0 or 1. Got " + index + " instead.");    }    parent.addView(child, index);    parent.setDrawerProperties();  }  public static class DrawerEventEmitter implements DrawerLayout.DrawerListener {    private final DrawerLayout mDrawerLayout;    private final EventDispatcher mEventDispatcher;    public DrawerEventEmitter(DrawerLayout drawerLayout, EventDispatcher eventDispatcher) {      mDrawerLayout = drawerLayout;      mEventDispatcher = eventDispatcher;    }    @Override    public void onDrawerSlide(View view, float v) {      mEventDispatcher.dispatchEvent(          new DrawerSlideEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), v));    }    @Override    public void onDrawerOpened(View view) {      mEventDispatcher.dispatchEvent(        new DrawerOpenedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis()));    }    @Override    public void onDrawerClosed(View view) {      mEventDispatcher.dispatchEvent(          new DrawerClosedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis()));    }    @Override    public void onDrawerStateChanged(int i) {      mEventDispatcher.dispatchEvent(          new DrawerStateChangedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), i));    }  }}

原生控件的实现步骤、方法、例子等其实在源码中都有了,想实现什么组件就照着源码开发,绝不会出错啦。

到此只是完成了android端的java代码,那么组件如何与js代码联系起来,并且供js代码调用呢,我们来看看工程中的react-navie文件夹吧,秘密都在它里面。
react-navie文件结构:

QQ20151218-1@2x.png

打开react-native文件夹,我一眼就注意到了ReactAndroid目录(因为做Android嘛,对含有Android的词比较敏感>_<), 翻遍了其下所有的目录文件,终于找到一个有用点的文件package.json,在其中找到关键的一句话
【”main”: “Libraries/react-native/react-native.js”】图上用红框标注了。
下一步就该看看Libraries目录了,Libraries目录结构:

QQ20151218-0@2x.png

图上我用红框标注了几个我们熟悉的控件命名的文件家,我们重点关注两个文件夹
Components与CustomComponents 我们看看里面有什么:

QQ20151218-2@2x.png

红线标注的控件是不是很熟悉,我们随便找一个控件进去看看,就看DrawerAndroid吧,截图如下:

QQ20151218-4@2x.png

QQ20151218-3@2x.png

大家遇到的各种不解之处,其实大部分都可以在源码中得到解答,我也在继续学习中,我只是和大家分享我学习的过程,我也只是顺藤摸瓜了解了如何方便的去自定义组件。其实里面的好多ES6语法我也不是特别理解,只是照猫画虎。欢迎大家来吐槽>_<。

0 0
原创粉丝点击