您的位置:首页 > Web前端 > React

可能是最in的React Native使用原生自定义下拉刷新组件

2017-01-25 22:04 791 查看
在 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>
)
}
}


希望你能有所收获,本文完!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: