敘述
滑動沖突可以說是日常開發(fā)中比較常見的一類問題,也是比較讓人頭疼的一類問題,尤其是在使用第三方框架的時候蟀悦,兩個原本完美的控件,組合在一起之后氧敢,忽然發(fā)現整個世界都不好了日戈。
關于滑動沖突
滑動沖突分類###
滑動沖突,總的來說就是兩類孙乖。
同方向滑動沖突
比如ScrollView嵌套ListView浙炼,或者是ScrollView嵌套自己不同方向滑動沖突
比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView的圆,這種情況其實很典型」呐。現在大部分應用最外層都是ViewPager+Fragment 的底部切換(比如微信)結構半火,這種時候越妈,就很容易出現滑動沖突。不過ViewPager里面無論是嵌套ListView還是ScrollView钮糖,滑動沖突是沒有的梅掠,畢竟是官方的東西酌住,可能已經考慮到了這些,所以比較完善阎抒。
復雜一點的滑動沖突酪我,基本上就是這兩個沖突結合的結果。
滑動沖突解決思路###
滑動沖突且叁,就其本質來說都哭,兩個不同方向(或者是同方向)的View,其中有一個是占主導地位的逞带,每次總是搶著去處理外界的滑動行為欺矫,這樣就導致一種很別扭的用戶體驗,明明只是橫向的滑動了一下展氓,縱向的列表卻在垂直方向發(fā)生了動作穆趴。就是說,這個占主導地位的View遇汞,每一次都身不由己的攔截了這個滑動的動作未妹,因此,要解決滑動沖突空入,就是得明確告訴這個占主導地位的View络它,什么時候你該攔截,什么時候你不應該攔截歪赢,應該由下一層的View去處理這個滑動動作酪耕。
這里不明白的同學,可以去了解一下Android Touch事件的分發(fā)機制轨淌,這也是解決滑動沖突的核心知識迂烁。
第二種滑動沖突,解決起來是比較簡單的递鹉。這里就結合例子說一下盟步。
滑動沖突
這里,說一下背景情況躏结。之前做下拉刷新却盘、上拉加載更多時一直使用的是PullToRefreshView這個控件,因為很方便媳拴,不用導入三方工程黄橘。在其內部可以放置ListView,GridView及ScrollView屈溉,非常方便塞关,用起來可謂是屢試不爽。但是直到有一天子巾,因項目需要帆赢,在ListView頂部加了一個輪播圖控件BannerView(這個可以參考之前寫的一篇學習筆記)小压。結果發(fā)現輪播圖滑動的時候,和縱向的下拉刷新組件沖突了椰于。
如之前所說怠益,解決滑動沖突的關鍵,就是明確告知接收到Touch的View瘾婿,是否需要攔截此次事件蜻牢。
解決方法
解決方案1,從外部攔截機制考慮###
這里偏陪,相當于是PullToRefreshView嵌套了ViewPager孩饼,那么每次優(yōu)先接收到Touch事件的必然是PullToRefreshView。因為正常情況下竹挡,父控件會優(yōu)先接收到touch事件镀娶。這樣就清楚了,看代碼:
在PullToRefreshView的onInterceptTouchEvent方法中:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int y = (int) e.getRawY();
int x = (int) e.getRawX();
boolean resume = false;
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// 發(fā)生down事件時,記錄y坐標
mLastMotionY = y;
mLastMotionX = x;
resume = false;
break;
case MotionEvent.ACTION_MOVE:
// deltaY > 0 是向下運動,< 0是向上運動
int deltaY = y - mLastMotionY;
int deleaX = x - mLastMotionX;
if (Math.abs(deleaX) > Math.abs(deltaY)) {
resume = false;
} else {
//當前正處于滑動
if (isRefreshViewScroll(deltaY)) {
resume = true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return resume;
}
這里最關鍵的代碼就是這行
if (Math.abs(deleaX) > Math.abs(deltaY)) {
resume = false;
}
橫向滑動距離大于縱向時揪罕,無須攔截這次滑動事件梯码,滑動事件會傳遞到下一層的view,也就是這里的輪播圖控件好啰,這樣橫向滑動輪播圖的時候轩娶,PullToRefreshView就不會有下拉的動作了。其實框往,就是這么簡單鳄抒,但前提是你必須明確了解Android Touch事件的傳遞機制,期間各個方法執(zhí)行的順序及意義椰弊。
ps: 關于上文中提到的isRefreshViewScroll 方法代碼(這個方法其實是PullToRefreshView這個控件自帶的一個方法)
/** * 是否應該到了父View,即PullToRefreshView滑動
*
* @param deltaY , deltaY > 0 是向下運動,< 0是向上運動
* @return
*/
private boolean isRefreshViewScroll(int deltaY) {
if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
return false;
}
// 對于ListView和GridView
if (mAdapterView != null) {
// 子view(ListView or GridView)滑動到最頂端
if (deltaY > 0) {
View child = mAdapterView.getChildAt(0);
if (child == null) {
// 如果mAdapterView中沒有數據,不攔截
return false;
}
if (mAdapterView.getFirstVisiblePosition() == 0
&& child.getTop() == 0) {
mPullState = PULL_DOWN_STATE;
return true;
}
int top = child.getTop();
int padding = mAdapterView.getPaddingTop();
if (mAdapterView.getFirstVisiblePosition() == 0
&& Math.abs(top - padding) <= 8) {// 這里之前用3可以判斷,但現在不行,還沒找到原因
mPullState = PULL_DOWN_STATE;
return true;
}
} else if (deltaY < 0) {
View lastChild = mAdapterView.getChildAt(mAdapterView
.getChildCount() - 1);
if (lastChild == null) {
// 如果mAdapterView中沒有數據,不攔截
return false;
}
// 最后一個子view的Bottom小于父View的高度說明mAdapterView的數據沒有填滿父view,
// 等于父View的高度說明mAdapterView已經滑動到最后
if (lastChild.getBottom() <= getHeight()
&& mAdapterView.getLastVisiblePosition() == mAdapterView
.getCount() - 1) {
mPullState = PULL_UP_STATE;
return true;
}
}
}
// 對于ScrollView
if (mScrollView != null) {
// 子scroll view滑動到最頂端
View child = mScrollView.getChildAt(0);
if (deltaY > 0 && mScrollView.getScrollY() == 0) {
mPullState = PULL_DOWN_STATE;
return true;
} else if (deltaY < 0
&& child.getMeasuredHeight() <= getHeight()
+ mScrollView.getScrollY()) {
mPullState = PULL_UP_STATE;
return true;
}
}
return false;
解決方案2许溅,從內容逆向思維分析###
有時候,我們不想去修改或者是無法修改最先接收到Touch事件的View 時秉版,比如這里我不想去修改PullToRefreshView的代碼贤重。就必須考慮從當前從Touch傳遞事件中最后的那個View逆向考慮。首先清焕,由Android中View的Touch事件傳遞機制并蝗,我們知道Touch事件,首先必然由最外層View接收到秸妥,并很有可能被它攔截滚停,如果無法更改這個最外層View,那么是不是就沒轍了呢粥惧?其實不然键畴,Android這么高大上的系統(tǒng)必然考慮到了這個問題,好了廢話不說影晓,先看代碼
private BannerView carouselView;
private Context mContext;
private PullToRefreshView refreshView;
refreshView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
});
carouselView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
carouselView.getParent().requestDisallowInterceptTouchEvent(true);
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = y - lastY;
int deltaX = x - lastX;
if (Math.abs(deltaX) < Math.abs(deltaY)) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
} else {
carouselView.getParent().requestDisallowInterceptTouchEvent(true);
}
default:
break;
}
return false;
}
});
首先說一下這個方法
public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)
Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
Parameters
disallowIntercept
True if the child does not want the parent to intercept touch events.
API里的意思很明確镰吵,子View如果不希望其父View攔截Touch事件時檩禾,可調用此方法挂签。當disallowIntercept這個參數為true時疤祭,父View將不攔截。
PS:這個方法的命名和其參數的使用邏輯饵婆,讓我想到了一句很有意思的話勺馆,敵人的敵人就是朋友,真不知道Google的大神們是怎么想的侨核,非要搞一個反邏輯草穆。
好了,言歸正傳搓译。這里攔截直接也很明確悲柱,在carouselView的onTouch方法中每次進入就設定父View不攔截此次事件,然后在MOTION_MOVE時候些己,根據滑動的距離判斷再決定是父View是否有權利攔截Touch事件(即滑動行為)豌鸡。
關鍵的處理邏輯就是這里:
if (Math.abs(deltaX) < Math.abs(deltaY)) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
} else {
carouselView.getParent().requestDisallowInterceptTouchEvent(true);
}
這個結合上面對這個方法的解釋,應該很好理解了段标,就不多做闡述了涯冠。
好了,這里可以看到逼庞,解決這種滑動沖突的方法很簡單蛇更,最根本的還是得充分了解Touch事件的傳遞機制,只有這樣赛糟,才能明白該在哪里做什么事情派任。
當然,橫豎滑動的沖突很好理解璧南,但同一方向的滑動沖突情況就有點復雜了吨瞎,下次再說。