1. 程式人生 > >Android:SwipeRefreshView巢狀DockingExpandableListView 懸停標題外加上拉重新整理下拉載入

Android:SwipeRefreshView巢狀DockingExpandableListView 懸停標題外加上拉重新整理下拉載入

我這個是參考之前的哥們做的,跟我們專案需求很類似,有些出入,我做了些改動,並嵌套了上了重新整理和下拉載入功能,巢狀過程終於到了些問題,並都已解決。這是原始DockingExpandableListView懸停標題文章的連結

下面給大家看我的演示圖:


註明下,我這個是預設展開,且不可摺疊的,我把摺疊點選事件禁止了,如有需要,找不到的可以聯絡我

一·  SwipeRefreshView巢狀DockingExpandableListView的滑動衝突問題

這個問題已經在定義的SwipeRefreshView解決了,重寫listview的SetOnScrollListener

 mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                // 移動過程中判斷時候能下拉載入更多
                if (canLoadMore()) {
                    // 載入資料
                    loadData();
                }
            }
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                //解決SwipeRefreshView巢狀DockingExpandableListView引起的滑動衝突和父標題懸停滾動效果失效問題
                boolean enable = false;
                if (mListView != null && mListView.getChildCount() > 0) {
                    // check if the first item of the list is visible
                    boolean firstItemVisible = mListView.getFirstVisiblePosition() == 0;
                    // check if the top of the first item is visible
                    boolean topOfFirstItemVisible = mListView.getChildAt(0).getTop() == 0;
                    // enabling or disabling the refresh layout
                    enable = firstItemVisible && topOfFirstItemVisible;
                }
                setEnabled(enable);//設定SwipeRefreshView是否獲取焦點
                
                //當listview重新獲得焦點的時候,繪製滾動懸停效果
                long packedPosition = mListView.getExpandableListPosition(firstVisibleItem);
                int groupPosition = mListView.getPackedPositionGroup(packedPosition);
                int childPosition = mListView.getPackedPositionChild(packedPosition);
                // update header view based on first visible item  // IMPORTANT: refer to getPackedPositionChild():  // If this group does not contain a child, returns -1. Need to handle this case in controller.
                mListView.updateDockingHeader(groupPosition, childPosition);
            }
        });

二·重寫onMeasure()和onLayout()方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mDockingHeader != null) {
            measureChild(mDockingHeader, widthMeasureSpec, heightMeasureSpec);
            mDockingHeaderWidth = mDockingHeader.getMeasuredWidth();
            mDockingHeaderHeight = mDockingHeader.getMeasuredHeight();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (mDockingHeader != null) {
            mDockingHeader.layout(0, 0, mDockingHeaderWidth, mDockingHeaderHeight);
        }
    }
重寫dispatchDraw()方法

懸停標題是畫上去的,而不是加到view hierarchy裡去的。因此,需要在完成其他子view的繪製之後,再把懸停標題欄畫上去:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mDockingHeaderVisible) {
            // draw header view instead of adding into view hierarchy
            drawChild(canvas, mDockingHeader, getDrawingTime());
        }
    }

四、根據滾動狀態決定如何繪製懸停標題
這個問題我寫到SwipeRefreshView中了

滾動到不同位置,懸停標題的顯示是不同的,因此需要根據滾動狀態定義一個狀態機的切換。讓SwipeRefreshView中的listview物件實現OnScrollListener監聽,並重寫onScroll()方法:



最為關鍵的updateDockingHeader()方法,根據狀態機來確定如何繪製懸停標題。在看這個方法之前,我們先看一下有哪幾種狀態,定義在IDockingController裡:

public interface IDockingController {
    int DOCKING_HEADER_HIDDEN = 1;
    int DOCKING_HEADER_DOCKING = 2;
    int DOCKING_HEADER_DOCKED = 3;

    int getDockingState(int firstVisibleGroup, int firstVisibleChild);
}

一共3種狀態,含義參見下圖:

