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