自定義View----一個Demo帶你完全掌握View的滑動衝突(五)

先上圖:android

這裏寫圖片描述

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

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

一.佈局的下拉回彈實現ide

下拉回彈的實現本質其實就是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就不貼出來了。至此,一個簡單的下拉回彈就已經實現了,關於快速滑動以及慣性滑動感興趣的能夠加進去,這裏不是本篇博客的重點就不作討論了。this

二.子View的滾動實現spa

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

<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放到這個父佈局中便可。

源碼地址:

https://github.com/18722527635/AndroidArtStudy

相關文章
相關標籤/搜索