3.5 View的滑動沖突
本節(jié)開始介紹View體系中一個深入的話題:滑動沖突收夸。相信開發(fā)Android的人都會有這種體會:滑動沖突實在是太坑人了,本來從網(wǎng)上下載的demo運行的好好的搪缨,但是只要出現(xiàn)滑動沖突个少,demo就無法正常工作了钥组。那么滑動沖突時如何產(chǎn)生的呢?其實在界面中只要內(nèi)外兩層同時可以滑動柬采,這個時候就會產(chǎn)生滑動沖突欢唾。如何解決滑動沖突呢?這既是一件困難的事又是一件簡單的事粉捻,說困難是因為許多開發(fā)者面對滑動沖突都會顯得束手無策礁遣,說簡單是因為滑動沖突的解決有固定的套路,只要知道了這個固定套路問題就好解決了肩刃。本節(jié)是View體系的核心章節(jié)祟霍,前面4節(jié)均是為本節(jié)服務的,通過本節(jié)的學習盈包,滑動沖突將不再是個問題沸呐。
3.5.1 常見的滑動沖突場景
常見的滑動沖突場景可以簡單分為如下三種(詳情請參看圖3-4):
- 場景1——外部滑動方向和內(nèi)部滑動方向不一致;
- 場景2——外部滑動方向和內(nèi)部滑動方向一致呢燥;
- 場景3——上面兩種情況的嵌套崭添。
圖3-2 滑動沖突的場景
先說場景1,主要是將ViewPager和Fragment配合使用所組成的頁面滑動效果疮茄,主流應用幾乎都會使用這個效果滥朱。在這種效果中,可以通過左右滑動來切換頁面力试,而每個頁面內(nèi)部往往又是一個ListView徙邻。本來這種情況下是有滑動沖突的,但是ViewPager處理了這種滑動沖突畸裳,因此采用ViewPager時我們無須關(guān)注這個問題缰犁,如果我們采用的不是ViewPager而是ScrollView等,那就必須手動處理滑動沖突了怖糊,否則造成的后果就是內(nèi)外兩層只能有一層能夠滑動帅容,這是因為兩者之間的滑動事件有沖突。除了這種典型情況外伍伤,還存在其他情況并徘,比如外部上下滑動,內(nèi)部左右滑動等扰魂,但是它們屬于同一類滑動沖突麦乞。
再說場景2,這種情況就稍微復雜一些劝评,當內(nèi)外兩層都在同一個方向可以滑動的時候姐直,顯然存在邏輯問題。因為當手指開始滑動的時候蒋畜,系統(tǒng)無法知道用戶到底是想讓哪一層滑動声畏,所以當手指滑動的時候就會出現(xiàn)問題,要么只有一層能滑動姻成,要么就是內(nèi)外兩層都滑動得很卡頓插龄。在實際的開發(fā)中愿棋,這種場景主要是指內(nèi)外兩層同時能上下滑動或者內(nèi)外兩層同時能左右滑動。
最后說下場景3辫狼,場景3是場景1和場景2兩種情況的嵌套初斑,因此場景3的滑動沖突看起來就更加復雜了。比如在許多應用中會有這么一個效果:內(nèi)層有一個場景1中的滑動效果膨处,然后外層又有一個場景2中的滑動效果见秤。具體說就是,外部有一個SlideMenu效果真椿,然后內(nèi)部有一個ViewPager鹃答,ViewPager的每一個頁面中又是一個ListView。雖然說場景3的滑動沖突看起來更復雜突硝,但是它是幾個單一的滑動沖突的疊加测摔,因此只需要分別處理內(nèi)層和中層、中層和外層之間的滑動沖突即可解恰,而具體的處理方法其實是和場景1锋八、場景2相同的。
從本質(zhì)上來說护盈,這三種滑動沖突場景的復雜度其實是相同的挟纱,因為他們的區(qū)別僅僅是滑動策略的不同,至于解決滑動沖突的方法腐宋,它們幾個是通用的紊服,在3.5.2節(jié)中將會詳細介紹這個問題。
3.5.2 滑動沖突的處理規(guī)則
一般來說胸竞,不管滑動沖突多么復雜欺嗤,他都有既定的規(guī)則,根據(jù)這些規(guī)則我們就可以選擇合適的方法去處理卫枝。
如圖3-4所示煎饼,對于場景1,它的處理規(guī)則是:當用戶左右滑動時校赤,需要讓外部的View攔截點擊事件腺占,當用戶上下滑動時,需要讓內(nèi)部的View攔截點擊事件痒谴。這個時候我們就可以根據(jù)它們的特征來解決滑動沖突,具體來說是:根據(jù)滑動是水平滑動還是豎直滑動來判斷到底是由誰來攔截事件铡羡,如圖3-5所示积蔚,根據(jù)滑動過程中兩個點之間的坐標就可以得出到底是水平滑動還是豎直滑動。如何根據(jù)坐標來得到滑動的方向呢烦周?這個很簡單尽爆,有很多可以參考怎顾,比如可以根據(jù)滑動路徑和水平方向所形成的夾角,也可以依據(jù)水平方向和豎直方向上的距離差來判斷漱贱,某些特殊時候還可以依據(jù)水平和豎直方向的速度差來做判斷槐雾。這里我們可以通過水平和豎直方向的距離差來判斷,比如豎直方向滑動的距離大就判斷為豎直滑動幅狮,否則判斷為水平滑動募强。根據(jù)這個規(guī)則就可以進行下一步的解決方法制定了。
對于場景2來說崇摄,比較特殊擎值,它無法根據(jù)滑動的角度、距離差以及速度差來做判斷逐抑,但是這個時候一般都能在業(yè)務上找到突破點鸠儿,比如業(yè)務上有規(guī)定:當處于某種狀態(tài)時需要外部View響應用戶的滑動,而處于另外一種狀態(tài)時則需要內(nèi)部View來響應View的滑動厕氨,根據(jù)這種業(yè)務上的需求我們也能得出相應的處理規(guī)則进每,有了處理規(guī)則同樣可以進行下一步處理。這種場景通過文字描述可能比較抽象命斧,在下一節(jié)會通過實際的例子來演示這種情況的解決方案田晚,那時就容易理解了,這里先有這個概念即可冯丙。
圖3-5 滑動過程示意
對于場景3來說肉瓦,它的滑動規(guī)則就更復雜了,和場景2一樣胃惜,它也無法直接根據(jù)滑動的角度泞莉、距離差以及速度差來做判斷,同樣還是只能從業(yè)務上找到突破點船殉,具體方法和場景2一樣鲫趁,都是從業(yè)務的需求上得出相應的處理規(guī)則,在下一節(jié)將會通過實際的例子來演示這種情況的解決方案利虫。
3.5.3 滑動沖突的解決方式
在3.5.1節(jié)中描述了三種典型的滑動沖突場景挨厚,在本節(jié)將會一一分析各種場景并給出具體的解決方法。首先我們要分析第一種滑動沖突場景糠惫,這也是最簡單疫剃、最典型的一種滑動沖突,因為它的滑動規(guī)則比較簡單硼讽,不管多復雜的滑動沖突巢价,它們之間的區(qū)別僅僅是滑動規(guī)則不同而已。拋開滑動規(guī)則不說,我們需要找到一種不依賴具體的滑動規(guī)則的通用的解決方法壤躲,在這里城菊,我們就根據(jù)場景1的情況來得出通用的解決方案,然后場景2和場景3我們只需要修改有關(guān)滑動規(guī)則的邏輯即可碉克。
上面說過凌唬,針對場景1中的滑動,我們可以根據(jù)滑動的距離差來進行判斷漏麦,這個距離差就是所謂的滑動規(guī)則客税。如果用ViewPager去實現(xiàn)場景1中的效果,我們不需要手動處理滑動沖突唁奢,因為ViewPager已經(jīng)幫我們做了霎挟,但是這里為了更好的演示滑動沖突的解決思想,沒有采用ViewPager麻掸。其實在滑動過程中得到滑動的角度這個是相當簡單的酥夭,但是到底要怎么做才能將點擊事件交給合適的View去處理呢?這時就要用到3.4節(jié)所講述的事件分發(fā)機制了脊奋。針對滑動沖突熬北,這里給出兩種解決滑動沖突的方式:外部攔截法和內(nèi)部攔截法。
1. 外部攔截法
所謂外部攔截法是指點擊事件都先經(jīng)過父容器的攔截處理诚隙,如果父容器要此事件就攔截讶隐,如果不需要此事件就不攔截,這樣就可以解決滑動沖突的問題久又,這種方式比較符合點擊事件的分發(fā)極致巫延。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做相應的攔截即可地消,這種方法的偽代碼如下所示炉峰。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVW: {
if (父容器需要當前點擊事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
上述代碼是外部攔截法的典型邏輯,針對不同的滑動沖突脉执,只需要修改父容器需要當前點擊事件這個條件即可疼阔,其他均不需要做修改并且也不能修改。這里對上述代碼再描述一下半夷,在onInterceptTouchEvent方法中婆廊,首先是ACTION_DOWN這個事件,父容器必須返回false巫橄,即不攔截ACTION_DOWN事件淘邻,這是因為一旦父容器攔截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理湘换,這個時候事件沒法再傳遞給子元素了列荔;其次是ACTION_MOVE事件敬尺,這個事件可以根據(jù)需要來決定是否攔截,如果父容器需要攔截就返回true贴浙,否則返回false;最后是ACTION_UP事件署恍,這里必須要返回false崎溃,因為ACTION_UP事件本身沒有太多意義。
考慮一種情況盯质,假設(shè)事件交由子元素處理袁串,如果父容器在ACTION_UP時返回了true,就會導致子元素無法接收到ACTION_UP事件呼巷,這個時候子元素中的onClick事件就無法觸發(fā)囱修,但是父容器比較特殊,一旦它開始攔截任何一個事件王悍,那么后續(xù)的事件都會交給它來處理破镰,而ACTION_UP作為最后一個事件也必定可以傳遞給父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP時返回了false压储。
2. 內(nèi)部攔截法
內(nèi)部攔截法是指父容器不攔截任何事件鲜漩,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉集惋,否則就交由父容器進行處理孕似,這種方法和Android中的事件分發(fā)機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作刮刑,使用起來較外部攔截法稍顯復雜喉祭。它的偽代碼如下所示,我們需要重寫子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) exent.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.disapatchTouchEvent(event);
}
上述代碼是內(nèi)部攔截法的典型代碼雷绢,當面對不同的滑動策略時只需要修改里面的條件即可泛烙,其他不需要做改動而且也不能有改動。除了子元素需要做處理以外习寸,父元素也要默認攔截了ACTION_DOWN以外的其他事件胶惰,這樣當子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續(xù)攔截所需的事件霞溪。
為什么父容器不能攔截ACTION_DOWN事件呢孵滞?那是因為ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT這個標記位的控制,所以一旦父容器攔截ACTION_DOWN事件鸯匹,那么所有的事件都無法傳遞到子元素中去坊饶,這樣內(nèi)部攔截就無法起作用了。父元素所做的修改如下所示殴蓬。
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
下面通過一個實例來分別介紹這兩種方法匿级。我們來實現(xiàn)一個類似于ViewPager中嵌套ListView的效果蟋滴,為了制造滑動沖突,我們寫一個類似于ViewPager的控件即可痘绎,名字就叫HorizontalScrollViewEx津函,這個控件的具體實現(xiàn)思想會在第4章進行詳細介紹,這里只講述滑動沖突的部分孤页。
為了實現(xiàn)ViewPager的效果尔苦,我們定義了一個類似于水平的LinearLayout的東西,只不過它可以水平滑動行施,初始化時我們在它的內(nèi)部添加若干個ListView允坚,這樣一來,由于它內(nèi)部的ListView可以豎直滑動蛾号。而它本身又可以水平滑動稠项,因此一個典型的滑動沖突場景就出現(xiàn)了,并且這種沖突屬于類型1的沖突鲜结。根據(jù)滑動策略展运,我們可以選擇水平和豎直的滑動距離差來解決滑動沖突。
首先來看一下Activity中的初始化代碼轻腺,如下所示乐疆。
public class DemoActivity_1 extends Activity {
private static final String TAG = "SecondActivity";
private HorizontalScrollViewEx mListContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo_1);
Log.d(TAG, "onCreate");
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
for (int i = 0; i < 3; i++) {
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, mListContainer, false);
layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.title);
textView.setText("page " + (i + 1));
layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
createList(layout);
mListContainer.addView(layout);
}
}
private void createList(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.list);
ArrayList<String> datas = new ArrayList<String>();
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.content_list_item, R.id.name, datas);
listView.setAdapter(adapter);
}
}
上述初始化代碼很簡單,就是創(chuàng)建了3個ListView并且把ListView加入到我們自定義的HorizontalScrollViewEx中贬养,這里HorizontalScrollViewEx是父容器挤土,而ListView則是子元素,這里就不再多介紹了误算。
首先采用外部攔截法來解決這個問題仰美,按照前面的分析,我們只需要修改父容器需要攔截的條件即可儿礼。對于本例來說咖杂,父容器的攔截條件就是滑動過程中水平距離差比豎直距離差大,在這種情況下蚊夫,父容器就攔截當前點擊事件诉字,根據(jù)這一條件進行相應修改,修改后的HorizontalScrollViewEx的onInterceptTouchEvent方法如下所示知纷。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if(Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercept = " + intercepted);
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
從上面的代碼來看壤圃,它和外部攔截法的偽代碼的差別很小,只是把父容器的攔截條件換成了具體的邏輯琅轧。在滑動過程中伍绳,當水平方向的距離大時就判斷為水平滑動,為了能夠水平滑動所以讓父容器攔截事件乍桂;而豎直距離大時父容器就不攔截事件冲杀,于是事件就傳遞給了ListView效床,所以ListView也能上下滑動,如此滑動沖突就解決了权谁。至于mScroller.abortAnimation()這一句話主要是為了優(yōu)化滑動體驗而加入的剩檀。
考慮一種情況,如果此時用戶正在水平滑動旺芽,但是在水平滑動停止之前如果用戶再迅速進行豎直滑動谨朝,就會導致界面在水平方向無法滑動到終點從而處于一種中間狀態(tài)。為了避免這種不好的體驗甥绿,當水平方向正在滑動時,下一個序列的點擊事件仍然交給父容器處理则披,這樣水平方向就不會停留在中間狀態(tài)了共缕。
下面是HorizontalScrollViewEx的具體實現(xiàn),只展示了和滑動沖突相關(guān)的代碼:
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分別記錄上次滑動的坐標
private int mLastX = 0;
private int mLastY = 0;
// 分別記錄上次滑動的坐標(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
...
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted = " + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.sbortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
...
}
如果采用內(nèi)部攔截法也是可以的士复,按照前面對內(nèi)部攔截法的分析图谷,我們只需要修改ListView的dispatchTouchEvent方法中的父容器的攔截邏輯,同時讓父容器攔截ACTION_MOVE和ACTIN_UP事件即可阱洪。為了重寫ListView的dispatchTouchEvent方法便贵,我們必須自定義一個ListView,稱之為ListViewEx冗荸,然后對內(nèi)部攔截法的末班代碼進行修改承璃,根據(jù)需要,ListViewEx的實現(xiàn)如下所示蚌本。
public class ListViewEx extends ListView {
private static final String TAG = "ListViewEx";
private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;
// 分別記錄上次滑動的坐標
private int mLastX = 0;
private int mLastY = 0;
...
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mHorzontalScrollViewEx2.requestDisallowInterceptToucheEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
除了上面對ListView所做的修改盔粹,我們還需要修改HorizontalScrollViewEx的onInterceptTouchEvent方法,修改后的類暫且叫HorizontalScrollViewEx2程癌,其onInterceptTouchEvent方法如下所示舷嗡。
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) ecent.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
上面的代碼就是內(nèi)部攔截法的示例,其中mScroller.abortAnimation()這一句不是必須的嵌莉,在當前這種情形下主要是為了優(yōu)化滑動體驗进萄。從實現(xiàn)上來看,內(nèi)部攔截法的操作要稍微復雜一些锐峭,因此推薦采用外部攔截法來解決常見的滑動沖突撤奸。
前面說過怜浅,只要我們根據(jù)場景1的情況來得出通用的解決方案,那么對于場景2和場景3來說我們只需要修改相關(guān)滑動規(guī)則的邏輯即可,下面我們就來演示如何利用場景1得出的通用的解決方案來解決更復雜的滑動沖突扬虚。這里只詳細分析場景2中的滑動沖突,對于場景3中的疊加型滑動沖突涨颜,由于它可以拆解為單一的滑動沖突。所以其滑動沖突的解決思想和場景1曙旭、場景2中的單一滑動沖突的解決思想一致,只需要分別解決每層之間的滑動沖突即可晶府,再加上本書的篇幅有限桂躏,這里就不對場景3進行詳細分析了。
對于場景2來說川陆,它的解決方法和場景1一樣剂习,只是滑動規(guī)則不同而已,在前面我們已經(jīng)的出了通用的解決方案较沪,因此這里我們只需要替換父容器的攔截規(guī)則即可鳞绕。注意,這里不再演示如何通過內(nèi)部攔截法來解決場景2中的滑動沖突尸曼,因為內(nèi)部攔截法沒有外部攔截法簡單易用们何,所以推薦采用外部攔截法來解決常見的滑動沖突。
下面通過一個實際的例子來分析場景2控轿,首先我們提供一個可以上下滑動的父容器冤竹,這里就叫StickyLayout,它看起來就像是可以上下滑動的豎直的LinearLayout茬射,然后在它的內(nèi)部分別放一個Header和一個ListView鹦蠕,這樣內(nèi)外兩層都能上下滑動,于是就形成了場景2中的滑動沖突了在抛。當然這個StickyLayout是有滑動規(guī)則的:當Header顯示時或者ListView滑動到頂部時钟病,由StickyLayout攔截事件;當Header隱藏時霜定,這要分情況档悠,如果ListView已經(jīng)滑動到頂部并且當前手勢是向下滑動的話,這個時候還是StickyLayout攔截事件望浩,其他情況則由ListView攔截事件辖所。這種滑動規(guī)則看起來有點復雜,為了解決它們之間的滑動沖突磨德,我們還是需要重寫父容器StickyLayout的onInterceptTouchEvent方法缘回,至于ListView則不用做任何修改,我們來看一下StickyLayout的具體實現(xiàn)典挑,滑動沖突相關(guān)的主要代碼如下所示酥宴。
public class StickyLayout extends LinearLayout {
private int mTouchSlop;
// 分別記錄上次滑動的坐標
private int mLastX = 0;
private int mLastY = 0;
// 分別記錄上次滑動的坐標(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
...
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int intercepted = 0;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastXIntercept = x;
mLastYIntercept = y;
mLastX = x;
mLastY = y;
intercepted = 0;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x = mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
intercepted = 0;
} else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
intercepted = 1;
} else if (mGiveUpTouchEventListener != null) {
if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
intercepted = 1;
}
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = 0;
mLastXIntercept = mLastYIntercept = 0;
break;
}
default:
break;
}
if (DEBUG) {
Log.d(TAG, "intercepted = " + intercepted);
}
return intercepted != 0 && mIsSticky;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsSticky) {
rturn true;
}
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (DEBUG) {
Log.d(TAG, "mHandlerHeight = " + mHeaderHeight + " deltaY = " + deltaY + " mLastY = " + mLastY);
}
mHanderHeight += deltaY;
setHeaderHeight(mHeaderHeight);
break;
}
case MotionEvent.ACTION_UP: {
// 這里做了一下判斷,當松開手的時候您觉,會自動向兩邊滑動拙寡,具體往哪邊滑,要看當前所處的位置
int destHeight = 0;
if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {
destHeight = 0;
mStatus = STATUS_COLLAPSED;
} else {
destHeight = mOriginalHeaderHeight;
mStatus = STATUS_EXPANDED;
}
// 慢慢滑向終點
this.smoothSetHeaderHeight(mHeaderHeight, destHeight, 500);
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
...
}
從上面的代碼來看琳水,這個StickyLayout的實現(xiàn)有點復雜肆糕,在第4章會詳細介紹這個自定義View的實現(xiàn)思想般堆,這里先有大概的印象即可。下面我們主要看它的onInterceptTouchEvent方法中對ACTION_MOVE的處理诚啃。如下所示淮摔。
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (mDisallowInterceptTouchEventOnHeader && y <= getHanderHeight()) {
intercepted = 0;
} else if (Match.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
intercepted = 1;
} else if (mGiveUpTouchEventListener != null) {
if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
intercepted = 1;
}
}
break;
}
我們來分析上面這段代碼的邏輯,這里的父容器是StickyLayout始赎,子元素是ListView和橙。首先,當事件落在Header上面時父容器不會攔截事件造垛;接著魔招,如果豎直距離差小于水平距離差,那么父容器也不會攔截事件五辽;然后仆百,當Header是展開狀態(tài)并且向上滑動時父容器攔截事件。另外一種情況奔脐,當ListView滑動到頂部了并且向下滑動時,父容器也會攔截事件吁讨,經(jīng)過這些層層判斷就可以達到我們想要的效果了髓迎。另外,giveUpTouchEvent是一個接口方法建丧,由外部實現(xiàn)排龄,在本例中主要是用來判斷ListView是否滑動到頂部,它的具體實現(xiàn)方法如下:
public boolean giveUpTouchEvent(MotionEvent event) {
if (expandableListView.getFirstVisiblePosition() == 0) {
View view = expandableListView.getChildAt(0);
if (view != null && view.getTop() >= 0) {
return true;
}
}
return false;
}
上面這個例子比較復雜翎朱,需要讀者多多體會其中的寫法和思想橄维。到這里滑動沖突的解決方法就介紹完畢了,至于場景3中的滑動沖突拴曲,利用本節(jié)所給出的通用的方法是可以輕松解決的争舞,讀者可以自己聯(lián)系一下。在第4章會介紹View的底層工作原理澈灼,并且會介紹如何寫出一個好的自定義View竞川。同時,在本節(jié)中所提到的兩個自定義View:HorizontalScrollViewEx和StickyLsyout將會在第4章中進行詳細的介紹叁熔,它們的完整源碼請看本書所提供的示例代碼委乌。