大家好,又見面了,我是你們的朋友全棧君。
我的公衆号程式員徐公,四年中大廠工作經驗,回複黑馬,領取 Android 學習視訊一份,回複徐公 666,可以獲得我精心整理的履歷,帶你走近大廠。
這篇部落客要講解一下幾個問題
- 粗略地介紹一下View的事件分發機制
- 解決事件滑動沖突的思路及方法
- ScrollView 裡面嵌套ViewPager導緻的滑動沖突
- ViewPager裡面嵌套ViewPager 導緻的滑動沖突
- 輪播圖的幾種實作方式
文章首發位址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127
先看一下效果圖
ScrollView裡面嵌套ViewPager

ViewPager裡面嵌套ViewPager
View的 事件分發機制
這篇部落格不打算詳細講解View的事件分發機制,因為網上已經出現了一系列的好 文章,我自己的水準也有限,目前肯定寫得不咋的。
先啰嗦一下,View 的事件分發機制主要涉及到一下三個 方法
- dispatchTouchEvent ,這個方法主要是用來分發事件的
- onInterceptTouchEvent,這個方法主要是用來攔截事件的(需要注意的是ViewGroup才有這個方法,View沒有onInterceptTouchEvent這個方法
- onTouchEvent 這個方法主要是用來處理事件的
- requestDisallowInterceptTouchEvent(true),這個方法能夠影響父View是否攔截事件,true 表示父 View 不攔截事件,false 表示父 View 攔截事件
下面引用圖解 Android 事件分發機制這一篇部落格的内容
- 仔細看的話,圖分為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 方法進行分發,
- 如果dispatchTouchEvent傳回true 消費事件,事件終結。
-
如果dispatchTouchEvent傳回 false ,則回傳給父View的onTouchEvent事件處理;
onTouchEvent事件傳回true,事件終結,傳回false,交給父View的OnTouchEvent方法處理
-
如果dispatchTouchEvent傳回super的話,預設會調用自己的onInterceptTouchEvent方法
預設的情況下interceptTouchEvent回調用super方法,super方法預設傳回false,是以會交給子View的onDispatchTouchEvent方法處理
如果 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);
}
複制
ScrollView 裡面嵌套ViewPager導緻的滑動沖突
外部解決法
如上面所述,從 父View ScrollView着手,重寫 OnInterceptTouchEvent方法,在上下滑動的時候攔截事件,在左右滑動的時候不攔截事件,傳回 false,這樣確定子View 的dispatchTouchEvent方法會被調用,代碼 如下
/**
* @ explain:這個ScrlloView不攔截水準滑動事件,
* 是用來解決 ScrollView裡面嵌套ViewPager使用的
* @ author:xujun on 2016/10/25 15:28
* @ email:[email protected]
*/
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不要攔截事件,其他的時候由子View 攔截事件
/** * @ explain:這個 ViewPager是用來解決ScrollView裡面嵌套ViewPager的 内部解決法的 * @ author:xujun on 2016/10/25 16:38 * @ email:[email protected] */
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) {
// 左右滑動請求父 View 不要攔截
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中完美解決,主要要一下兩種方法
第一種方法,重寫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();
複制
這段代碼在初始化的時候就讓該界面的頂部的某一個控件獲得焦點,滾動條自然就顯示到頂部了。
在Fragment中的相關解決方法
同樣是調用第二種方法,調用RecyclerView上面的View的一下方法,讓其擷取焦點
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
複制
這段代碼在初始化的時候就讓該界面的頂部的某一個控件獲得焦點,滾動條自然就顯示到頂部了。但是該方法存在缺點,就是當我們上面的view如果滑動到一半的時候,切換到下一個Fragment,在切換回來的時候,RecyclerView的第一個item會自動滑動到頂部。目前我還沒有找到相對比較好的解決這個問題的方法,大家知道相關解決方法的話也歡迎聯系我,可以加我 微信或者在留言區評論,謝謝。
網友提供的解決方案
關于 ViewPagerActivity 在Fragment頁面切換的時候,RecyclerView搶占焦點的問題已經解決,特别 感謝Jianqiu,他的部落格位址:http://niorgai.github.io/
在 ViewPagerActivity 裡面的 Fragment的 代碼中加入以下代碼,可以阻止 RecyclerView 的子 View 獲得焦點,進而阻止 RecyclerView 搶占位置。
// 是為了確定mNoHorizontalScrollView他的子孫不能獲得焦點
mNoHorizontalScrollView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
複制
詳細代碼見項目中的ListFragement
個人疑點
借鑒于解決Activity的方法,目前我還沒有找到一個方法是在Fragemnt界面完全繪制完畢以後回調的方法,如果大家知道怎樣處理的 話,歡迎大家提出來
ViewPager裡面嵌套ViewPager導緻的滑動沖突
内部解決法
從子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的,一般有一下幾種實作方式
- 使用我們上述提高的ScrollView裡面嵌套ViewPager和RecyclerView,這種實作方式需要自己解決View滑動事件的沖突,同時還有我在上述提高的在Fragment中存在的問題
- 使用listView的addHeaderView來實作,或者是通過多種不同的item來實作
- 使用RecyclerView添加headerView來實作,或者複用多種不同的item來實作。關于RecyclerView如何添加headerView可以參考鴻洋大神的這一篇部落格 Android 優雅的為RecyclerView添加HeaderView和FooterView
- 使用SupportLibrary中的CoordinatorLayout等控件
其布局檔案如下,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打造各種炫酷的效果
總結
- 當我們滑動方向不同的時候,采用外部解決法和内部解決法,複雜度差不多。
- 當我們滑動的方向相同的話,建議采用内部解決法來解決,因為采用外部解決法複雜度比較高。而且有時候我們是采用别人的開源控件,這時候去修改别人的源碼可能會發生一些意想不到的bug。
題外話
- 在這篇部落格的最後提高的實作輪播圖+list清單的幾種實作形式,剛開始是不想寫的,後面因為ScrollView裡面嵌套ViewPager和RecyclerView在fragment中RecyclerView搶占焦點,在某些情況下使用者體驗不好,才寫出來的,跟這篇部落格要講解的View滑動事件沖突沒有多大關系,隻是給讀者提供多種思路而已
- 至于CoordinatorLayout,是google IO 2015中提出來的,功能很強大,可以說是專門為了解決嵌套導滑動而産生的,極大地友善了開發者,對于初學者,可以暫時不必掌握它,先把其他的基礎學好就好
- 同時賣一下廣告,歡迎大家到我的github上面star或者fork,謝謝
參考文章:圖解 Android 事件分發機制
文章首發位址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127
源碼下載下傳位址:https://github.com/gdutxiaoxu/TouchDemo.git
大家如果覺得不錯的話,可以關注我的微信公衆号程式員徐公
釋出者:全棧程式員棧長,轉載請注明出處:https://javaforall.cn/163201.html原文連結:https://javaforall.cn