一娃循、引言
上一篇文章中,我們講到了View的事件分發(fā) 機制斗蒋,明白了點擊事件是如何從根View一步步地傳遞到目標View的捌斧。
而在日常開發(fā)中,使用一些復雜的View嵌套時捞蚂,經(jīng)常會遇到滑動沖突的問題妇押。有時候單單是為了解決這樣一個滑動沖突就消耗了不少時間。
既然問題已經(jīng)被拋出來了柱嫌,那有什么通用的辦法解決呢?本文主要通過分析滑動沖突抑片,到解決滑動沖突,并給出相關的demo作為例子蚓峦,讓View的滑動沖突不再是一件難事很泊。
二、常見的滑動沖突
常見的滑動沖突可以分為以下三種:
2.1 場景一
外部滑動方向和內(nèi)部滑動方向不一致。這里主要表現(xiàn)為ViewPager和Fragment配合使用所組成的頁面滑動效果定罢。
在這種效果中耕皮,可以通過左右滑動來切換頁面,而每一個頁面內(nèi)部又是一個ListView秩彤。
本身這種情況是有滑動沖突的叔扼,但是ViewPager內(nèi)部處理了這種滑動沖突事哭,因此采用ViewPager時無須關注這個問題。但是瓜富,如果我們采用的不是ViewPager鳍咱,而是ScrollView,則必須手動解決滑動沖突与柑。否則造成的后果是內(nèi)外兩層只有一層是可以滑動的谤辜,而且會顯得很卡頓。當然价捧,還有其他情況丑念,比如說外部上下滑動、內(nèi)部左右滑動都屬于同一種類型的滑動沖突干旧。
當用戶左右滑動時渠欺,需要讓外部的View攔截點擊事件;而當用戶上下滑動時椎眯,需要讓內(nèi)部View攔截點擊事件挠将。
在滑動過程中,通過兩個點的坐標就可以得到目前的左右滑動還是水平滑動编整。當豎直方向的滑動距離差大于水平方向的滑動距離差時舔稀,就判斷為豎直滑動,否則判斷為水平滑動掌测。
2.2 場景二
外部滑動方向和內(nèi)部滑動方向一致内贮。當內(nèi)外兩層都在同一個方向可以滑動時,是存在邏輯上問題的汞斧。因為當手指開始滑動的時候夜郁,系統(tǒng)無法知道用戶到底是想讓哪一層滑動。
場景二無法像場景一那樣通過滑動的角度粘勒、距離差以及速度差來做判斷竞端,而一般這個時候則需要從業(yè)務上找到突破點,比如業(yè)務上有規(guī)定:當處于某種狀態(tài)時庙睡,需要外部View響應用戶的滑動事富,而處于另外一種狀態(tài)時則需要內(nèi)部View來響應View的滑動。根據(jù)這種業(yè)務上的需求我們可以得到響應的處理規(guī)則乘陪。這樣的描述還是挺抽象的统台,在下一節(jié)我們會通過實際的例子來演示這種情況的解決方案,這里先有個概念即可啡邑。
2.3 場景三
場景三是場景一和場景二的嵌套贱勃,因此場景三的滑動沖突看起來更加復雜了。許多應用都有這樣一個效果:內(nèi)層有一個場景一的滑動效果,然后外層又有一個場景二的滑動效果募寨。舉個例子族展,外部有一個SlideMenu效果,然后內(nèi)部有一個ViewPager拔鹰,ViewPager的每一個頁面又是一個ListView。雖然說場景三的滑動沖突看起來更復雜贵涵,但是它是幾個單一的滑動沖突的疊加列肢,因此只需要分別處理內(nèi)層、中層和外層之間的滑動沖突即可宾茂。具體的處理方法其實和場景一瓷马、場景二相同的。
場景三和場景二一樣跨晴,無法直接通過滑動的角度欧聘、距離差以及速度差來做判斷,同樣還是只能從業(yè)務上找到突破點端盆,具體方法和場景二一樣怀骤,都是從業(yè)務的需求上得出響應的處理規(guī)則,在下一節(jié)將會通過實際的例子來演示這種情況的解決方案焕妙。
三蒋伦、滑動沖突的解決方式
1、外部攔截法
所謂的外部攔截法是指點擊事件都先經(jīng)過父容器的攔截處理焚鹊,如果父容器需要此事件痕届,那就攔截,如果不需要此事件就不攔截末患,這樣就可以解決滑動沖突的問題研叫,這種方法比較符合點擊事件的分發(fā)機制。
外部攔截法需要重寫父容器的onInterceptTouchEvent方法璧针,然后在內(nèi)部做相應的攔截即可嚷炉。這種方法的偽代碼如下所示:
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要當前點擊事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
上述代碼是外部攔截法的典型邏輯,針對不同的滑動沖突陈莽,只需要修改父容器需要當前點擊事件這個條件即可渤昌,其他均不需要做修改并且也不能修改。
當事件為ACTION_DOWN事件時走搁,必須返回false独柑,即不攔截該事件。因為父容器一旦攔截了該事件私植,那么后續(xù)的ACTION_MOVE和ACTION_UP事件都會直接交給父容器處理忌栅,這個時候事件就沒法傳遞給子元素了。
當事件為ACTION_MOVE事件時,可以根據(jù)需要來決定是否要攔截索绪,如果父容器需要攔截就返回true湖员,否則返回false。
當事件是ACTION_UP事件時瑞驱,也要返回false娘摔,因為ACTION_UP事件本身已經(jīng)沒有多大意義了。
可以考慮這樣的一種情況唤反,假設事件交由子元素處理凳寺,如果父容器在ACTION_UP時返回了true,那么會導致子元素無法收到ACTION_UP事件彤侍,這個時候子元素的onClick事件就無法觸發(fā)肠缨,因為當子元素為View時,必須接收到ACTION_UP事件才能觸發(fā)onClick事件盏阶。即使父容器的ACTION_UP事件返回了false晒奕,但父容器決定要攔截任何一個事件時,那么后續(xù)的事件也會交給它來處理名斟。
2脑慧、內(nèi)部攔截法
內(nèi)部攔截法是指父容器不攔截任何事件,所有的事件都傳遞到子元素蒸眠,如果子元素需要此事件就直接消耗掉漾橙,否則交給父容器進行處理。
這種方法和Android的事件分發(fā)機制不一樣楞卡,需要配合requestDisallowInterceptTouchEvent 方法才能正常工作霜运,相較于外部攔截法稍顯復雜。
內(nèi)部攔截法需要重寫子元素的dispatchTouchEvent方法蒋腮。這種方法的偽代碼如下所示:
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if (父容器需要此類點擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
上面代碼是內(nèi)部攔截法的典型代碼淘捡,但面對不同的滑動策略時,只需要修改父容器需要此類點擊事件的條件即可池摧,其他不需要修改也不能修改焦除。
當然,這里除了子元素需要做處理之外作彤,父元素也要默認攔截除ACTION_DOWN之外的其他事件膘魄,這樣當子元素調(diào)用parent. requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續(xù)攔截所需的事件竭讳。
父容器不攔截ACTION_DOWN事件的原因和上面的一樣的创葡,如果攔截了ACTION_DOWN事件,那么所有的事件都無法傳遞到子元素中去绢慢,這樣內(nèi)部攔截法就不起作用了灿渴。父元素所做的修改如下所示:
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
四、實例演示
下面來看一個例子,分別說明如何利用外部攔截法和內(nèi)部攔截法來解決View的滑動沖突骚露。
下面的例子最外層是HorizontalListView蹬挤,這是一個可以水平滑動的ListView,是我從Github上拉過來的棘幸。出處是MeetMe/Android-HorizontalListView焰扳,HorizontalListView的里面是多個ItemView,而ItemView是可以上下滑動的ListView误续。
下面是Activity的代碼:
public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
HorizontalListView horizontalView = findViewById(R.id.horizontal_list_view);
horizontalView.setAdapter(new MyListAdapter());
}
class MyListAdapter extends BaseAdapter {
@Override
public int getCount() {
return 3;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = LayoutInflater.from(TestActivity.this).inflate(R.layout.horizontal_item, parent, false);
ListView listView = convertView.findViewById(R.id.list_view);
listView.setAdapter(new ArrayAdapter<String>(TestActivity.this, android.R.layout.simple_list_item_1, new String[]{"1", "2", "3", "4", "5", " 6", "7", "8", "9", "10", "1", "2", "3", "4", "5", " 6", "7", "8", "9", "10"}));
return convertView;
}
}
}
Activity的代碼邏輯挺簡單的蓝翰,這里創(chuàng)建了三個子Item,并將其加入了HorizontalListView中女嘲,而三個子Item每一個又是由ListView構成。
horizontal_item的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mucfc.myapplication.CustomListView
android:id="@+id/list_view"
android:layout_width="180dp"
android:layout_height="match_parent"/>
</LinearLayout>
CustomListView的代碼如下:
public class CustomListView extends ListView {
public CustomListView(Context context) {
super(context);
}
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
HorizontalListView的完整代碼由于比較長诞帐,所以這里省略欣尼。
如果運行上面的例子,則會出現(xiàn)滑動沖突的情況停蕉。具體的表現(xiàn)是愕鼓,左右滑動和上下滑動不流暢,給人的感覺就是很卡頓慧起。
下面就從外部攔截法和內(nèi)部攔截法這兩種方法著手菇晃,看一下如何解決上述沖突。
外部攔截法
如果采用外部攔截法蚓挤,則需要在HorizontalListView中重寫onInterceptTouchEvent方法磺送,如下所示:
private int mLastX;
private int mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) (ev.getX());
int y = (int) (ev.getY());
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(x - mLastX) > Math.abs(y - mLastY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastX = x;
mLastY = y;
return intercept;
}
從上述的代碼看,它和外部攔截法的偽代碼差別很小灿意,只是將父容器的攔截條件換成了具體的邏輯估灿。這里的判斷是,當水平滑動方向的距離大于豎直滑動方向的距離時缤剧,則判斷為水平滑動馅袁,此時需要讓父容器進行攔截事件;而當豎直滑動方向的距離大于水平滑動方向的距離時荒辕,則判斷為豎直滑動汗销。
內(nèi)部攔截法
如果采用內(nèi)部攔截法,需要在HorizontalListView中重寫onInterceptTouchEvent方法抵窒,并在CustomListView中重寫dispatchOnTouchEvent方法弛针,具體代碼如下所示:
## CustomListView ##
private int mLastX;
private int mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(x - mLastX) > Math.abs(y - mLastY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
## HorizontalListView ##
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
相比于外部攔截法,內(nèi)部攔截法需要修改CustomListView和HorizontalListView兩個類的代碼估脆,復雜度相對來說比較高钦奋。但也能夠?qū)崿F(xiàn)滑動沖突的解決。至于在實際應用中要使用哪種方法,完全看個人的愛好付材,只要能解決問題朦拖,方法沒有好壞之分。
五厌衔、參考文章
本文主要參考 《Android開發(fā)藝術探索》 一書中的《View事件體系》這一章的內(nèi)容璧帝,這本書深入淺出的講解了Android開發(fā)中的知識點,是開發(fā)進階的必備利器富寿。