0%

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

作者:Oz

版权声明:本文图文为博主原创,转载请注明出处。

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

概述

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

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

react-native-ptr

Android 端的支持实现

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

自定义下拉刷新控件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//自定义的下拉刷新控件
public class PullToRefreshView extends ViewGroup {
...

public PullToRefreshView(Context context) {
...
}

public void setRefreshing(boolean refreshing) {
...
}

public void setOnRefreshListener(OnRefreshListener listener) {
...
}
}

创建 ViewManager 的实现类

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

1
2
3
4
5
6
7
8
9
10
11
12
public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
@Override
public String getName() {
return "PtrLayout";
}

@Override
protected PullToRefreshView createViewInstance(ThemedReactContext reactContext) {
return new PullToRefreshView(reactContext);
}
...
}

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

给 ViewManager 添加事件监听

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 NativeAndroid 部分的代码。

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

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

1
2
3
4
5
6
7
public class SwipeRefreshViewManager extends ViewGroupManager<PullToRefreshView>{
...
@ReactProp(name = "refreshing")
public void setRefreshing(PullToRefreshView view, boolean refreshing) {
view.setRefreshing(refreshing);
}
}

将 ViewManager 注册到应用

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

1
2
3
4
5
6
7
8
public class MainPackage implements ReactPackage {
...
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
return Arrays.asList(new SwipeRefreshViewManager());
}
...
}

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

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

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

实现下拉刷新组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
'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};

下拉刷新组件的使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的工程师们的妙笔生花,真是应了那句话叫“不要写死”!Facebook 的工程师做到了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 (
...
);
})

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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>
)
}
}

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

最后祝大家鸡年大吉吧!