ScrollView嵌套ListView手勢沖突

外層ScrollView,內嵌ListView,都是垂直方向。采用內部攔截法,實現ListView能滾動時則讓ListView處理,當ListView滑到頂部或者底部不能滑動時讓ScrollView處理


布局
上面有一段文本,中間是ListView,下面還有一段文本

<?xml version="1.0" encoding="utf-8"?>
<org.icegeneral.scroll.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sv"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="AAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA\nAAAAAAAAAA" />

        <org.icegeneral.scroll.MyListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#888888">

        </org.icegeneral.scroll.MyListView>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="BBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB\nBBBBBBBBBB" />
    </LinearLayout>
</org.icegeneral.scroll.MyScrollView>

MyScrollView
ACTION_DOWN必須讓給ListView,ListView才能收到ACTION_MOVE,ListView才能判斷自己是否還能滾動
第二點要注意的是:因為ACTION_DOWN讓給ListView,那么ACTION_DOWN就無法進入ScrollView的onTouchEvent, 但是ScrollView的滾動需要在ACTION_DOWN階段做一些準備,所以主動調用了一次

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev);
            return false;
        }
        return true;
    }

}

MyListView

public class MyListView extends ListView {

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private float lastY;

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            if (lastY > ev.getY()) {
                // 如果是向上滑動,且不能滑動了,則讓ScrollView處理
                if (!canScrollList(1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            } else if (ev.getY() > lastY) {
                // 如果是向下滑動,且不能滑動了,則讓ScrollView處理
                if (!canScrollList(-1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }
        lastY = ev.getY();
        return super.dispatchTouchEvent(ev);
    }

}

Activity

protected void onCreate(Bundle savedInstanceState) {
    ....
    scrollView.smoothScrollTo(0, 0);
}

ACTION_DOWN的特殊性
解釋下為什么ListView的ACTION_UP無需調用
getParent().getParent().requestDisallowInterceptTouchEvent(false)
來看下ViewGroup源碼

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState(); //重點是這句
        }

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
    }
    ...
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; //移除FLAG_DISALLOW_INTERCEPT
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

所以如果是ACTION_DOWN,會調用resetTouchState(),移除FLAG_DISALLOW_INTERCEPT,所以ACTION_DOWN時,parent一定會進入onInterceptTouchEvent

其他
其實現在都用RecyclerView代替ListView,ScrollView嵌套RecyclerView時,對于手勢已經支持得很好,不必自己處理沖突。這個在demo里也有寫,做個對比

代碼

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

推薦閱讀更多精彩內容