先上圖: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放到這個父佈局中便可。
源碼地址: