Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 從0開始自定義控件之 View 的滑動沖突詳解(四)

Android 從0開始自定義控件之 View 的滑動沖突詳解(四)

編輯:關於Android編程

前言

滑動沖突可以說每一個 Android 開發者都遇到過,雖然 Android 已經在如 ViewPager 這些控件內部處理了滑動沖突,但是在我們自己定義控件,或者一些復雜的布局情況下,依然要去解決滑動沖突的情況。
這一篇文章總結了下滑動沖突出現的場景,以及其中的規則和解決方法。

常見的滑動沖突場景

外部滑動方向和內部滑動方向不一致。 外部滑動方向和內部滑動方向一致。 上面兩種情況的嵌套。

第一種場景:
出現這種情況,主要的情景是ViewPager和Fragment組合使用時,ViewPager需要左右滑動,而其內部的控件需要上下滑動。雖然ViewPager內部已經為我們處理了這種沖突,但是我們在自定義控件時,有很大幾率遇到這種沖突情況,所以還是很必要了解下如何處理的。

第二種場景:
第二種情況比較特殊,因為外部控件和內部控件的滑動方向是一致的。那麼這時候,系統就不知道該去如何響應滑動了。在實際的開發中,出現這種情況,一般是內外層控件同時可以上下或者左右滑動。其他情況,則根據業務需求進行處理。比如ListView頭部有一個可下拉的刷新頭,那麼就要判斷ListView是否滑動到頂部,到頂部時滑動出現刷新頭。

第三種場景:
第三種則是前二種的組合嵌套情況,比較復雜。在處理時,需要一層層的解決沖突。

滑動沖突的處理規則

一般來說,不管滑動沖突有多麼復雜,都有一套規律,我們可以按照規律來進行一一解決。

第一種場景的處理規則:
根據用戶是水平還是垂直滑動來進行處理。主要是判斷用戶的滑動方向,當用戶左右滑動時,讓需要左右滑動的控件攔截事件進行處理。相反的,當用戶上下滑動時,則讓需要上下滑動的控件攔截事件進行處理。

那麼如何獲取用戶的滑動方向呢?主要是根據滑動過程中的兩個點之間的坐標就可以得出是水平還是垂直滑動。我們可以根據滑動路徑和水平方向所形成的夾角,也可以依據水平方向和豎直方向的距離差,某些特殊的情況下,還可以根據水平和垂直方向的速度差來做判斷。

一般情況下的話,都是根據水平和豎直方向的距離差來進行判斷的。比如豎直方向的滑動距離大就判斷為豎直滑動,否則為水平滑動。

第二種場景的處理規則:
第二種場景主要是根據業務上的不同來處理。比如說,業務上有規則,當在某種狀態時需要外部的View響應滑動,當在另一種狀態時,內部的View相應滑動。根據這種業務上的需求,我們就可以得出具體的規則了。

第三種場景的處理規則:
第三種的情況就復雜了,可能是第一種和第二種的“合體”,也可能是多重嵌套。這時候,也需要像第二種的處理規則一樣,根據業務來得出規則。

滑動沖突的解決方式

上面說過,針對場景一的解決方案,可以根據滑動的距離差來判斷,這個距離差就是所謂的滑動規則。但是要怎麼做才能夠將事件交給指定的View去處理呢?這裡就要用到事件分發機制了。針對滑動沖突,這裡給出兩種解決滑動沖突的方式:外部攔截法和內部攔截法。

外部攔截法

外部攔截法是指當父控件接收到事件後,判斷該事件是否需要,如果需要則就行攔截,否則就不攔截。外部攔截法,需要重寫父控件的onInterceptTouchEvent()方法,在內部做攔截。相應的偽代碼如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if(父容器需要點擊事件){
                intercepted = true;
            }else{
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    return intercepted;
}

上面的是外部攔截法的典型寫法,面對不同的滑動類型,只需要修改上面的判斷條件即可,其他不用修改也不能夠修改。這裡再簡要描述下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN這個事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因為一旦父容器攔截了ACTION_DOWN事件,那麼後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器進行處理,這個時候就沒有辦法再傳遞給子元素了。其次是ACTION_MOVE事件,這個事件可以根據需求來決定是否攔截,如果父容器需要攔截就返回true,否則返回false。最後是ACTION_UP事件,這裡也直接返回false。這是因為,如果父容器在ACTION_UP時返回true,就會導致子元素無法觸發onClick事件(在View的源碼中onClick事件是在ACTION_UP中觸發的)。

實例

下面用一個栗子來演示下外部攔截法的用法,這裡通過上篇文章中寫到的Scroller來實現一個簡易的ViewPager,並處理其與內部ListView的滑動沖突:

自定義ScrollerLayout:
/**
 * 作者:周游
 * 時間:2016/11/13
 * 博客:http://blog.csdn.net/airsaid
 */
public class ScrollerLayout extends ViewGroup {

