【一】傳統事件分發
1.1 傳統事件分發流程
Activity:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
ViewGroup:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)
View:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
僞代碼表示dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三者之間的關係:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
用圖來表示事件傳遞的過程:
1.2 傳統事件滑動衝突
來看ViewGroup的分發(PS:本文中的源碼是基於Android API 24分析的~):
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
------other code------
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
------other code------
handled = super.dispatchTouchEvent(transformedEvent);
}
當ViewGroup不攔截事件並由ViewGroup的子View處理事件時,mFirstTouchTarget會被賦值並指向子View,上面的代碼可以分成下面幾種情況:
1、如果MotionEvent.ACTION_DOWN事件被ViewGroup攔截,那麼mFirstTouchTarget==null,那麼後續的ACTION_MOVE、ACTION_UP事件都不會往子View中傳遞了,而會走ViewGroup的onTouchEvent方法。
2、當mFirstTouchTarget != null時,即子View處理後續ACTION_MOVE、ACTION_UP事件時,子View中可以通過設置getParent().requestDisallowInterceptTouchEvent(true)
,該設置會影響上述代碼中的disallowIntercept
變量,從而使ViewGroup不攔截事件,將事件傳遞到子View中去
下面分析一下當遇到滑動衝突的時候一般的解決方法:
1.2.1外部攔截法
外部攔截法即在父ViewGroup的onInterceptTouchEvent中去做處理,僞代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (ViewGroup攔截ACTION_MOVE條件) {
isIntercept = true;
} else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
break;
}
return isIntercept;
}
這裏注意除非父ViewGroup要處理所有事件,否則一定不能攔截ACTION_DOWN
,因爲一旦攔截了ACTION_DOWN
事件,後續的MOVE、UP事件將都不能傳遞到子View了
1.2.2內部攔截法
內部攔截法是指父ViewGroup不攔截任何事件,所有事件都傳遞到子View中,通過getParent().requestDisallowInterceptTouchEvent(boolean)
來控制後續事件讓不讓父ViewGroup去攔截。來看requestDisallowInterceptTouchEvent
的源碼:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
主要是修改標誌位mGroupFlags
,這個標誌位也是在父ViewGroup的dispatchTouchEvent中控制是否走onInterceptTouchEvent()攔截方法的,disallowIntercept爲true時,父ViewGroup不會再走onInterceptTouchEvent()攔截事件;反之會走onInterceptTouchEvent()方法,默認是false。
內部攔截法的使用舉例:
在子View的dispatchTouchEvent中:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父ViewGroup需要攔截並處理事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
在父ViewGroup的onInterceptTouchEvent中:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return ev.getAction() != MotionEvent.ACTION_DOWN;
}
內部攔截法同樣不能攔截ACTION_DOWN事件,否則事件不能傳遞到子View中。
1.3 傳統嵌套滑動衝突
在開始介紹滑動衝突之前,先介紹一下測量規格MeasureSpec
,MeasureSpec
參與了View的測量過程,子View的MeasureSpec的創建是由父View的MeasureSpec和子View自身的LayoutParams共同決定的,MeasureSpec
的組成:
MeasureSpec是一個32位的int值,高2位是specMode
,低30位是specSize
,如下:
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
SpecMode是測量模式,SpecSize是在某種測量模式下的測量大小,SpecMode有三個值:
USPECIFIED
: 父View對子View沒有任何限制。
EXACTLY
: 父View已經檢測出View的精確大小,View的最終大小就是SpecSize指定的值。它對應於LayoutParams中的match_parent和具體數值這兩種模式。
AT_MOST
:父View指定了一個可用大小SpecSize,View的大小不能大於這個值,具體是什麼值要看不同View的具體實現。它對應於LayoutParams中的wrap_content。
1.3.1 ScrollView+ ListView嵌套衝突
默認ListView中的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-----其他代碼-----
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
}
通過源碼我們知道當測量模式是UNSPECIFIED時,高度只是一個item的高度(包括上下的padding);當測量模式是AT_MOST時,高度是所有item的高度,即整個listview的高度。通過測試發現ScrollView和ListView嵌套使用時,傳給ListView的測量模式是UNSPECIFIED,所以只能顯示一個Item的高度,那怎麼顯示整個ListView的高度呢?通過上面的分析我們已經知道答案了:重寫ListView的onMeasure方法!
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightSpec);
}
放棄通過父View及自身LayoutParams生成的MeasureSpec(specMode爲UNSPECIFIED),重新生成一個specMode爲AT_MOST的MeasureSpec即可。
1.3.2 ScrollView+ ViewPager嵌套問題
來看ViewPager的onMeasure源碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, our internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
通過分析源碼我們知道:
測量模式specMode是UNSPECIFIED
時,ViewPager的高度是0;
測量模式specMode是AT_MOST
或EXACTLY
時,ViewPager的高度直接取的父View傳入的值。
通過測試發現當ScrollView和ViewPager嵌套使用時,測量模式specMode是UNSPECIFIED
,所以默認高度是0.
【二】 傳統事件分發 VS NestedScrolling
- 傳統事件分發:子View處理Touch事件時,父View可以進行攔截並在父View中處理,但是一旦父View進行攔截,後續事件都不會再往子View中傳遞了。
- NestedScrolling:子View在滾動的時候,首先將dx、dy交給NestedScrollingParent進行消耗,剩餘部分還給子View。
【三】NestedScrolling嵌套滑動
從Android5.0開始引入了NestedScrolling機制(5.0之前可以用Support V4包向前兼容),用來處理子View和父View嵌套滑動時的交互機制。子View一般是可以滑動的View並且需要實現 NestedScrollingChild 接口,父View需要實現NestedScrollingParent接口。
2.1 NestedScrollingChild
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
2.2 NestedScrollingParent
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
2.3 兩者之間的關係
一、startNestedScroll
首先子view需要開啓整個流程(內部主要是找到合適的能接受nestedScroll的parent),通知父View,我要和你配合處理TouchEvent
二、dispatchNestedPreScroll
在子View的onInterceptTouchEvent或者onTouch中(一般在 MontionEvent.ACTION_MOVE事件裏),調用該方法通知父View滑動的距離。該方法的第三第四個參數返回父view消費掉的 scroll長度和子View的窗體偏移量。如果這個scroll沒有被消費完,則子view進行處理剩下的一些距離,由於窗體進行了移動,如果你記錄了手指最後的位置,需要根據第四個參數offsetInWindow計算偏移量,才能保證下一次的touch事件的計算是正確的。
如果父view接受了它的滾動參數,進行了部分消費,則這個函數返回true,否則爲false。
這個函數一般在子view處理scroll前調用。
三、dispatchNestedScroll
向父view彙報滾動情況,包括子view消費的部分和子view沒有消費的部分。
如果父view接受了它的滾動參數,進行了部分消費,則這個函數返回true,否則爲false。
這個函數一般在子view處理scroll後調用。
四、stopNestedScroll
結束整個流程。
更詳細見:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0603/2990.html
2.4 二個NestedScrolling 嵌套滑動例子
直接見github吧:
https://github.com/crazyqiang/AndroidStudy
或者見鴻神的博客:
https://blog.csdn.net/lmj623565791/article/details/52204039