ReactNative自定義控件之 RefreshLayout

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>
        )
    }
}
發佈了117 篇原創文章 · 獲贊 29 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章