前言
最近項目中有一個視頻小窗口,爲什麼不是懸浮窗,是因爲在固定的父窗口可以隨意拖動,可以點擊進入房間或者點擊關閉按鈕關閉小窗口。效果圖如下:
看到上方的效果圖,大概也知道需要自定義ViewGroup,既需要處理ViewGroup的移動事件又同時兼容子類的點擊事件。其實之前也寫過一篇關於事件分發的文章,但是覺得網上太多這類文章了就沒有發表,網上有大把關於事件分發的文章,我剛開始參加工作那會,由於感覺事件分發一直模糊不清,就去網上看到各種文章解析,看的時候覺得還好,就跟着文章看一下源碼,順着流程走一遍,但是發現遇到實際情況還是感覺有心無力。但是現在再看網上關於事件分發的文章,我發現了一些問題,有部分人對於事件分發瞭解並不是很清晰,還有一部分是隻對分發、攔截、回傳、反攔截等源碼說一遍,但是事件下面細分的ACTION_DOWN、ACTION_MOVE、ACTION_UP並不是很清楚,不過最多的都是隻是說理論,可能正在遇到實際問題還會感覺束手無策。接下來幾篇文章我會根據實際項目遇到的例子來說明事件分發。
功能實現
首先要分析整個功能,需要自定義ViewGroup繼承現有的各種ViewGroup,剩下的就是關於自定義VieGroup功能點的實現,首先滑動事件分配給ViewGroup,讓之在父窗口之內隨意滑動,其次是ViewGroup中的其他子控件的點擊事件要分發下去,講到這裏大家也明白了,肯定涉及到事件分發衝突問題,需要判斷當前事件是滑動還是點擊事件,如果是滑動需要攔截,如果是點擊分發給子類就行。
分析功能點
- 整個ViewGroup可以在父窗口隨意滑動
- 點擊整個條目又可以跳轉
- 點擊關閉按鈕可以關閉當前的ViewGroup
判斷是否是滑動
判斷是否是滑動分爲兩部分,一是系統能檢測到的最小滑動距離(常量爲8dp),系統提供一個API,所以我們可以直接利用此API來判斷是否是滑動,。二是滑動過程要攔截ACTION_MOVE,響應自己的onTouchEvent方法處理ViewGroup的滑動。
//獲取系統檢測最小滑動距離
ViewConfiguration.get(mContext).getScaledTouchSlop();
//判斷是否是滑動
if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
interceptd = true;
} else {
interceptd = false;
}
攔截事件
先看攔截事件的處理,我下面分析,攔截事件處理如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean interceptd = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
interceptd = false;
break;
case MotionEvent.ACTION_MOVE:
//計算移動距離 判定是否滑動
float dx = event.getX() - mDownX;
float dy = event.getY() - mDownY;
if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
interceptd = true;
} else {
interceptd = false;
}
break;
case MotionEvent.ACTION_UP:
interceptd = false;
break;
}
return interceptd;
}
關於攔截事件的分析:
首先,只有判定爲滑動的時候才需要攔截事件,由於攔截事件是在分發事件裏面調用的,並且由於dispatchTouchEvent內部及其複雜,所以處理滑動衝突,只需要處理onInterceptTouchEvent和onTouchEvent即可,除了特別情況,一般情況無需dispatchTouchEvent事件。攔截事件onInterceptTouchEvent只要返回true就會把事件攔截掉,這樣ACTION_DOWN、ACTION_UP直接返回false就行,ACTION_MOVE事件的時候,如果X、Y軸其中之一的移動距離大於系統能檢測的最小滑動距離就判定爲滑動。
注意事項:
1、系統對於一個事件裏面的三個子事件的優先級不同,ACTION_DOWN優先級是最高的。如果攔截事件裏面攔截了ACTION_DOWN事件,其他的事件都會被攔截
2、requestDisallowInterceptTouchEvent(true)會告訴父類不要攔截事件,除了ACTION_DOWN事件以外,因爲ACTION_DOWN在分發的過程中,會重置requestDisallowInterceptTouchEvent方法的標誌位,因此如果父類設置了攔截ACTION_DOWN,即使子類調用requestDisallowInterceptTouchEvent(true)強制請求事件也是無法響應的。
消費事件
攔截事件已經處理了在滑動的時候攔截,所以在onTouchEvent事件裏就可以直接處理滑動事件了
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if (mDownX >= 0
&& mDownY >= mRootTopY
&& mDownX <= mRootMeasuredWidth
&& mDownY <= (mRootMeasuredHeight + mRootTopY)) {
float dx = event.getX() - mDownX;
float dy = event.getY() - mDownY;
float ownX = getX();
//獲取手指按下的距離與控件本身Y軸的距離
float ownY = getY();
//理論中X軸拖動的距離
float endX = ownX + dx;
//理論中Y軸拖動的距離
float endY = ownY + dy;
//X軸可以拖動的最大距離
float maxX = mRootMeasuredWidth - getWidth();
//Y軸可以拖動的最大距離
float maxY = mRootMeasuredHeight - getHeight();
//X軸邊界限制
endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
//Y軸邊界限制
endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
//開始移動
setX(endX);
setY(endY);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
關於消費滑動事件的分析:
首先要判斷需要在父窗口內隨意滑動,所以需要下面這一判斷條件,具體父窗口的位置在接下來會說明。
if (mDownX >= 0
&& mDownY >= mRootTopY
&& mDownX <= mRootMeasuredWidth
&& mDownY <= (mRootMeasuredHeight + mRootTopY))
接着要判斷最後的位置不能超過父類所在的區域
//X軸邊界限制
endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
//Y軸邊界限制
endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
最後通過使用setX(endX)來設置位置,爲什麼不使用其他幾種方式設置位置呢?,可能有人使用過layout(int l, int t, int r, int b)這個方法來移動View,那肯定會遇到翻頁或者刷新,原本移動的View又恢復到原來位置了,那是因爲只要父類調用requestLayout();方法,子類就會重新佈局,這涉及到關於view的測量、佈局、繪製過程了,還有一種方式是通過屬性動畫也可以改變位置,屬性動畫通過反射屬性改變view的真實值。
肯定還有疑問就是爲什麼onTouchEvent方法直接返回true,而不像onInterceptTouchEvent需要各個方法判斷返回值的方式,因爲滑動事件的處理要交給父類處理,需要返回true才能去消費事件而不至於分發給子類消費。
注意事項:
1、切記要使用event.getX()來或者座標,相對於父窗口的位置,而不是使用event.getRawX(),event.getRawX()是相對於整個屏幕座標
2、在ACTION_UP事件裏也可以添加貼邊動畫,增強體驗
確定父窗口
在onInterceptTouchEvent方法和dispatchTouchEvent方法裏面都可以獲取到父窗口的大小,dispatchTouchEvent是必定要走的方法,所以放在onInterceptTouchEvent方法中有利於瞭解事件的攔截。下面是onInterceptTouchEvent方法中的ACTION_DOWN事件
case MotionEvent.ACTION_DOWN:
interceptd = false;
//測量按下位置
mDownX = event.getX();
mDownY = event.getY();
//測量父類的位置和寬高
if (!mHasMeasuredParent) {
ViewGroup mViewGroup = (ViewGroup) getParent();
if (mViewGroup != null) {
//獲取父佈局的高度
mRootMeasuredHeight = mViewGroup.getMeasuredHeight();
mRootMeasuredWidth = mViewGroup.getMeasuredWidth();
int top = mViewGroup.getTop();
//獲取父佈局頂點的座標
mRootTopY = mViewGroup.getTop();;
mHasMeasuredParent = true;
}
}
break;
說明:mHasMeasuredParent參數爲了防止每次都去獲取,但是如果你的父窗口也是動態改變的,去掉此判定條件
寫在最後
至此,關於在父窗口可以隨意拖動的ViewGroup功能已經完成,通過實際的例子也發現了,事件分發並不簡單的關於一個方法的處理,有時候會涉及到好幾個方法結合起來使用。網上的文章雖然多,但是大部分都是講述事件主幹線,對於單個事件分析不透徹,當然如果本人中結論如果有誤,請及時下方評論糾正,在此感謝。由於接下來我會根據實際項目事件分發一系列的文章,所以準備把demo上傳到GitHub上,本文的demo如下。