結論: 在自定義控件中如下重寫onInterceptTouchEvent
就告訴所有父View:不要攔截事件,讓我消費?。?/strong>
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
這是一個從源碼角度分析滑動沖突的原因
以及在源碼中理解為何能解決滑動沖突
這是MainActivity主界面的布局內容:
xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.solory.learnview.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp" />
<ImageView
android:id="@+id/imageView"
android:layout_width="105dp"
android:layout_height="86dp"
android:layout_margin="8dp"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/article_1"
android:textSize="36sp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/colorPrimary">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/article_2" />
</ScrollView>
</LinearLayout>
</ScrollView>
MainActivity不用動。
外面的ScrollView正常滑動,但是里面的那個ScrollView動不了。
直接給出解決方案再看如何解決:
新建一個類繼承ScrollView
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 onInterceptTouchEvent(MotionEvent ev) {
//關鍵點在這
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
}
buildProject,然后在xml中將里面的ScrollView修改成這個MyScrollView。
...
<com.solory.learnview.MyScrollView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/colorPrimary">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/article_2" />
</com.solory.learnview.MyScrollView>
...
跑起來:問題解決。
那么現在研究為什么,為什么在重寫的onInterceptTouchEvent(MotionEvent ev)
中神奇的一句代碼
getParent().requestDisallowInterceptTouchEvent(true);
就把問題解決了?
好了。跟著我的思路來。
-
先看ScollView源碼中的onInterceptTouchEvent:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * 這個方法決定了我們是否要攔截這個事件。 * 如果我們返回true, onMotionEvent方法將被調用, * 我們將在那執行實際的滾動操作。 */ /* * 最常見的情況:用戶在拖拽中。 * 他在動他的手指。我們想要截取這個 * 事件. */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } if (super.onInterceptTouchEvent(ev)) { return true; } /* * 如果我們不能滾動,不要試圖截取觸摸。 */ if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } ... ... ...
清晰明了,干凈簡單,后面還有一大段代碼,就不放了。這里一進來就是一個判斷,如果進來的是ACTION_MOVE, 那么直接返回true,直接攔截,那么后面就沒他的子View什么事了(不懂的話去看一下ViewGroup的dispatchTouchEvent方法),event被傳入他自己的onTouchEvent中去進行滾動操作了。
那么我們一開始內部的ScrollView滑動沒有響應的原因就是,那時候手指是在滑動的,一直不斷傳入ACTION_MOVE, 所以event一直被外部的ScrollView在如上的操作中攔截了。
意思就是只要你手指在ScrollView上滑動,ScrollView內部的子View就永遠接收不到任何事件,就是永遠無響應。
沖突的原因明白了,現在看如何解決的
-
回頭看MyScrollView是如何解決的:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); return super.onInterceptTouchEvent(ev); }
意思就是取得父類,然后請求父類不攔截TouchEvent的意思。
首先getParent就是返回父類,在這里是返回的那個LinearLayout,然后點
requestDisallowInterceptTouchEvent
進去看,發現是一個叫做ViewParent的接口中的抽象方法,
注釋的英文:當一個子View不想要他的父View和它的祖先View們攔截觸摸事件的時候。調用該方法 他的父View應該將該方法接著向上傳遞給每一個祖先View們。
-
抽象方法的話,看一下是誰實現了,因為繼承的ScrollView,所以先看對應的ScrollView中的實現
//-----ScrollView中 @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { //我也不知道這個方法干嘛的,反正不影響整體思路,先跳過。 recycleVelocityTracker(); } //無論如何,都會執行父類的該方法。 super.requestDisallowInterceptTouchEvent(disallowIntercept); }
-
那么我們查看父類中的實現,ViewGroup中:
//-----ViewGroup中 @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); } }
意思就是將自己的FLAG更改,變成disallowIntercept,并且遞歸,只要有父View, 就把父View的FLAG同樣設置。
大意為,設置了這個方法,MyScrollView就通過遞歸,告訴了他的父View和向上的所有祖先View:統統不要攔截事件!交給我來!
-
那這個FLAG是在哪里發揮作用?當然是在ViewGroup的
dispatchTouchEvent(MotionEvent event)
內部,并且用一個if條件先于onInterceptMotionEvent(MotionEvent event)
來判斷
圖片為證:
摸了摸老夫的胡須,嗯...,說的真好啊~
但是!
//-----MyScrollView中
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
這段代碼的getParent().requestDisallowInterceptTouchEvent(true);
能執行到的前提是MyScrollView能執行onInterceptTouchEvent
,也就是能執行dispatchTouchEvent
,可是事件早都被外層的ScrollView攔截了,你還怎么獲取父類然后請求不要攔截TouchEvent?
當時我在這里思考了蠻久的, 那么我們返回到ScrollView的onInterceptTouchEvent
里去看吧
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
...
...
...
他只攔截ACTION_MOVE,不攔截ACTION_DOWN,所以當ACTION_DOWN的那一次事件還是可以傳到下面的子View去的,而利用這一點,MyScrollView利用第一次觸碰那唯一的一次event,將他FLAG給改了,事件就可以順利地傳遞到MyScrollView了~
總結:
- 在自定義控件中重寫
onInterceptTouchEvent
就告訴所有父View:不要攔截事件,讓我消費??!@Override public boolean onInterceptTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); return super.onInterceptTouchEvent(ev); }