React Native 之封裝Android 的ViewGroup

Ultra-Pull-to-Refresh 框架介紹

在原生的Android端,最火的下拉刷新就是liaohuqiuandroid-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之后就需要將它ReactPackagecreateViewManagers()方法中,最后將ReactPackage注冊到MainApplicationgetPackages()方法里.

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中會觸發ViewGroupManageraddView方法,可以在該方法中運行onFinishInflate的方法,這樣,子布局就會被加載了.

詳細源碼:

https://github.com/qq3061280/ReactNativeSimpleSource/tree/master/FirstProject

運行方法:

npm install

react-native run-android

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,150評論 25 708
  • 峰高腳下已千尺, 云鳥橫空伸手擒。 來路盡隨西日暮, 忽聽泉瀑響松林。
    飛飛_b4dc閱讀 338評論 2 11
  • 冬至,二十四節氣中陰極陽生的日子。 冬至俗稱數九,也就是九九的開始。在中國北方有冬至吃餃子的風俗。俗話說:“冬至到...
    溪蘭弦語閱讀 338評論 1 2
  • 聽歌,刷微博,看劇,看視頻這幾件事倘若每天不停息的進行,不會有人厭煩,相反,早起,工作,寫計劃,單單幾個就會想放棄
    長命百歲吧妮姐閱讀 131評論 0 0
  • 當你不在 她不在 他不在 我不得不一個人 一個人 往前 往前走往前闖往前行 習慣了一個人的時光 卻在黑夜里 抱...
    角落蜷縮閱讀 156評論 0 1