Android自訂控制項系列 十:利用添加自訂布局來搞定觸摸事件的分發,解決組合介面中特定控制項響應特定方向的事件,android自訂控制項
這個例子是比較有用的,基本上可以說,寫完這一次,以後很多情況下,直接拿過來addView一下,然後再addInterceptorView一下,就可以輕輕鬆鬆的達到組合介面中特定控制項來響應特定方向的觸摸事件了。
請尊重原創勞動成果,轉載請註明出處:http://blog.csdn.net/cyp331203/article/details/45198549,非允許請勿用於商業或盈利用途,違者必究。
在寫Android應用的過程之中,經常會遇到這樣的情況:介面包含了多個控制項,我們希望觸摸在介面上的不同滑動動作能被不同的控制項所接收,或者在介面不同位置滑動的動作能被不同的控制項所接收,換句話說,能否讓特定子view響應特定方向的觸摸事件?一個典型的例子就是ListView和Header的組合:
遇到的問題:
在的例子中,會發現一個問題,就是當手指在頂部輪播圖上滑動的時候,如果我們想滑動輪播圖,只能在手指非常水平的時候才能讓輪播圖翻動,而在手指滑動軌跡稍微有一點傾斜的時候,就發現觸摸事件被ListView給響應了,變成了上下滑動ListView,這種體驗顯然不是很好。
假如說我們現在想要一種簡單的實現:可能整個應用有很多頁面,現在想在當前這個特定的介面,使得當手指在輪播圖範圍內滑動的時候,當手指軌跡角度<45度的時候(方向上較水平),那麼讓輪播圖響應觸摸事件,使得頂部圖片能夠水平滑動;讓當手指手勢軌跡角度>45度的時候(方向上較豎直),能夠ListView來響應觸摸事件,使得整個ListView能夠上下滑動,這種效果要如何?呢?
解決辦法:
專欄的上一篇文章中,詳細分析了Android的觸摸事件的分發流程和ViewGroup的原始碼(不熟悉的朋友可以看看:Android自訂控制項系列九:從源碼看Android觸摸事件分發機制)。看過上一篇文章之後,應該瞭解到,Andrioid事件的分發是一層一層的進行的,最開始分發的時候總是從上層到下層,從活動的Activity開始,到DecorView,然後到我們寫的布局,然後再是布局中的其他組件,那麼本文的解決辦法就是自訂一個ViewGroup,包裹在原來的ListView之外,放在這個特定的介面上。由於事件分發是一層層的進行的,所以我們重寫這個外層的自訂ViewGroup的dispatchTouchEvent方法就可以實現控制所有子view的事件分發機制,從而在這個特定的介面實現我們想要的觸摸事件的響應機制。
寫一個自定的FrameLayout叫InterceptorFrameLayout,重寫dispatchTouchEvent(MotionEvent ev)方法,主要解決幾個問題:
1、在事件分發的時候,我們得到的是MotionEvent 事件,如何判斷這個事件是否落在我們想要的控制項地區上呢?
思路:可以在InterceptorFrameLayout中,使用一個Map集合,來存放我們想要控制觸摸事件的View和對應的代表方向的參數,對外界暴露add和remove方法,來添加和移除攔截的view對象。然後拿到event事件之後,調用event.getRawX和event.getRawY可以拿到相對螢幕左上方的絕對座標,然後遍曆view的map集合對所有的判斷觸摸的絕對座標是不是在View的範圍內,且要攔截的方向參數是否符合。判斷觸摸是否在view上,可以使用view.getLocationOnScreen(int[])方法,得到的int數組,第一個元素表示view的左上方的x座標,第二個元素表示view的右上方座標,具體判斷方法如下:
public static boolean isTouchInView(MotionEvent ev, View view) {//判斷ev是否發生在view的範圍內static int[] touchLocation = new int[2];view.getLocationOnScreen(touchLocation);//通過getLocationOnScreen方法,擷取當前子view左上方的座標float motionX = ev.getRawX();float motionY = ev.getRawY();// 返回是否在範圍內,通過觸摸事件的座標和本子view的左上右下四邊的座標比較,來判斷是不是落在view內return motionX >= touchLocation[0]&& motionX <= (touchLocation[0] + view.getWidth())&& motionY >= touchLocation[1]&& motionY <= (touchLocation[1] + view.getHeight());}
/** 在集合中尋找對應event和方向參數的view,找到了則返回,沒找到返回null */private View findTargetView(MotionEvent ev, int orientation) {// mViewAndOrientation為存放要監測觸摸事件的子view和對應方向參數的集合Set<View> keySet = mViewAndOrientation.keySet();for (View view : keySet) {Integer ori = mViewAndOrientation.get(view);// 由於所有的方向參數都是二進位相互與運算為0的// 所以這裡使用與運算來判斷方向是否符合// 這裡所有的判斷條件是:// ①該子view在mViewAndOrientation集合內// ②方向一致// ③觸摸事件落在該子view的範圍內// ④該子view可以消費掉本次事件// 同時滿足上面四個條件,則代表該子view是我們要找的子view,於是返回if ((ori & orientation) == orientation && isTouchInView(ev, view)&& view.dispatchTouchEvent(ev)) {return view;}}return null;}
2、重寫dispatchTouchEvent方法:
①如何處理Down事件和Move以及Cancel和Up事件的關係。
這個關係的紐帶實際上就是mFirstTouchTarget,如果看完上一篇博文:Android自訂控制項系列九:從源碼看Android觸摸事件分發機制還有印象的話,源碼中mFirstTouchTarget會記錄能夠在Down事件時能夠消費事件的子view,然後在Down事件之後的其他事件響應,都可以根據mFirstTouchTarget的狀態來做進一步的判斷後續動作。在這裡我們也仿照源碼的方式,定義一個mFirstTarget。在每一次進入到dispatchTouchEvent的時候,先需要判斷一下mFirstTarget是否為空白,如果mFirstTarget不為空白,則代表之前有Down事件能夠被某一個監測集合中的子view消費,於是我們可以繼續調用boolean flag = mFirstTarget.dispatchTouchEvent()方法,將後續的事件(Move,Cancel,UP等)通過dispatchTouchEvent傳遞到這個對應的子view--即mFirstTarget上去;這個時候,如果flag返回true,則表示該子view(mFirstTarget)已經完全消費掉了事件,那麼就應該將mFirstTarget重新置為空白,方便下一次事件的分發;或者這個touch事件是Cancel或者Up,那麼也表示本次事件的終止,於是也要將mFirstTarget置空。然後再將flag的值返回。
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {int action = ev.getAction();// 意思應該是觸發移動事件的最短距離,如果小於這個距離就不觸發行動控制項,// 如viewpager就是用這個距離來判斷使用者是否翻頁mTouchSlop = configuration.getScaledTouchSlop();if (mFirstTarget != null) {// mFirstTarget不為空白,表示最近的一次DOWN事件已經被mViewAndOrientation集合中的某個子view響應// 於是將後續的事件繼續分發給這個子viewboolean flag = mFirstTarget.dispatchTouchEvent(ev);// 如果flag=true,表示本次事件被子view消耗,如果事件是ACTION_CANCEL或者ACTION_UP,// 也代表事件的結束,於是將mFirstTarget置空,便於下一次DOWN事件的響應if (flag&& (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)) {mFirstTarget = null;}// 返回flagreturn flag;} ...}
②處理Down事件:
在Down事件發生的時候,我們並不知道接下來的Move的方向,所以在這個時候,我們只能把事件傳遞下去,並返回合格子view的view.dispatchTouchEvent()方法的結果,如果能夠找到合格集合中的子 view,且這個子view.dispatchTouchEvent能夠返回true,代表找到了合格子view,所以將其值賦值給mFirstTarget。在Down事件的過程中,需要記錄本次Down事件的x,y座標,以供隨後的MOVE事件做判斷使用。
// 拿到本次事件的座標,由於只需要計算差值,所以getX也可以final float currentX = ev.getX();final float currentY = ev.getY();switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mFirstTarget = findTargetView(ev, ORIENTATION_ALL);downX = currentX;downY = currentY;break;
③MOVE事件:
在MOVE事件發生的時候,我們再次擷取一下當前的x,y座標,然後跟DOWN事件的時候做一下對比,即可得出當前滑動方向是朝哪個方向,然後就可以根據這個方向和觸摸事件,尋找是否具有符合要求的子view,有則賦值給mFirstTarget:
case MotionEvent.ACTION_MOVE:if (Math.abs(currentX - downX) > Math.abs(currentY - downY)&& Math.abs(currentX - downX) > mTouchSlop) {System.out.println("左右滑動");// 左右滑動if (currentX - downX > 0) {// 右滑mFirstTarget = findTargetView(ev, ORIENTATION_RIGHT);} else {// 左滑mFirstTarget = findTargetView(ev, ORIENTATION_LEFT);}} else if (Math.abs(currentY - downY) > Math.abs(currentX - downX)&& Math.abs(currentY - downY) > mTouchSlop) {System.out.println("上下滑動");// 上下滑動if (currentY - downY > 0) {// 向下mFirstTarget = findTargetView(ev, ORIENTATION_DOWN);} else {// 向上mFirstTarget = findTargetView(ev, ORIENTATION_UP);}mFirstTarget = null;}break;
④處理CANCEL或者UP事件:
如果事件是Cancel或者Up,則表示本次觸摸事件結束了,那麼將mFirstTarget置空,方便接收下一次DOWN事件:
case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:mFirstTarget = null;break;}
隨後,如果mFirstTarget不為空白,則表示找到了對應的子view來接收,不需要繼續分發事件,則返回true;如果此時mFirstTarget為空白,則表示集合中沒有能響應本次事件的子view,那麼交給super.dispatchTouchEvent(ev)處理:
// 走到這裡,只要mFirstTarget不為空白,則在集合中找到了對應的子view,// 則返回true,表示本次事件被消耗,不繼續分發if (mFirstTarget != null) {return true;} else {return super.dispatchTouchEvent(ev);}
重寫完了之後,就可以將原本添加ListView的地方用我們寫的這個InterceptorFrameLayout添加進去,然後將ListView通過addview添加成InterceptorFrameLayout的孩子。這樣就可以達到目的啦,來看看效果:
下面是InterceptorFrameLayout完整代碼:
package com.example.viewpagerlistview.view;import java.util.HashMap;import java.util.Set;import com.example.viewpagerlistview.application.BaseApplication;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.ViewConfiguration;import android.widget.FrameLayout;/** * @author : 苦咖啡 * * @version : 1.0 * * @date :2015年4月19日 * * @blog : http://blog.csdn.net/cyp331203 * * @desc : */public class InterceptorFrameLayout extends FrameLayout {/** 代表滑動方向向上 */public static final int ORIENTATION_UP = 0x1;// 0000 0001/** 代表滑動方向向下 */public static final int ORIENTATION_DOWN = 0x2;// 0000 0010/** 代表滑動方向向左 */public static final int ORIENTATION_LEFT = 0x4;// 0000 0100/** 代表滑動方向向右 */public static final int ORIENTATION_RIGHT = 0x8;// 0000 1000/** 代表滑動方向的所有方向 */public static final int ORIENTATION_ALL = 0x10;// 0001 0000/** 存放view的左上方的x和y座標 */static int[] touchLocation = new int[2];/** 用來代表觸發移動事件的最短距離,如果小於這個距離就不觸發行動控制項,如viewpager就是用這個距離來判斷使用者是否翻頁 */private int mTouchSlop;/** 用來記錄Down事件發生時的x座標 */private float downX;/** 用來記錄Down事件發生時的y座標 */private float downY;/** 用來存放需要自主控制事件分發的子view,以及其對應的滑動方向 */private HashMap<View, Integer> mViewAndOrientation = new HashMap<View, Integer>();/** 表示某次事件發生時,找到的mViewAndOrientation中合格子view */private View mFirstTarget = null;private ViewConfiguration configuration;public InterceptorFrameLayout(Context context, AttributeSet attrs,int defStyleAttr) {super(context, attrs, defStyleAttr);init();}public InterceptorFrameLayout(Context context, AttributeSet attrs) {super(context, attrs);init();}public InterceptorFrameLayout(Context context) {super(context);init();}private void init() {configuration = ViewConfiguration.get(getContext());}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {int action = ev.getAction();// 意思應該是觸發移動事件的最短距離,如果小於這個距離就不觸發行動控制項,// 如viewpager就是用這個距離來判斷使用者是否翻頁mTouchSlop = configuration.getScaledTouchSlop();if (mFirstTarget != null) {// mFirstTarget不為空白,表示最近的一次DOWN事件已經被mViewAndOrientation集合中的某個子view響應// 於是將後續的事件繼續分發給這個子viewboolean flag = mFirstTarget.dispatchTouchEvent(ev);// 如果flag=true,表示事件被完全消耗,結束了,如果事件是ACTION_CANCEL或者ACTION_UP,// 也代表事件的結束,於是將mFirstTarget置空,便於下一次DOWN事件的響應if (flag&& (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)) {mFirstTarget = null;}// 返回flagreturn flag;}// 拿到本次事件的座標,由於只需要計算差值,所以getX也可以final float currentX = ev.getX();final float currentY = ev.getY();switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mFirstTarget = findTargetView(ev, ORIENTATION_ALL);downX = currentX;downY = currentY;break;case MotionEvent.ACTION_MOVE:if (Math.abs(currentX - downX) / Math.abs(currentY - downY) > 0.5f&& Math.abs(currentX - downX) > mTouchSlop) {System.out.print("左右滑動");// 左右滑動if (currentX - downX > 0) {// 右滑mFirstTarget = findTargetView(ev, ORIENTATION_RIGHT);System.out.println("mFirstTarget="+mFirstTarget);} else {// 左滑mFirstTarget = findTargetView(ev, ORIENTATION_LEFT);System.out.println("mFirstTarget="+mFirstTarget);}} else if (Math.abs(currentY - downY) / Math.abs(currentX - downX) > 0.5f&& Math.abs(currentY - downY) > mTouchSlop) {System.out.print("上下滑動");// 上下滑動if (currentY - downY > 0) {// 向下mFirstTarget = findTargetView(ev, ORIENTATION_DOWN);System.out.println("mFirstTarget="+mFirstTarget);} else {// 向上mFirstTarget = findTargetView(ev, ORIENTATION_UP);System.out.println("mFirstTarget="+mFirstTarget);}mFirstTarget = null;}break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:mFirstTarget = null;break;}// 走到這裡,只要mFirstTarget不為空白,則在集合中找到了對應的子view,// 則返回true,表示本次事件被消耗,不繼續分發if (mFirstTarget != null) {return true;} else {return super.dispatchTouchEvent(ev);}}/** 在集合中尋找對應event和方向參數的view,找到了則返回,沒找到返回null */private View findTargetView(MotionEvent ev, int orientation) {// mViewAndOrientation為存放要監測觸摸事件的子view和對應方向參數的集合Set<View> keySet = mViewAndOrientation.keySet();for (View view : keySet) {Integer ori = mViewAndOrientation.get(view);// 由於所有的方向參數都是二進位相互與運算為0的// 所以這裡使用與運算來判斷方向是否符合// 這裡所有的判斷條件是:// ①該子view在mViewAndOrientation集合內// ②方向一致// ③觸摸事件落在該子view的範圍內// ④該子view可以消費掉本次事件// 同時滿足上面四個條件,則代表該子view是我們要找的子view,於是返回if ((ori & orientation) == orientation && isTouchInView(ev, view)&& view.dispatchTouchEvent(ev)) {return view;}}return null;}public static boolean isTouchInView(MotionEvent ev, View view) {view.getLocationOnScreen(touchLocation);float motionX = ev.getRawX();float motionY = ev.getRawY();// 返回是否在範圍內return motionX >= touchLocation[0]&& motionX <= (touchLocation[0] + view.getWidth())&& motionY >= touchLocation[1]&& motionY <= (touchLocation[1] + view.getHeight());}/** 添加攔截 */public void addInterceptorView(final View view, final int orientation) {// 到主線程執行BaseApplication.getMainThreadHandler().post(new Runnable() {@Overridepublic void run() {if (!mViewAndOrientation.containsKey(view)) {mViewAndOrientation.put(view, orientation);}}});}/** 去除攔截效果 */public void removeInterceptorView(final View v) {// 到主線程執行BaseApplication.getMainThreadHandler().post(new Runnable() {@Overridepublic void run() {if (!mViewAndOrientation.containsKey(v)) {mViewAndOrientation.remove(v);}}});}}
demo項目源碼下載:http://download.csdn.net/detail/cyp331203/8621903
請尊重原創勞動成果,轉載請註明出處:http://blog.csdn.net/cyp331203/article/details/45198549,非允許請勿用於商業或盈利用途,違者必究。