美文网首页React Native 周报Android技术分享Android技术知识
如何创建一个android的react-native组件(二)

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

作者: alexis_zyp | 来源:发表于2015-12-18 12:04 被阅读3406次

    接着上一篇文章,这一篇我把自己上传到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) {
    
                    int childCount = view.getChildCount();
                    for (int i = 0; i < childCount; i++) {
                        ((RadioButton)view.getChildAt(i)).setChecked(false);
                        if (view.getChildAt(i).getId() == checkedId) {
                            ((RadioButton)view.getChildAt(i)).setChecked(true);
     
                            reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
                                    .dispatchEvent( 
                                            new AndroidSegmentedEvent( 
                                                    view.getId(),
                                                    SystemClock.uptimeMillis(),
                                                    i));
                        } 
                    } 
                }
            });
    
    
        }
    
    
        @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<AndroidSegmentedEvent>

    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; // <-- import
    
    public 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语法我也不是特别理解,只是照猫画虎。欢迎大家来吐槽>_<。

    1.如何自定义react-native的android组件(一)
    3.react-native-0.16.1 自定义Android组件部分的源码初探

    相关文章

      网友评论

        本文标题:如何创建一个android的react-native组件(二)

        本文链接:https://www.haomeiwen.com/subject/elpphttx.html