1. 程式人生 > >ScrollView巢狀ListView處理事件衝突

ScrollView巢狀ListView處理事件衝突

當ListView巢狀在ScrollView中時會有兩個問題

  1. 列表內容顯示不全

  2. 滑動事件衝突

第二個問題就是下面要講的廢話了。

一、首先要實現的效果是

  1. 手指在ListView中滑動時,滑動事件要交給ListView來處理,也就是說手指在ListView的內容區域中可以上下滑動。

  2. ListView中的內容滑動到頂部後如果手指還是繼續向下滑(自己模擬一下),此時的滑動事件應該交給ScrollView來處理,也就是說ScrollView可以繼續滑動。

  3. ListView中的內容滑動到底部後如果手指還是繼續向上滑(自己模擬一下),此時的滑動事件應該交給ScrollView來處理,也就是說ScrollView可以繼續滑動。

下面就來一個一個的實現上面所列出的效果

二、手指在ListView中滑動時,滑動事件要交給ListView來處理

1.首先如果不做任何處理,ListView巢狀在ScrollView中時,預設滑動事件是被ScrollView處理掉的,效果是這樣的:

1203_scrollview_move

我們都知道ViewGroup預設是不攔截事件的,看一下ViewGroup的原始碼就知道:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

而ScrollView是繼承自FrameLayout的,那為什麼ScrollView會自己處理掉滑動事件呢,到ScrollView的原始碼裡一搜,在onInterceptTouchEvent方法中居然有這麼觸目驚心的一段:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }

    if (getScrollY() == 0 && !canScrollVertically(1)) {
        return false;
    }

    switch
(action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId+ " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; if (mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); mIsBeingDragged = !mScroller.isFinished(); if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } startNestedScroll(SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } stopNestedScroll(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } return mIsBeingDragged; }

(1). 首先看到的是如果ScrollView接收到MOVE事件,並且這個mIsBeingDragged為true,它的onInterceptTouchEvent方法直接就返回true了,也就是攔截了滑動事件,交給它自己處理了。

(2). 接著看switch裡面case MotionEvent.ACTION_DOWN,如果按下的時候手指落在了子控制元件裡面mIsBeingDragged置為false。

裡面還有這麼一句mIsBeingDragged = !mScroller.isFinished();,mIsBeingDragged就表示當前ScrollView是否在滑動

這mIsBeingDragged有啥卵用呢?聯絡上面1中所說,假如ScrollView還在滑動的時候,你想去觸控嵌在裡面的ListView,沒門,ScrollView滑動還沒結束呢,繼續直接return true。

另外要說的是,不管咋樣ScrollView並不會把ListView的點選事件給攔截掉。

(3). 再來看看switch裡面case MotionEvent.ACTION_MOVE,如果y軸方向上的滑動距離大於最小滑動距離,則將mIsBeingDragged設定為true。結合上面第二點所說,啥情況呢?也就是說雖然我手指落在了子View裡面,但是如果我要滑動的話,誰也攔不住老紙(Parent)!!

從上面幾點來看,ScrollView確實預設會自己處理掉滑動事件。我們想想事件分發的流程,如果父控制元件攔截了事件,子控制元件就沒辦法接收到事件了。那如何才能讓ListView來處理滑動事件呢,接著說。

2.想要讓ListView來處理滑動事件,首先要重寫它的dispatchTouchEvent方法

我們繼承ListView實現自己的一個MyListView,重寫它的dispatchTouchEvent方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.e(TAG, TAG + "dispatchTouchEvent");

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, TAG + "dispatchTouchEvent -> MotionEvent.ACTION_DOWN");
            downY = ev.getRawY();
            y = downY;
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        ......
    }
}

在它的 MotionEvent.ACTION_DOWN 事件中呼叫: getParent().requestDisallowInterceptTouchEvent(true);

這個’requestDisallowInterceptTouchEvent’真繞口,就是說

我[ListView]的父親啊[getParent()]請求您[request]行行好別讓[Disallow]您的onInterceptTouchEvent方法再攔截我的事件了啊[true]

因此如果你在ListView中呼叫了這個方法之後,父控制元件(ScrollView)就不會攔截ListView的滑動事件了。ListView的內容也就可以正常滑動了。

三、ListView中的內容滑動到頂部後以及滑動到底部後,事件應該交給ScrollView來處理

在MyListView中實現這兩個方法:

public boolean scrollToBottom() {
    int first = getFirstVisiblePosition();
    int last = getLastVisiblePosition();
    int visibleCoutn = getChildCount();
    int count = getCount();
    if ((first + visibleCoutn) == count) {
        return true;
    }
    return false;
}

public boolean scrollToTop() {
    int first = getFirstVisiblePosition();
    int last = getLastVisiblePosition();
    int visibleCoutn = getChildCount();
    int count = getCount();

    if (first == 0) {
        return true;
    }
    return false;
}

一個用於判斷ListView是否滑動到底部,一個用於判斷ListView是否滑動到頂部。

接著繼續重寫dispatchTouchEvent方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.e(TAG, TAG + "dispatchTouchEvent");

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downY = ev.getRawY();
            y = downY;
            getParent().requestDisallowInterceptTouchEvent(true);
            break;

        case MotionEvent.ACTION_MOVE:
            y = ev.getRawY();
            if (scrollToTop()) {
                if (y - downY > mTouchSlop) {
                    /**
                     * Point 1 : 如果滑動到頂部,並且手指還想向下滑動,則事件交還給父控制元件,要求父控制元件可以攔截事件
                     */
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                } else if (y - downY < -mTouchSlop) {
                    /**
                     * Point 2 : 如果滑動到頂部,並且手指正常向上滑動,則事件由自己處理,要求父控制元件不許攔截事件
                     */
                    getParent().requestDisallowInterceptTouchEvent(true);

                }

            }

            if (scrollToBottom()) {
                if (y - downY < -mTouchSlop) {
                    /**
                     * Point 3 : 如果滑動到底部,並且手指還想向上滑動,則事件交還給父控制元件,要求父控制元件可以攔截事件
                     */
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                } else if (y - downY > mTouchSlop) {
                    /**
                     * Point 4 : 如果滑動到底部,並且手指正常向下滑動,則事件由自己處理,要求父控制元件不許攔截事件
                     */
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }

            break;

        case MotionEvent.ACTION_UP:
            break;

        default:
            break;
    }
    return super.dispatchTouchEvent(ev);
}

很簡單,看註釋,實現的效果:

1203_scroll_lv

THE END.