一個Demo帶你徹底掌握View的滑動衝突

本文已授權微信公衆號:鴻洋(hongyangAndroid)在微信公衆號平臺原創首發。

最近在重新學習Android自定義View這一塊的內容,遇到了平時開發中經常碰到的一個棘手問題:View的滑動衝突。相信不少小夥伴都有相同的感覺,看似簡單真正做起來卻又不知道從何下手。今天就從一個簡單的Demo帶你徹底掌握解決View滑動衝突的辦法。

老規矩,先上圖:

這裏寫圖片描述

示例圖中是一個常見的下拉回彈,手指向下滑動的時候,整個佈局會一起滑動。下拉到一定距離的時候鬆手,佈局會自動回彈到開始的位置;手指向上滑動的時候,佈局的子View會滑動到最底部,然後手指再向下滑動,佈局的子View會滑動到最頂部,最後手指繼續向下滑動,整個佈局會一起滑動,下拉到一定距離後鬆手自動回彈到開始位置。

最終實現的效果如上所示,一起看看怎樣一步步實現最終的效果:

一.佈局的下拉回彈實現

下拉回彈的實現本質其實就是View的滑動,目前Android中實現View的滑動可以分爲三種方式:通過改變View的佈局參數使得View重新佈局從而實現滑動;通過scrollTo/scrollBy方法來實現View的滑動;通過動畫給View施加平移效果來實現滑動。這裏我們採用第一種方式來實現,考慮到整個佈局是豎直排列,我們可以直接自定義一個LinearLayout來作爲父佈局。然後調用layout(int l, int t, int r, int b)方法重新佈局,達到滑動的效果。

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private int i = 0;


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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if ((yMove - yDown) > 0) {
                    mMove = yMove - yDown;
                    i += mMove;
                    layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                break;
        }
        return true;
    }
}

MotionEvent.ACTION_DOWN: 獲取剛開始觸碰的y座標
MotionEvent.ACTION_MOVE: 如果是向下滑動,計算出每次滑動的距離與滑動的總距離,將每次滑動的距離作爲layout(int l, int t, int r, int b)方法的參數,重新進行佈局,達到佈局滑動的效果。
MotionEvent.ACTION_UP: 將滑動的總距離作爲layout(int l, int t, int r, int b)方法的參數,重新進行佈局,達到佈局自動回彈的效果。

此時的佈局文件是這樣的:

    <org.tyk.android.artstudy.MyParentView
        android:id="@+id/parent_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/divider"></View>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="70dp">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_marginLeft="10dp"
                        android:background="@drawable/b" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_marginLeft="80dp"
                        android:text="回到首頁"
                        android:textSize="20sp" />

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentRight="true"
                        android:layout_centerVertical="true"
                        android:layout_marginRight="10dp"
                       android:background="@drawable/right_arrow" />
                </RelativeLayout>
    </org.tyk.android.artstudy.MyParentView>

中間重複的RelativeLayout就不貼出來了。至此,一個簡單的下拉回彈就已經實現了,關於快速滑動以及慣性滑動感興趣的可以加進去,這裏不是本篇博客的重點就不做討論了。

二.子View的滾動實現

手指向下滑動的時候,佈局的下拉回彈已經實現,現在我希望手指向上滑動的時候,佈局的子View能夠滾動。平時接觸最多的能滾動的View就是ScrollView,所以我的第一反應就是在自定義的LinearLayout內,添加一個ScrollView,讓子View能夠滾動。說幹就幹:

 <org.tyk.android.artstudy.MyParentView
        android:id="@+id/parent_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
            </LinearLayout>
        </ScrollView>
 </org.tyk.android.artstudy.MyParentView>

興高采烈的加上去,最後運行的結果是:佈局完全變成了一個ScrollView,之前的下拉回彈效果已經完全消失!!!這顯然不是我期待的結果。

仔細分析一下這種現象,其實這就是常見的View滑動衝突場景之一:外部滑動方向與內部滑動方向一致。父佈局MyParentView需要響應豎直方向上的向下滑動,實現下拉回彈,子佈局ScrollView也需要響應豎直方向上的上下滑動,實現子View的滾動。當內外兩層都在同一個方向上可以滑動的時候,就會出現邏輯問題。因爲當手指滑動的時候,系統無法知道用戶想讓哪一層滑動。所以這種場景下的滑動衝突需要我們手動去解決。

解決辦法:
外部攔截法:外部攔截法是指點擊事件先經過父容器的攔截處理,如果父容器需要處理此事件就進行攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突的問題。外部攔截法需要重寫父容器的onInterceptTouchEvent()方法,在內部做相應的攔截即可。

具體實現:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (yMove - yDown < 0) {
                    isIntercept = false;
                } else if (yMove - yDown > 0) {
                    isIntercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return isIntercept;
    }

實現分析:
在自定義的父佈局中重寫onInterceptTouchEvent()方法,MotionEvent.ACTION_MOVE的時候,進行判斷。如果手指是向上滑動,onInterceptTouchEvent()返回false,表示父佈局不攔截當前事件,當前事件交給子View處理,那麼我們的子View就能滾動;如果手指是向下滑動,onInterceptTouchEvent()返回true,表示父佈局攔截當前事件,當前事件交給父佈局處理,那麼我們父佈局就能實現下拉回彈。

三.連續滑動的實現