    private final Scroller mScroller;
    private int mLeftBorder;
    private int mRightBorder;
    private float mXDown;
    private float mXMove;
    private float mXLastMove;
    private int mLastXIntercept;
    private int mLastYIntercept;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            // 為每一個子View測量大小
            measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(changed){
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childAt = getChildAt(i);
                // 為每一個子View重新布局
                childAt.layout(i * childAt.getMeasuredWidth(), 0
                        , (i + 1) * childAt.getMeasuredWidth(), childAt.getMeasuredHeight());
            }
            // 初始化左右邊界值
            mLeftBorder = getChildAt(0).getLeft();
            mRightBorder = getChildAt(getChildCount() - 1).getRight();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;

                intercepted = false;
                // 如果滑動沒有完成,就繼續由父控件處理
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                mXLastMove = mXMove;

                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                // 判斷是否左右滑動,是則攔截事件
                if(Math.abs(deltaX) > Math.abs(deltaY)){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
           case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                if(getScrollX() + scrolledX < mLeftBorder){
                    scrollTo(mLeftBorder, 0);
                    return true;
                }else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
                    scrollTo(mRightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                mScroller.startScroll(getScrollX(), 0 , dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}
布局:



    

代碼中,添加3個ListView:
public class MainActivity extends AppCompatActivity {

    private ScrollerLayout mLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mLayout = (ScrollerLayout) findViewById(R.id.scrollerLayout);

        for (int i = 0; i < 3; i++) {
            ListView listView = new ListView(this);
            List list =  new ArrayList<>();
            for (int i1 = 0; i1 < 50; i1++) {
                list.add("page" + i + ", name: " + i1);
            }
            ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list);
            listView.setAdapter(adapter);
            mLayout.addView(listView);
        }
    }

}

運行結果:
這裡寫圖片描述

內部攔截法

內部攔截法是指父容器不攔截任何事件,所有的事件傳遞給子元素,如果子元素需要此事件則直接消耗掉,否則就交由父控件進行處理。這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent()方法才能正常工作,使用起來比較外部攔截法要稍微復雜一點,我們需要重寫子元素的dispatchTouchEvent()方法,它的偽代碼如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            // 要求父控件不攔截事件
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if(父容器需要點擊事件){
                // 要求父控件攔截事件
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(ev);
}

上面代碼是內部攔截法的典型寫法,面對不同滑動類型,只需要修改上面的判斷條件即可,其他不用修改也不能夠修改。除了子元素要做處理之外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用parenet.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。

為什麼父容器不能攔截ACTION_DOWN事件呢? 這是因為 ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT 這個標記位的控制,所以一旦父容器攔截ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去,這樣內部攔截法就失去了作用了。父元素所做的修改如下:

switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
        intercepted = false;
        break;
    case MotionEvent.ACTION_MOVE:
        intercepted = true;
        break;
    case MotionEvent.ACTION_UP:
        intercepted = true;
        break;
}

實例

這裡根據上一個實例做出修改,首先是自定義一個ListView:

/**
 * 作者:周游
 * 時間:2016/11/20
 * 博客:http://blog.csdn.net/airsaid
 */
public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }

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

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

    private int mLastX;
    private int mLastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                // 要求父控件不攔截事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                // 如果是左右滑動
                if(Math.abs(deltaX) > Math.abs(deltaY)){
                    // 要求父控件攔截事件
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}

在代碼中將添加的ListView改為剛剛自定義好的ListView:

for (int i = 0; i < 3; i++) {
    MyListView listView = new MyListView(this);
    List list =  new ArrayList<>();
    for (int i1 = 0; i1 < 50; i1++) {
        list.add("page" + i + ", name: " + i1);
    }
    ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list);
    listView.setAdapter(adapter);
    mLayout.addView(listView);
}

修改父控件的onInterceptTouchEvent方法,攔截除ACTION_DOWN以外的事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            mXDown = ev.getRawX();
            mXLastMove = mXDown;

            intercepted = false;
            // 如果滑動沒有完成,就繼續由父控件處理
            if(!mScroller.isFinished()){
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            mXMove = ev.getRawX();
            mXLastMove = mXMove;

            intercepted = true;
            break;
        case MotionEvent.ACTION_UP:
            intercepted = true;
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

運行結果就不貼了,和上個例子一樣。對比兩種方式可以看出,實現的效果是一樣的,但是內部攔截法要稍微復雜了一些。

場景二和場景三的解決方案和上面說的解決方案都一樣,只不過滑動的規則不同而已。我們如果遇到了場景而和場景三的情況,只需要改變滑動規則就可以了。場景三可能要復雜一些,需要一層層的解決滑動沖突。

總結

解決滑動沖突,有一定的規則,萬變不離其宗,只要掌握了其中的規則,那麼多復雜的滑動沖突都可以游刃而解。不過前提還是需要了解事件的傳遞機制,這樣才能很清晰的知道事件傳遞到了哪裡。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved