• 为什么这么说呢,因为随着业务的爆发式增长,传统的原生开发模式有点显得跟不上节奏了,这也促使各个公司希望寻找到一个更加高效的开发方案,当下可以被选择的方案中, React Native 及Weex 都是不错的技术方案...

    在 2016 年移动端跨平台开发是几个最热的技术之一,相信在 2017 年这股热潮将持续发酵。为什么这么说呢,因为随着业务的爆发式增长,传统的原生开发模式有点显得跟不上节奏了,这也促使各个公司希望寻找到一个更加高效的开发方案,当下可以被选择的方案中, React Native 及Weex 都是不错的技术方案。在年前团队内部的一场 React Native vs Weex 的技术对垒中本来我选择的是 Weex 的阵营,但当时在多维的技术指标中新生的 Weex 还是不敌 React Native ,团队内部最终敲定了采用 React Native 跨平台方案。http://www.tuicool.com/articles/ERjyyiU

    1、概述

    闲话不多说,这里的主要目的是跟大家聊聊 React Native 在 Android 平台使用原生自定义 View ,这里默认大家对 React Native 已经有一定的了解, React Native 中的组件都是基于 iOS/Android 的官方组件进行封装,所以在一些特别的场景下并不能很好的满足需求。正如标题中的下拉刷新组件, React Native 在 Android 平台采用的是 android.support.v4.widget.SwipeRefreshLayout ,一些 iOS 设计优先的团队(譬如我司)而言对于 Android 开发人员简直就是灾难。在众多开源的 React Native 项目中大家也不会再这些细节上较真,但是公司的 UED 这关可不好过。

    听说流行有图有真相,那先来个在 iOS 端经典的菊花图的 Android reac-native 版:

    2、Android 端的支持实现

    适配 Android 平台的原生组件可以参看官方文档 Native UI Components ,如果网络不方便的话也可以参看翻译版 原生UI组件 。

    2.1 自定义下拉刷新控件

    这里就不讲如何自定义 Android 控件,假设你是一位有一定经验的开发人员。

    //自定义的下拉刷新控件
    public class PullToRefreshView extends ViewGroup {
        ...
          
        public PullToRefreshView(Context context) {
            ...
        }  
        
        public void setRefreshing(boolean refreshing) {
            ...
        }
        
        public void setOnRefreshListener(OnRefreshListener listener) {
            ...
        }
    }
    

    2.2 创建 ViewManager 的实现类

    官方文档中给我们的示例是创建 SimpleViewManager 的实现类,但此处的下拉刷新控件是个 ViewGroup ,所以此处实现类应继承 ViewManager 的另一个子类 ViewGroupManager 。

    public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        @Override
        public String getName() {
            return "PtrLayout";
        }
    
        @Override
        protected PullToRefreshView createViewInstance(ThemedReactContext reactContext) {
            return new PullToRefreshView(reactContext);
        }
        ...
    }
    

    到这里一个简单的 ViewGroupManager 就实现了。

    2.3 给 ViewManager 添加事件监听

    但我们这是一个下拉刷新控件,有一个问题是我们如何将下拉刷新的监听事件传递给 JavaScript 呢?官方文档中写的并不清晰,还是翻阅源码吧,果不其然在源码中寻找到了我们想要的答案。

    覆写 addEventEmitters 函数将事件监听传递给 JavaScript 。

    public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        ...
        @Override
        protected void addEventEmitters(ThemedReactContext reactContext, PullToRefreshView view) {
            view.setOnRefreshListener(new PullToRefreshView.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
                            .dispatchEvent(new PtrRefreshEvent(view.getId()));
                }
            });
        }
      
        @Nullable
        @Override
        public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
            return MapBuilder.<String, Object>builder()
                    .put("topRefresh", MapBuilder.of("registrationName", "onRefresh"))
                    .build();
        }
        ...
    }
    

    我们将事件封装为 PtrRefreshEvent 。

    public class PtrRefreshEvent extends Event<PtrRefreshEvent>{
    
        protected PtrRefreshEvent(int viewTag) {
            super(viewTag);
        }
    
        @Override
        public String getEventName() {
            return "topRefresh";
        }
    
        @Override
        public void dispatch(RCTEventEmitter rctEventEmitter) {
            rctEventEmitter.receiveEvent(getViewTag(),getEventName(),null);
        }
    }
    

    细心地你肯定发现了 getExportedCustomDirectEventTypeConstants 这个函数,这里先说明一下,覆写该函数,将 topRefresh 这个事件名在 JavaScript 端映射到 onRefresh 回调属性上,这部分我们后面会在结合 JavaScript 再解释下用法。

    关于组件这部分大家可以参看 React Native 的 Android 部分的代码。

    2.4 使用@ReactProp 注解导出属性的设置方法

    这部分内容官方文档的介绍足够使用了,这里不再细说。

    public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        ...
        @ReactProp(name = "refreshing")
        public void setRefreshing(PullToRefreshView view, boolean refreshing) {
            view.setRefreshing(refreshing);
        }
    }
    

    2.5 将 ViewManager 注册到应用

    如果你熟悉 Android 的 React Native 集成的话,你只需要将 SwipeRefreshViewManager 添加到 ReactPackage 中即可,

    public class MainPackage implements ReactPackage {
        ...
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
            return Arrays.asList(new SwipeRefreshViewManager());
        }
        ...
    }
    

    到这里 Android 端的实现已经全部完成了。

    3、React/JS 端的组件实现及使用

    接下来我们来聊一聊使用 React 实现下拉刷新的组件,当然在这之前期望你对 jsx/es6 的语法及 react/react-native 的 API 有一定的了解。

    3.1 实现下拉刷新组件

    还记得吗,在 Android 我们通过 SwipeRefreshViewManager 中 getName 返回的控件名称,将会在这里用于引用这个原生控件。

    'use strict';
    import React, {Component, PropTypes} from 'react';
    import {View, requireNativeComponent} from 'react-native';
    import NativeMethodsMixin from 'react/lib/NativeMethodsMixin';
    import mixin from 'react-mixin';
    //引用原生下拉刷新控件
    const NativePtrView = requireNativeComponent('PtrLayout', PtrView);
    //封装一个react组件,该组件中引用了原生控件的实现
    class PtrView extends Component {
        static propTypes = {
            ...View.propTypes,
            onRefresh: PropTypes.func,
            refreshing: PropTypes.bool.isRequired
        };
    
        _nativeRef = (null: ?PtrView);
        _lastNativeRefreshing = false;
    
        constructor(props) {
            super(props);
        }
    
        componentDidMount() {
            this._lastNativeRefreshing = this.props.refreshing;
        }
    
        componentDidUpdate(prevProps = {refreshing: false}) {
            if (this.props.refreshing !== prevProps.refreshing) {
                this._lastNativeRefreshing = this.props.refreshing;
            } else if (this.props.refreshing !== this._lastNativeRefreshing) {
                this._nativeRef.setNativeProps({refreshing: this.props.refreshing});
                this._lastNativeRefreshing = this.props.refreshing;
            }
        }
    	//渲染原生下拉刷新控件,这里onRefresh就是在ViewManager::getExportedCustomDirectEventTypeConstants 
        //这个函数中 topRefresh 的映射属性。 
        render() {
            return (
                <NativePtrView
                    {...this.props}
                    ref={ref => this._nativeRef = ref}
                    onRefresh={this._onRefresh.bind(this)}/>
            )
        }
    
        _onRefresh() {
            this._lastNativeRefreshing = true;
            this.props.onRefresh && this.props.onRefresh();
            this.forceUpdate();
        }
    }
    mixin.onClass(PtrView, NativeMethodsMixin);
    
    export {PtrView};
    

    3.2 下拉刷新组件的使用

    说到使用就太简单了,虽然简单但仍然要说,我们知道官方提供的组件例如 ListView 中通过 refreshControl 来指定刷新控制器,用法是这样的:

    class Demo1 extends Component {
        ...
        render() {
            return (
                <View style={{flex: 1}}>
                    <ListView 
                        ...
                        refreshControl={
                            <RefreshControl
                                refreshing={this.state.refreshing}
                                onRefresh={this._refresh.bind(this)} />
                        }
                    />
                </View>
            )
        }
    }
    

    我就在想既然可以通过 refreshControl 来指定刷新控制器,那我自定义的下拉刷新组件是不是也可以通过 refreshControl 来指定呢?带着这样的疑问,我仔细读了读 ListView/ScrollView的源码,发现这个猜想还是蛮靠谱的,也赞叹 Facebook 的工程师们的妙笔生花。

    const ScrollView = React.createClass({
        let ScrollViewClass;
        if (Platform.OS === 'ios') {
          ScrollViewClass = RCTScrollView;
        } else if (Platform.OS === 'android') {
          if (this.props.horizontal) {
            ScrollViewClass = AndroidHorizontalScrollView;
          } else {
            ScrollViewClass = AndroidScrollView;
          }
        }
        ...
    
        const refreshControl = this.props.refreshControl;
        if (refreshControl) {
          if (Platform.OS === 'ios') {
            ...
          } else if (Platform.OS === 'android') {
            // On Android wrap the ScrollView with a AndroidSwipeRefreshLayout.
            // Since the ScrollView is wrapped add the style props to the
            // AndroidSwipeRefreshLayout and use flex: 1 for the ScrollView.
            // 此处就是重点,通过 cloneElement 创建一个新的 ReactElement,而 refreshControl 是通过 props 指定而来并没有写死,Good!
            return React.cloneElement(
              refreshControl,
              {style: props.style},
              <ScrollViewClass {...props} ref={this._setScrollViewRef}>
                {contentContainer}
              </ScrollViewClass>
            );
          }
        }
        return (
          ...
        );
    })
    

    基于以上的分析以及我们对于属性的封装,我们的写法也相当的原味:

    class Demo2 extends Component {
        ...
        render() {
            return (
                <View style={{flex: 1}}>
                    <ListView 
                        ...
                        refreshControl={
                             //这里为了保证只在Android平台上使用该组件,如果iOS端也有原生控件的实现,
                             //那就不必考虑平台了。
                             Platform.OS === 'android' ? 
                             <PtrView
                                  refreshing={this.state.refreshing}
                                  onRefresh={this._refresh.bind(this)} />
                             :
                             <RefreshControl
                                  refreshing={this.state.refreshing}
                                  onRefresh={this._refresh.bind(this)} />
                        }
                    />
                </View>
            )
        }
    }
    

    希望你能有所收获,本文完!

    展开全文
  • ReactNative自定义控件之 RefreshLayout1 自定义下拉刷新控件 //自定义的下拉刷新控件 public class PullToRefreshView extends ViewGroup { ... public PullToRefreshView(Context context) { ... } public ...

    ReactNative自定义控件之 RefreshLayout

    1 自定义下拉刷新控件

    
        //自定义的下拉刷新控件
    public class PullToRefreshView extends ViewGroup {
        ...
    
        public PullToRefreshView(Context context) {
            ...
        }  
    
        public void setRefreshing(boolean refreshing) {
            ...
        }
    
        public void setOnRefreshListener(OnRefreshListener listener) {
            ...
        }
    }
    

    2 创建 ViewManager 的实现类

    官方文档中给我们的示例是创建 SimpleViewManager 的实现类,但此处的下拉刷新控件是个 ViewGroup ,所以此处实现类应继承 ViewManager 的另一个子类 ViewGroupManager

    public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        @Override
        public String getName() {
            return "PtrLayout";
        }
    
        @Override
        protected PullToRefreshView createViewInstance(ThemedReactContext reactContext) {
            return new PullToRefreshView(reactContext);
        }
        ...
    }

    3 给 ViewManager 添加事件监听

    但我们这是一个下拉刷新控件,有一个问题是我们如何将下拉刷新的监听事件传递给 JavaScript 呢?官方文档中写的并不清晰,还是翻阅源码吧,果不其然在源码中寻找到了我们想要的答案。

    覆写 addEventEmitters 函数将事件监听传递给 JavaScript 。

    
        public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        ...
        @Override
        protected void addEventEmitters(ThemedReactContext reactContext, PullToRefreshView view) {
            view.setOnRefreshListener(new PullToRefreshView.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
                            .dispatchEvent(new PtrRefreshEvent(view.getId()));
                }
            });
        }
    
        @Nullable
        @Override
        public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
            return MapBuilder.<String, Object>builder()
                    .put("topRefresh", MapBuilder.of("registrationName", "onRefresh"))
                    .build();
        }
        ...
    }
    
    //我们将事件封装为 PtrRefreshEvent 。
    
    public class PtrRefreshEvent extends Event<PtrRefreshEvent>{
    
        protected PtrRefreshEvent(int viewTag) {
            super(viewTag);
        }
    
        @Override
        public String getEventName() {
            return "topRefresh";
        }
    
        @Override
        public void dispatch(RCTEventEmitter rctEventEmitter) {
            rctEventEmitter.receiveEvent(getViewTag(),getEventName(),null);
        }
    }
    
    细心地你肯定发现了 getExportedCustomDirectEventTypeConstants 这个函数,这里先说明一下,覆写该函数,将 topRefresh 这个事件名在 JavaScript 端映射到 onRefresh 回调属性上,这部分我们后面会在结合 JavaScript 再解释下用法。
    
    关于组件这部分大家可以参看 React Native 的 Android 部分的代码。
    
    
    

    4 使用@ReactProp 注解导出属性的设置方法

    这部分内容官方文档的介绍足够使用了,这里不再细说。

    public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
        ...
        @ReactProp(name = "refreshing")
        public void setRefreshing(PullToRefreshView view, boolean refreshing) {
            view.setRefreshing(refreshing);
        }
    }

    5 将 ViewManager 注册到应用

    如果你熟悉 Android 的 React Native 集成的话,你只需要将 SwipeRefreshViewManager 添加到 ReactPackage 中即可

    public class MainPackage implements ReactPackage {
        ...
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
            return Arrays.asList(new SwipeRefreshViewManager());
        }
        ...
    }

    6 实现下拉刷新组件

    还记得吗,在 Android 我们通过 SwipeRefreshViewManager 中 getName 返回的控件名称,将会在这里用于引用这个原生控件。

    'use strict';
    import React, {Component, PropTypes} from 'react';
    import {View, requireNativeComponent} from 'react-native';
    import NativeMethodsMixin from 'react/lib/NativeMethodsMixin';
    import mixin from 'react-mixin';
    //引用原生下拉刷新控件
    const NativePtrView = requireNativeComponent('PtrLayout', PtrView);
    //封装一个react组件,该组件中引用了原生控件的实现
    class PtrView extends Component {
        static propTypes = {
            ...View.propTypes,
            onRefresh: PropTypes.func,
            refreshing: PropTypes.bool.isRequired
        };
    
        _nativeRef = (null: ?PtrView);
        _lastNativeRefreshing = false;
    
        constructor(props) {
            super(props);
        }
    
        componentDidMount() {
            this._lastNativeRefreshing = this.props.refreshing;
        }
    
        componentDidUpdate(prevProps = {refreshing: false}) {
            if (this.props.refreshing !== prevProps.refreshing) {
                this._lastNativeRefreshing = this.props.refreshing;
            } else if (this.props.refreshing !== this._lastNativeRefreshing) {
                this._nativeRef.setNativeProps({refreshing: this.props.refreshing});
                this._lastNativeRefreshing = this.props.refreshing;
            }
        }
        //渲染原生下拉刷新控件,这里onRefresh就是在ViewManager::getExportedCustomDirectEventTypeConstants 
        //这个函数中 topRefresh 的映射属性。 
        render() {
            return (
                <NativePtrView
                    {...this.props}
                    ref={ref => this._nativeRef = ref}
                    onRefresh={this._onRefresh.bind(this)}/>
            )
        }
    
        _onRefresh() {
            this._lastNativeRefreshing = true;
            this.props.onRefresh && this.props.onRefresh();
            this.forceUpdate();
        }
    }
    mixin.onClass(PtrView, NativeMethodsMixin);
    
    export {PtrView};

    7 下拉刷新组件的使用

    说到使用就太简单了,虽然简单但仍然要说,我们知道官方提供的组件例如 ListView 中通过 refreshControl 来指定刷新控制器,用法是这样的:

    class Demo1 extends Component {
        ...
        render() {
            return (
                <View style={{flex: 1}}>
                    <ListView 
                        ...
                        refreshControl={
                            <RefreshControl
                                refreshing={this.state.refreshing}
                                onRefresh={this._refresh.bind(this)} />
                        }
                    />
                </View>
            )
        }
    }

    8 我就在想既然可以通过 refreshControl 来指定刷新控制器,那我自定义的下拉刷新组件是不是也可以通过 refreshControl 来指定呢?带着这样的疑问,我仔细读了读 ListView/ScrollView 的源码,发现这个猜想还是蛮靠谱的,也赞叹 Facebook 的工程师们的妙笔生花。

    const ScrollView = React.createClass({
        let ScrollViewClass;
        if (Platform.OS === 'ios') {
          ScrollViewClass = RCTScrollView;
        } else if (Platform.OS === 'android') {
          if (this.props.horizontal) {
            ScrollViewClass = AndroidHorizontalScrollView;
          } else {
            ScrollViewClass = AndroidScrollView;
          }
        }
        ...
    
        const refreshControl = this.props.refreshControl;
        if (refreshControl) {
          if (Platform.OS === 'ios') {
            ...
          } else if (Platform.OS === 'android') {
            // On Android wrap the ScrollView with a AndroidSwipeRefreshLayout.
            // Since the ScrollView is wrapped add the style props to the
            // AndroidSwipeRefreshLayout and use flex: 1 for the ScrollView.
            // 此处就是重点,通过 cloneElement 创建一个新的 ReactElement,而 refreshControl 是通过 props 指定而来并没有写死,Good!
            return React.cloneElement(
              refreshControl,
              {style: props.style},
              <ScrollViewClass {...props} ref={this._setScrollViewRef}>
                {contentContainer}
              </ScrollViewClass>
            );
          }
        }
        return (
          ...
        );
    })

    9 基于以上的分析以及我们对于属性的封装,我们的写法也相当的原味:

    class Demo2 extends Component {
        ...
        render() {
            return (
                <View style={{flex: 1}}>
                    <ListView 
                        ...
                        refreshControl={
                             //这里为了保证只在Android平台上使用该组件,如果iOS端也有原生控件的实现,
                             //那就不必考虑平台了。
                             Platform.OS === 'android' ? 
                             <PtrView
                                  refreshing={this.state.refreshing}
                                  onRefresh={this._refresh.bind(this)} />
                             :
                             <RefreshControl
                                  refreshing={this.state.refreshing}
                                  onRefresh={this._refresh.bind(this)} />
                        }
                    />
                </View>
            )
        }
    }
    展开全文
1
收藏数 2
精华内容 0
热门标签