剛開始我以爲這樣就萬事大吉了,可後來我又發現一個很嚴重的問題:手指向上滑動的時候,子View開始滾動,然後手指再向下滑動,整個父佈局開始向下滑動,鬆手後便自動回彈。也就是說,剛纔滾動的子View已經回不到開始的位置。仔細分析一下其實這結果是意料之中的,因爲只要我手指是向下滑動,onInterceptTouchEvent()便返回true,父佈局會攔截當前事件。這裏其實又是上面提到的View滑動衝突:理想的結果是當子View滾動後,如果子View沒有滾動到開始的位置,父佈局就不要攔截滑動事件;如果子View已經滾動到開始的位置,父佈局就開始攔截滑動事件。

解決辦法:
內部攔截法:內部攔截法是指點擊事件先經過子View處理,如果子View需要此事件就直接消耗掉,否則就交給父容器進行處理,這樣就可以解決滑動衝突的問題。內部攔截法需要配合requestDisallowInterceptTouchEvent()方法,來確定子View是否允許父佈局攔截事件。

具體實現:

public class MyScrollView extends ScrollView {


    public MyScrollView(Context context) {
        this(context, null);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:

                int scrollY = getScrollY();
                if (scrollY == 0) {
                    //允許父View進行事件攔截
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    //禁止父View進行事件攔截
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return super.onTouchEvent(ev);

    }
}

實現分析:
自定義一個ScrollView,重寫onTouchEvent()方法,在MotionEvent.ACTION_MOVE的時候,得到滑動的距離。如果滑動的距離爲0,表示子View已經滾動到開始位置,此時調用 getParent().requestDisallowInterceptTouchEvent(false)方法,允許父View進行事件攔截;如果滑動的距離不爲0,表示子View沒有滾動到開始位置,此時調用 getParent().requestDisallowInterceptTouchEvent(true)方法,禁止父View進行事件攔截。這樣只要子View沒有滾動到開始的位置,父佈局都不會攔截事件,一旦子View滾動到開始的位置,父佈局就開始攔截事件,形成連續的滑動。

好了,針對其他場景更復雜的滑動衝突,解決滑動衝突的原理與方式無非就是這兩種方法。希望看完本篇博客能對你有所幫助,下一篇再見~~~

寫在最後:

昨天一直忙到下午纔有時間去看博客,看到這篇博客評論下面炸開了鍋。這裏有幾個問題說明一下:

關於Denon源碼的問題,因爲這個Demo的源碼不是單獨的,合集打包下來有30多M,所以當時就沒傳上去。我相信按照文章所說的步驟來,肯定會實現最後的效果,最後我上傳的源碼與文章代碼是一模一樣的,這一點我是百分百保證的。

關於Demo存在的問題,這個問題是真實存在的:

這裏寫圖片描述

謝謝這位小夥伴,我當時也立即回覆了他,今天我把這個問題解決了。

public class MyScrollView extends ScrollView {


    private scrollTopListener listener;

    public void setListener(scrollTopListener listener) {
        this.listener = listener;
    }

    public MyScrollView(Context context) {
        this(context, null);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {


            case MotionEvent.ACTION_MOVE:

                int scrollY = getScrollY();
                if (scrollY == 0) {
                    //允許父View進行事件攔截
                    getParent().requestDisallowInterceptTouchEvent(false);
                    listener.scrollTop();
                } else {
                    //禁止父View進行事件攔截
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return super.onTouchEvent(ev);

    }


    public interface scrollTopListener {
        void scrollTop();
    }


}

給自定義的ScrollView添加一個接口,監聽是否滑到開始的位置。

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private boolean isIntercept;
    private int i = 0;
    private MyScrollView myScrollView;
    private boolean isOnTop;


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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        myScrollView = (MyScrollView) getChildAt(0);
        myScrollView.setListener(new MyScrollView.scrollTopListener() {
            @Override
            public void scrollTop() {
                isOnTop = true;
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                //上滑
                if (yMove - yDown < 0) {
                    isIntercept = false;
                    //下滑
                } else if (yMove - yDown > 0) {
                    isIntercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return isIntercept;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (isOnTop) {
                    yDown = y;
                    isOnTop = false;
                }
                if (isIntercept && (yMove - yDown) > 0) {
                    mMove = yMove - yDown;
                    i += mMove;
                    layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                isIntercept = false;
                break;
        }

        return true;
    }


}

自定義的父佈局中,實現這個接口,然後在MotionEvent.ACTION_MOVE的時候,進行判斷:

if (isOnTop) {
yDown = y;
isOnTop = false;
}

如果滑動到頂部,就讓yDown的初始值爲(int) event.getY(),這樣就不會出現閃的問題,滑動也更加自然流暢。

關於Demo的優化與改進,我很感謝這位小夥伴:

這裏寫圖片描述

他用不同的方式實現了一樣的效果,並且還把源碼發到了我的郵箱。實現的效果一模一樣,並且只用了自定義的父佈局加外部攔截法,貼一下代碼:

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private int i = 0;
    private boolean isIntercept = false;

    public MyParentView(Context context) {
        super(context);
    }

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

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

    private ScrollView scrollView;

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        scrollView = (ScrollView) getChildAt(0);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        onInterceptTouchEvent(ev);
        return super.dispatchTouchEvent(ev);
    }

       @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int y = (int) ev.getY();
        int mScrollY = scrollView.getScrollY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (yMove - yDown > 0 && mScrollY == 0) {
                    if (!isIntercept) {
                        yDown = (int) ev.getY();
                        isIntercept = true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                isIntercept = false;
                break;
        }
        if (isIntercept) {
            mMove = yMove - yDown;
            i += mMove;
            layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
        }
        return isIntercept;
    }
}

這樣就不用自定義一個ScrollView,直接將原生的ScrollView放到這個父佈局中即可。大家可以試試他的方法,點個大大的贊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章