天天看點

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

近期在又一次學習Android自己定義View這一塊的内容。遇到了平時開發中常常碰到的一個棘手問題:View的滑動沖突。相信不少小夥伴都有同樣的感覺。看似簡單真正做起來卻又不知道從何下手。

今天就從一個簡單的Demo帶你徹底掌握解決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存在的問題。這個問題是真實存在的:

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

謝謝這位小夥伴。我當時也馬上回複了他。今天我把這個問題攻克了。

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的優化與改進。我非常感謝這位小夥伴:

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

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

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放到這個父布局中就可以。大家能夠試試他的方法。