React Native 之封装Android 的ViewGro
Ultra-Pull-to-Refresh 框架介绍
在原生的Android端,最火的下拉刷新就是liaohuqiu
的 android-Ultra-Pull-To-Refresh 框架.
该框架有几个特点:
- 继承ViewGroup,Content可以包含任何View .
- 简介完善的Header抽象,方便拓展,自定义显示效果
封装ViewGroup
在官网中,有介绍封装普通的View 是通过集成SimpleViewGroup
的,但并没有提及封装ViewGroup的办法.
某天看RefreshControl
这个组件的源码,在Android端的实现就是用谷歌官方的SwipeRefreshLayout
.该原生组件封装在SwipeRefreshLayoutManager
中,使用的是继承ViewGroupManager
,照葫芦画瓢,那就使用PtrFrameLayout继承ViewGroupManager
.
封装的源码如下:
public class ReactPtrLayout extends ViewGroupManager<PtrFrameLayout> {
private static final int STOP_REFRESH=1;
@Override
public String getName() {
return "PtrFrameLayout";
}
@Override
protected PtrFrameLayout createViewInstance(ThemedReactContext reactContext) {
final PtrFrameLayout rootView= (PtrFrameLayout)LayoutInflater.from(reactContext).inflate(R.layout.ptr_layout,null);
return rootView;
}
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("stop_refresh",STOP_REFRESH);
}
@Override
public void receiveCommand(PtrFrameLayout root, int commandId, @Nullable ReadableArray args) {
switch (commandId){
case STOP_REFRESH:
root.completeRefresh(PtrState.REFRESH_SUCCESS);
return;
}
}
@Override
protected void addEventEmitters(final ThemedReactContext reactContext, final PtrFrameLayout view) {
super.addEventEmitters(reactContext, view);
view.setOnRefreshListener(new PtrFrameLayout.OnRefreshListener() {
@Override
public void onRefresh() {
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
.dispatchEvent(new RefreshEvent(view.getId(), SystemClock.nanoTime()));
}
});
}
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put("topRefresh", MapBuilder.of("registrationName", "onRefresh"))
.build();
}
}
getName()
该方法是暴露给ReactNative端调用的名称
createViewInstance()
该方法用来返回PtrFrameLayout
的实例.
getCommandsMap()
接收ReactNative发送过来的命令,在receiveCommand()
方法中去处理该命令. 如这里就是ReactNative端可以发送停止刷新的命令.
addEventEmitters()
发送给ReactNative端一些时间,在getExportedCustomDirectEventTypeConstants()
方法暴露给ReactNative端.如该代码就是监听 PtrFrameLayout
的刷新事件,将刷新事件回调到ReactNative的onRefresh
方法中.
封装完该View之后就需要将它ReactPackage
的createViewManagers()
方法中,最后将ReactPackage
注册到MainApplication
的getPackages()
方法里.
ReactNative中调用
JS代码:
'use strict';
const React = require('React');
const ReactNative = require('ReactNative');
const requireNativeComponent = require('requireNativeComponent');
const View = require('View');
const Text = require('Text');
const Dimensions=require('Dimensions');
const deviceWidth = Dimensions.get('window').width;
const ScrollView =require('ScrollView');
var UIManager = require('UIManager');
const PK_REF_KEY="pk_ref_key";
const PtrFrameLayout =React.createClass({
propTypes: {
...View.propTypes,
},
generatedContent:function () {
return (
<ScrollView style={{width:deviceWidth,height:300,backgroundColor:'white'}} >
{this.props.children}
</ScrollView>
);
},
stopRefresh:function () {
UIManager.dispatchViewManagerCommand(
this.getPluImageHandle(),
1,
null
);
},
getPluImageHandle: function() {
return ReactNative.findNodeHandle(this.refs[PK_REF_KEY]);
},
render:function () {
return (
<AndroidPtrFrameLayout
ref={PK_REF_KEY}
onRefresh={()=>{
this.props.doRefresh&&this.props.doRefresh();
}}
{...this.props} >
{this.generatedContent()}
</AndroidPtrFrameLayout>
);
}
});
let AndroidPtrFrameLayout=requireNativeComponent('PtrFrameLayout',PtrFrameLayout,{});
module.exports=PtrFrameLayout;
使用 requireNativeComponent
方法找到原生的PtrFrameLayout,在render
方法中将其封装.
使用UIManager.dispatchViewManagerCommand
方法调用掉PtrFrameLayout
的指令名是1
的方法.
代码的使用
import PtrFrameLayout from './PtrFrameLayout';
......
<PtrFrameLayout
ref={KEY_REFRESH}
doRefresh={this._onRefresh}
style={{flex:1,backgroundColor:'#F1F1F1'}}>
.....some other view.....
</PtrFrameLayout>
出现的问题
- 在完成后,始终看不见 PtrFrameLayout的内容.
该问题困扰已久,为什么官方封装的SwipeRefreshLayout
可以,这个Ultra-Pull-To-Refresh
又不可以.最后看了该控件源码,有一段很关键的部分是这样的:
...
@Override
protected void onFinishInflate() {
final int childCount = getChildCount();
if (childCount > 2) {
throw new IllegalStateException("PtrFrameLayout can only contains 2 children");
} else if (childCount == 2) {
if (mHeaderId != 0 && mHeaderView == null) {
mHeaderView = findViewById(mHeaderId);
}
if (mContainerId != 0 && mContent == null) {
mContent = findViewById(mContainerId);
}
// not specify header or content
if (mContent == null || mHeaderView == null) {
View child1 = getChildAt(0);
View child2 = getChildAt(1);
if (child1 instanceof PtrUIHandler) {
mHeaderView = child1;
mContent = child2;
} else if (child2 instanceof PtrUIHandler) {
mHeaderView = child2;
mContent = child1;
} else {
// both are not specified
if (mContent == null && mHeaderView == null) {
mHeaderView = child1;
mContent = child2;
}
// only one is specified
else {
if (mHeaderView == null) {
mHeaderView = mContent == child1 ? child2 : child1;
} else {
mContent = mHeaderView == child1 ? child2 : child1;
}
}
}
}
} else if (childCount == 1) {
mContent = getChildAt(0);
} else {
TextView errorView = new TextView(getContext());
errorView.setClickable(true);
errorView.setTextColor(0xffff6600);
errorView.setGravity(Gravity.CENTER);
errorView.setTextSize(20);
errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?");
mContent = errorView;
addView(mContent);
}
if (mHeaderView != null) {
mHeaderView.bringToFront();
}
super.onFinishInflate();
}
...
原来该控件是在onFinishInflate()
中去加载子布局文件的,该方法的触发时机 加载完xml文件
,但通过ReactNative添加子布局并没有生成任何xml,所以肯定执行不了该方法.
但ReactNative端的子布局要加到PtrFrameLayout
中会触发ViewGroupManager
的addView
方法,可以在该方法中运行onFinishInflate
的方法,这样,子布局就会被加载了.
详细源码:
https://github.com/qq3061280/ReactNativeSimpleSource/tree/master/FirstProject
运行方法:
npm install
react-native run-android