DOCKING_HEADER_HIDDEN:當分組沒有展開,或者組裡沒有子項的時候,是不需要繪製懸停標題的

DOCKING_HEADER_DOCKING:當滾動到上一個分組的最後一個子項時,需要把舊的標題“推”出去,“停靠”新的標題,所以這個狀態命名為“docking”

DOCKING_HEADER_DOCKED:新標題“停靠”完畢,在該分組內部滾動,稱為“docked”狀態

基於這個狀態機,我們來看一下updateDockingHeader()方法的實現:

 private void updateDockingHeader(int groupPosition, int childPosition) {
        if (getExpandableListAdapter() == null) {
            return;
        }

        if (getExpandableListAdapter() instanceof IDockingController) {
            IDockingController dockingController = (IDockingController)getExpandableListAdapter();
            mDockingHeaderState = dockingController.getDockingState(groupPosition, childPosition);
            switch (mDockingHeaderState) {
                case IDockingController.DOCKING_HEADER_HIDDEN:
                    mDockingHeaderVisible = false;
                    break;
                case IDockingController.DOCKING_HEADER_DOCKED:
                    if (mListener != null) {
                        mListener.onUpdate(mDockingHeader, groupPosition, isGroupExpanded(groupPosition));
                    }
                    // Header view might be "GONE" status at the beginning, so we might not be able
                    // to get its width and height during initial measure procedure.
                    // Do manual measure and layout operations here.
                    mDockingHeader.measure(
                            MeasureSpec.makeMeasureSpec(mDockingHeaderWidth, MeasureSpec.AT_MOST),
                            MeasureSpec.makeMeasureSpec(mDockingHeaderHeight, MeasureSpec.AT_MOST));
                    mDockingHeader.layout(0, 0, mDockingHeaderWidth, mDockingHeaderHeight);
                    mDockingHeaderVisible = true;
                    break;
                case IDockingController.DOCKING_HEADER_DOCKING:
                    if (mListener != null) {
                        mListener.onUpdate(mDockingHeader, groupPosition, isGroupExpanded(groupPosition));
                    }

                    View firstVisibleView = getChildAt(0);
                    int yOffset;
                    if (firstVisibleView.getBottom() < mDockingHeaderHeight) {
                        yOffset = firstVisibleView.getBottom() - mDockingHeaderHeight;
                    } else {
                        yOffset = 0;
                    }

                    // The yOffset is always non-positive. When a new header view is "docking",
                    // previous header view need to be "scrolled over". Thus we need to draw the
                    // old header view based on last child's scroll amount.
                    mDockingHeader.measure(
                            MeasureSpec.makeMeasureSpec(mDockingHeaderWidth, MeasureSpec.AT_MOST),
                            MeasureSpec.makeMeasureSpec(mDockingHeaderHeight, MeasureSpec.AT_MOST));
                    mDockingHeader.layout(0, yOffset, mDockingHeaderWidth, mDockingHeaderHeight + yOffset);
                    mDockingHeaderVisible = true;
                    break;
            }
        }
    }

其中,是否顯示懸停標題是通過一個叫做mDockingHeaderVisible的boolean變數控制的,這個在上面的dispatchDraw()方法裡也見到了。

重點看“docking”狀態的處理:通過計算第一個可見項的bottom和高度之間的差異,也就是這個yOffset,確定懸停標題在y軸方向的偏移量。這樣在繪製懸停標題的時候,我們就只能看到一部分,造成一種被“推出去”的感覺。


五、懸停標題狀態機

在剛剛提到的那個IDockingController接口裡有一個方法叫getDockingState(),在updateDockingHeader()方法裡就是通過呼叫這個方法來確定當前懸停標題的狀態的。DockingExpandableListViewAdapter實現了該介面和方法,完成狀態機狀態轉換:

@Override
    public int getDockingState(int firstVisibleGroup, int firstVisibleChild) {
        // No need to draw header view if this group does not contain any child & also not expanded.
        if (firstVisibleChild == -1 && !mListView.isGroupExpanded(firstVisibleGroup)) {
            return DOCKING_HEADER_HIDDEN;
        }

        // Reaching current group's last child, preparing for docking next group header.
        if (firstVisibleChild == getChildrenCount(firstVisibleGroup) - 1) {
            return IDockingController.DOCKING_HEADER_DOCKING;
        }

        // Scrolling inside current group, header view is docked.
        return IDockingController.DOCKING_HEADER_DOCKED;
    }

邏輯非常簡單清晰:

如果當前group沒有子項,並且也不是展開狀態,就返回DOCKING_HEADER_HIDDEN狀態,不繪製懸停標題;

如果到達了當前group的最後一個子項,進入DOCKING_HEADER_DOCKING狀態;

其他情況,在當前group內部滾動,返回DOCKING_HEADER_DOCKED狀態。

六、Touch事件處理

文章最前面提到過,這個標題檢視是畫上去,而不是新增到view hierarchy裡的,因此它是無法響應touch事件的!那就需要我們自己根據點選區域進行判斷了,需要重寫onInterceptTouchEvent()和onTouchEvent()方法,又因為我這裡需求是不折疊的,所以我把點選事件都禁掉了:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN && mDockingHeaderVisible) {
            Rect rect = new Rect();
            mDockingHeader.getDrawingRect(rect);
            if (rect.contains((int)ev.getX(), (int)ev.getY())
                    && mDockingHeaderState == IDockingController.DOCKING_HEADER_DOCKED) {
                // Hit header view area, intercept the touch event
                return false;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mDockingHeaderVisible) {
            Rect rect = new Rect();
            mDockingHeader.getDrawingRect(rect);

            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if (rect.contains((int)ev.getX(), (int)ev.getY())) {
                        // forbid event handling by list view's item
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    long flatPostion = getExpandableListPosition(getFirstVisiblePosition());
                    int groupPos = ExpandableListView.getPackedPositionGroup(flatPostion);
                    if (rect.contains((int)ev.getX(), (int)ev.getY()) &&
                            mDockingHeaderState == IDockingController.DOCKING_HEADER_DOCKED) {
                        // handle header view click event (do group expansion & collapse)
//                        if (isGroupExpanded(groupPos)) {
//                            collapseGroup(groupPos);
//                        } else {
//                            expandGroup(groupPos);
//                        }
                        return false;
                    }
                    break;
            }
        }

        return super.onTouchEvent(ev);
    }

七、更新標題檢視內容

前面已經完成了懸停標題狀態機的控制,但是具體標題欄上應該怎麼顯示(比如變更標題文字、顯示收縮展開圖示等等),需要使用者來處理。因此定義了一個IDockingHeaderUpdateListener介面,使用者需要實現onUpdate()方法,根據當前的group ID以及收縮展開狀態決定如何更新懸停標題檢視:

public interface IDockingHeaderUpdateListener {
    void onUpdate(View headerView, int groupPosition, boolean expanded);
}
我在DemoDockingAdapterDataSource加了一個方法,更新標題可以這樣動態設定:
public String getGroupname(int groupPosition) {
        if (mGroups.get(groupPosition) != null) {
            return mGroups.get(groupPosition);
        }

        return null;
    }
然後在MainActivity中呼叫,這樣也去分了分頁載入的不同父級標題,父級標題名字如果相同,預設是不顯示的:
listView.setDockingHeader(headerView, new IDockingHeaderUpdateListener() {
            @Override
            public void onUpdate(View headerView, int groupPosition, boolean expanded) {

                String groupTitle =   listData.getGroupname(groupPosition);
                TextView titleView = (TextView) headerView.findViewById(R.id.group_view_title);
                titleView.setText(groupTitle);
            }
        });

Adapter的資料來源我就不貼了,還有不明白的可以看下面原始碼,也可以給我私信,希望我的版本能幫你解決問題,謝謝!!