這篇博客主要講解一下幾個問題java
文章首發地址CSDN:blog.csdn.net/gdutxiaoxu/…android
這篇博客大打算詳細講解View的事件分發機制,由於網上已經出現了一系列的好 文章,我本身的水平也有限,目前確定寫得不咋的。git
先囉嗦一下,View 的事件分發機制主要涉及到一下三個 方法程序員
- 仔細看的話,圖分爲3層,從上往下依次是Activity、ViewGroup、View
- 事件從左上角那個白色箭頭開始,由Activity的dispatchTouchEvent作分發
- 箭頭的上面字表明方法返回值,(return true、return false、return super.xxxxx(),super 的意思是調用父類實現。
- dispatchTouchEvent和 onTouchEvent的框裏有個【true---->消費】的字,表示的意思是若是方法返回true,那麼表明事件就此消費,不會繼續往別的地方傳了,事件終止。
- 目前全部的圖的事件是針對ACTION_DOWN的,對於ACTION_MOVE和ACTION_UP咱們最後作分析。
- 以前圖中的Activity 的dispatchTouchEvent 有誤(圖已修復),只有return super.dispatchTouchEvent(ev) 纔是往下走,返回true 或者 false 事件就被消費了(終止傳遞)。
當TouchEvent發生時,首先Activity將TouchEvent傳遞給最頂層的View,TouchEvent最早到達最頂層 view 的 dispatchTouchEvent ,而後由 dispatchTouchEvent 方法進行分發,github
若是dispatchTouchEvent返回true 消費事件,事件終結。面試
若是dispatchTouchEvent返回 false ,則回傳給父View的onTouchEvent事件處理;bash
onTouchEvent事件返回true,事件終結,返回false,交給父View的OnTouchEvent方法處理微信
若是dispatchTouchEvent返回super的話,默認會調用本身的onInterceptTouchEvent方法app
默認的狀況下interceptTouchEvent回調用super方法,super方法默認返回false,因此會交給子View的onDispatchTouchEvent方法處理ide
若是 interceptTouchEvent 返回 true ,也就是攔截掉了,則交給它的 onTouchEvent 來處理,
若是 interceptTouchEvent 返回 false ,那麼就傳遞給子 view ,由子 view 的 dispatchTouchEvent 再來開始這個事件的分發。
關於更多詳細分析,請查看原博客圖解 Android 事件分發機制,真心推薦,寫得很好。
第一種狀況,滑動方向不一樣
第二種狀況,滑動方向相同
第三種狀況,上述兩種狀況的嵌套
看了上面三種狀況,咱們知道他們的共同特色是父View 和子View都想爭着響應咱們的觸摸事件,但遺憾的是咱們的觸摸事件 同一時刻只能被某一個View或者ViewGroup攔截消費,因此就產生了滑動衝突?那既然同一時刻只能由某一個View或者ViewGroup消費攔截,那咱們就只須要 決定在某個時刻由這個View或者ViewGroup攔截事件,另外的 某個時刻 有另一個View或者ViewGroup攔截事件不就OK了嗎?綜上,正如 在 《Android開發藝術》 一書提出的,總共 有兩種接覺方案
如下解決思路來自於 《Android開發藝術》 書籍
下面的兩種方法針對第一種狀況(滑動方向不一樣),父View是上下滑動,子View是左右滑動的狀況。
從父View着手,重寫onInterceptTouchEvent方法,在父View須要攔截的時候攔截,不要的時候返回false,爲代碼大概 以下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
// 這裏是夠攔截的判斷依據是左右滑動,讀者可根據本身的邏輯進行是否攔截
if (deltaX > deltaY) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
複製代碼
從子View左右,父View先不要攔截任何事件,全部的 事件傳遞給 子View,若是子View須要此事件就消費掉,不須要此事件的話就交給 父View處理。
實現思路 以下,重寫子 View的dispatchTouchEvent方法,在Action_down 動做中經過方法 requestDisallowInterceptTouchEvent(true) 先請求 父 View不要攔截事件,這樣保證 子View可以 接受到Action_move事件,再在Action_move動做中根據 本身的邏輯是否要攔截事件,不要的 話交給 父View處理
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保證子View可以接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// 這裏是夠攔截的判斷依據是左右滑動,讀者可根據本身的邏輯進行是否攔截
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
複製代碼
如上面所述,從 父ViewScrollView着手,重寫 OnInterceptTouchEvent方法,在上下滑動的時候攔截事件,在左右滑動的時候不攔截事件,返回 false,這樣確保子View 的dispatchTouchEvent方法會被調用,代碼 以下
/**
* @ explain:這個ScrlloView不攔截水平滑動事件,
* 是用來解決 ScrollView裏面嵌套ViewPager使用的
* @ author:xujun on 2016/10/25 15:28
* @ email:gdutxiaoxu@163.com
*/
public class VerticalScrollView extends ScrollView {
public VerticalScrollView(Context context) {
super(context);
}
public VerticalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int
defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private float mDownPosX = 0;
private float mDownPosY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
// 這裏是夠攔截的判斷依據是左右滑動,讀者可根據本身的邏輯進行是否攔截
if (deltaX > deltaY) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}
複製代碼
如上面上述,經過requestDisallowInterceptTouchEvent(true)方法來影響父View是否攔截事件,咱們經過重寫ViewPager的 dispatchTouchEvent()方法,在左右滑動的時候請求父View ScrollView不要攔截事件,其餘的時候攔截事件
/** * @ explain:這個 ViewPager是用來解決ScrollView裏面嵌套ViewPager的 內部解決法的 * @ author:xujun on 2016/10/25 16:38 * @ email:gdutxiaoxu@163.com */
public class MyViewPager extends ViewPager {
private static final String TAG = "xujun";
int lastX = -1;
int lastY = -1;
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保證子View可以接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// 這裏是夠攔截的判斷依據是左右滑動,讀者可根據本身的邏輯進行是否攔截
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
}
複製代碼
當咱們ScrollView的最上層的Layout裏面多多個孩子的時候,當下面一個孩子是RecyclerView或者ListView的時候,每每會活動滑動到ListView或者RecyclerView 的第一個item,致使進入界面的時候會致使RecyclerView 上面的 View被滑動到界面意外,看不見,這時候的用戶體驗是比較差的
即結構以下面的時候
因而我查找了相關的資料,在Activity中完美解決,主要要一下兩種方法
第一種方法,重寫Activity的onWindowFocusChanged()方法,在裏面調用mNoHorizontalScrollView.scrollTo(0,0);方法,滑動到頂部,由於onWindowFocusChanged是在全部View繪製完畢的時候纔會回調的,不熟悉的話建議先回去看一下Activity的生命週期的相關介紹
private void scroll() {
mNoHorizontalScrollView.scrollTo(0,0);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus && first){
first=false;
scroll();
}
}
複製代碼
第二種解決方法,調用RecyclerView上面的View的一下方法,讓其獲取焦點
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
複製代碼
這段代碼在初始化的時候就讓該界面的頂部的某一個控件得到焦點,滾動條天然就顯示到頂部了。
一樣是調用第二種方法,調用RecyclerView上面的View的一下方法,讓其獲取焦點
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
複製代碼
這段代碼在初始化的時候就讓該界面的頂部的某一個控件得到焦點,滾動條天然就顯示到頂部了。可是給方法存在缺點,就是當咱們上面的view若是滑動到一半的時候,切換到下一個Fragment,在切換回來的時候,RecyclerView的第一個item會自動滑動到頂部。目前我尚未找到相對比較好的解決這個問題的方法,你們知道相關解決方法的話也歡迎聯繫我,能夠加我 微信或者在留言區評論,謝謝
借鑑於解決Activity的方法,目前我尚未找到一個方法是在Fragemnt界面徹底繪製完畢之後回調的方法,若是你們知道怎樣處理的 話,歡迎你們提出來
從子View ViewPager着手,重寫 子View的 dispatchTouchEvent方法,在子 View須要攔截的時候進行攔截,不然交給父View處理,代碼以下
public class ChildViewPager extends ViewPager {
private static final String TAG = "xujun";
public ChildViewPager(Context context) {
super(context);
}
public ChildViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int curPosition;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
curPosition = this.getCurrentItem();
int count = this.getAdapter().getCount();
Log.i(TAG, "curPosition:=" +curPosition);
// 噹噹前頁面在最後一頁和第0頁的時候,由父親攔截觸摸事件
if (curPosition == count - 1|| curPosition==0) {
getParent().requestDisallowInterceptTouchEvent(false);
} else {//其餘狀況,由孩子攔截觸摸事件
getParent().requestDisallowInterceptTouchEvent(true);
}
}
return super.dispatchTouchEvent(ev);
}
}
複製代碼
這個若是要採用內部解決法來解決的話想,相對很麻煩,我提一下本身的我的思路,咱們能夠先測量子View在哪一個區域,而後咱們在根據咱們按下的點是否在區域之內,若是是的話,在根據子View時候須要攔截進行處理
對於這種效果,上面是輪播圖的,下面是RecyclerView或者ListView的,通常有一下幾種實現方式
其佈局文件以下,Activity代碼見項目中的SixActivity
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:fitsSystemWindows="true"
>
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
>
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|snap">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</android.support.v4.view.ViewPager>
<TextView
android:id="@+id/tv_page"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="right"
android:text="1/10"
android:textColor="#000"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>
複製代碼
關於CoordinatorLayout的更多用法,能夠參考個人這一篇博客使用CoordinatorLayout打造各類炫酷的效果
參考文章:圖解 Android 事件分發機制
文章首發地址CSDN:blog.csdn.net/gdutxiaoxu/…
源碼下載地址:github.com/gdutxiaoxu/…
掃一掃,歡迎關注個人微信公衆號 stormjun94(徐公碼字), 目前是一名程序員,不只分享 Android開發相關知識,同時還分享技術人成長曆程,包括我的總結,職場經驗,面試經驗等,但願能讓你少走一點彎路。