轉載 :
http://blog.csdn.net/zizidemenghanxiao/article/details/50184295
結論Tips
(1)同一個事件序列:是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束。
其中會有一個down,多個move,一個up事件。
(2)正常情況下,一個事件序列只能被一個VIew攔截且消耗,因爲一旦攔截,剩下的就都交給攔截的那個View了。不過可以採取特殊手段,比如說一個View將本該自己處理的事件通過onTouchEvent 強行傳遞給其他View處理。
(3)一個View一旦決定攔截,那剩下的事件序列都會給它,而且它的 onInterceptTouchEvent 就不會再被調用了。
(4)某個VIew一旦開始處理事件,也就是到了onTouchEvent的地步,如果它不消耗 ACTION_DOWN事件,也就是說onTouchEvent返回了false,那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交給它的父元素去處理。就是說事件一旦交給一個VIew處理,他就必須消耗掉,不然同一事件序列中剩下的事件就不再交給它來處理了。
(5)(沒看懂這條)如果VIew不消耗除 ACTION_DOWN 以外的其他事件(就是說消耗了ACTION_DOWN,卻沒消耗其他的),那麼這個點擊事件會消失,此時父元素的 onTouchEvent 並不會被調用,並且當前View可以持續接到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent 方法默認返回 false。
(7)View沒有 onInterceptTouchEvent 方法,一旦有點擊事件傳遞給它,那麼它的 onTouchEvent 方法就會被調用。
(8)View 的onTouchEvent 默認都會消耗事件(返回true),除非它是不可點擊的(clickable 和 longClickable 同時爲 false)。
View的longClickable 屬性默認都爲false,clickable 屬性要看具體的控件,比如Button爲true,TextView爲false。
(9)View的enable 屬性不影響 onTouchEvent 的默認返回值。哪怕一個View是disable狀態的,只要它的clickable 或者 longClickable 有一個爲true,那麼它的onTouchEvent 就返回ture。
(10)onClick會發生的前提是當前的View 是可點擊的,並且它收到了 down 和 up的事件。
(11)事件傳遞過程是由外向內的,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,就是下面的那個FLAG_DISALLOW_INTERCEPT標誌位,但是ACTION_DOWN事件除外。
點擊事件的分發過程由三個很重要的方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發,dispatch派分。如果事件能夠傳遞給當前View,這個方法就一定會被調用,
返回結果受當前View的 onTouchEvent 和下級 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法的內部調用,用來判斷是否攔截某個事件,如果當前View 攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,
返回結果表示是否攔截當前事件。true表示攔截。
public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法中調用,用來處理點擊事件,
返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View再也無法接收到該事件的後續事件。
就是說如果消耗了down,還可以消耗move,up等,如果不消耗,後續的就都沒有了。
但如果不消耗,該事件序列就要交由上一層來處理。
一、View基礎知識
1、什麼是View:
View是一種界面層的控件的一種抽象,它代表了一個控件。
ViewGroup爲控件組。
View中也可以包含多個控件,形成控件樹。
ImageButton是一個View,LinearLayout是一個View,也是一個ViewGroup。
2、View的位置參數:
(1)視圖座標系:(子視圖在父視圖中的位置關係)
以父視圖的左上角爲原點。
在觸控事件MotionEvent中,通過getX()、getY()所獲得的座標就是視圖座標系中的座標。
View的位置對應View的四個屬性:top、left、right、button。
所以說View的寬高分別可以這樣表示:
width = right - left
height = bottom - top
獲取方式:getLeft(),getRight(),getTop(),getBottom()。
從Android3.0開始,另外還有幾個參數:x、y、translationX 和 translationY。
x、y是VIew左上角的座標,translationX 和 translationY 是View左上角相對於父容器的偏移量。默認爲0。
且 x = left + translationX,y = top + translationY
是不是還是不清楚,就是說在View平移的過程中,top、left、right、button這四個值是不會變化的,是原始位置,變化的是x、y、translationX 和 translationY這四個參數。
(2)Android座標系(以屏幕的左上角爲原點)
系統提供了 getLocationOnScreen(intlocation[])這樣的方法來獲取Android座標系中點的位置,即該視圖左上角在Android座標系中的座標。
另外在觸控事件MotionEvent中使用getRawX()、getRawY()方法所獲得的座標同樣是An座標系中的座標。
(3)總結獲取各種座標值的辦法:
屬於MotionEvent的:
getX(),getY(),(相對父容器的)
getRawX(),getRawY();(相對屏幕的)
屬於View自身的:
getTop(),getRight(),getTop(),getBottom()。(相對父容器的)
3、MotionEvent 手指觸摸事件類型
ACTION_DOWN、ACTION_MOVE、ACTION_UP 就是這三個啦。
在這裏我們將常會獲取座標:
getX/getY:返回相對於當前View左上角的 x 和 y 座標。(視圖座標系)
getRawX/getRawY:返回相對於手機屏幕左上角的 x 和 y 座標。(Android座標系)
- /**
- * 按下
- */
- public static final int ACTION_DOWN = 0;
-
- /**
- * 擡起
- */
- public static final int ACTION_UP = 1;
-
- /**
- * 移動
- */
- public static final int ACTION_MOVE = 2;
-
- /**
- * 觸摸動作取消
- */
- public static final int ACTION_CANCEL = 3;
-
- /**
- * 觸摸動作超出邊界
- */
- public static final int ACTION_OUTSIDE = 4;
-
- /**
- * 多點觸摸按下動作
- */
- public static final int ACTION_POINTER_DOWN = 5;
-
- /**
- * 多點離開動作
- */
- public static final int ACTION_POINTER_UP = 6;
4、TouchSlop 系統能識別的最小滑動距離
獲取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop() 。
我在程序中用的時候發現需要這樣寫才行:要用this才行:
- private int m = ViewConfiguration.get(DemoActivity_1.this).getScaledTouchSlop();
其中的get是爲了獲取一個ViewConfiguration類型的對象,然後這個對象再調用getScaledTouchSlop方法。
而它的默認值是定義在這裏的:
裏面的config.xml中:
- <!-- Base "touch slop" value used by ViewConfiguration as a
- movement threshold where scrolling should begin. -->
- <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
5、VelocityTracker速度追蹤
用於追蹤手指在滑動過程中的速度,包括水平和豎直。
它所謂的滑動速度指的是一段時間內手指劃過的像素數,比如假設時間間隔爲1s,速度就指的是手指在水平方向從左向右滑過100像素時,速度就是100,那如果向左滑動,速度就是負值啦。
- // 首先在View的onTouchEvent方法中獲取追蹤速度的對象
- VelocityTracker velocityTracker = VelocityTracker.obtain();
- // 將追蹤速度對象與事件綁定起來:
- velocityTracker.addMovement(event);
- // 獲取速度之前先計算速度,這一句不可缺:
- velocityTracker.computeCurrentVelocity(1000);
- // 分別計算水平和豎直速度:
- int xVelocity = (int) velocityTracker.getXVelocity();
- int yVelocity = (int) velocityTracker.getYVelocity();
-
- // 不需要使用它的時候,來重置和回收:
- velocityTracker.clear();
- velocityTracker.recycle();
6、GestureDetector手勢檢測
用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。
一般情況下,我們知道View類有個View.OnTouchListener內部接口,通過重寫他的onTouch(View v, MotionEvent event)方法,我們可以處理一些touch事件,但是這個方法太過簡單,如果需要處理一些複雜的手勢,用這個接口就會很麻煩(因爲我們要自己根據用戶觸摸的軌跡去判斷是什麼手勢)。
Android sdk給我們提供了GestureDetector(Gesture:手勢Detector:識別)類,通過這個類我們可以識別很多的手勢,主要是通過他的onTouchEvent(event)方法完成了不同手勢的識別。雖然他能識別手勢,但是不同的手勢要怎麼處理,應該是提供給程序員實現的。
GestureDetector這個類對外提供了兩個接口:OnGestureListener,OnDoubleTapListener,還有一個內部類SimpleOnGestureListener。
GestureDetector.OnDoubleTapListener接口:用來通知DoubleTap事件,類似於鼠標的雙擊事件。
1、onDoubleTap(MotionEvent e):
在雙擊的第二下,按下時觸發 。它不能和 onSingleTapConfirmed 共存。
2、onDoubleTapEvent(MotionEvent e):
通知雙擊手勢中的事件,包含down、up和move事件。
(這裏指的是在雙擊之間發生的事件,例如在同一個地方雙擊會產生雙擊手勢,而在雙擊手勢裏面還會發生down和up事件,這兩個事件由該函數通知);
雙擊的第二下按下時,down和up都會觸發,可用e.getAction()區分。
3,onSingleTapConfirmed(MotionEvent e):
用來判定該次點擊是單擊而不是雙擊,如果連續點擊兩次就是雙擊手勢,如果只點擊一次,系統等待一段時間後沒有收到第二次點擊則判定該次點擊爲單擊而不是雙擊,然後觸發SingleTapConfirmed事件。這個方法不同於onSingleTapUp,他是在GestureDetector確信用戶在第一次觸摸屏幕後,沒有緊跟着第二次觸摸屏幕,也就是不是「雙擊」的時候觸發
GestureDetector.OnGestureListener接口:用來通知普通的手勢事件,該接口有如下六個回調函數:
1、onDown(MotionEvent e):
down事件;
2、onSingleTapUp(MotionEvente):
一次點擊up事件;在touch down後又沒有滑動(onScroll),又沒有長按(onLongPress),然後Touch up時觸發。
點擊一下非常快的(不滑動)Touchup:
onDown->onSingleTapUp->onSingleTapConfirmed
點擊一下稍微慢點的(不滑動)Touchup:
onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed
3、onShowPress(MotionEvent e):
down事件發生而move或則up還沒發生前觸發該事件;按下了還沒有滑動時觸發(與onDown,onLongPress)。
比較:onDown只要按下後一定立刻觸發。而按下後停留一會兒且沒有滑動,則先觸發onShowPress再是onLongPress。
所以按下後一直不滑動按照:onDown->onShowPress->onLongPress這個順序觸發。
4、onLongPress(MotionEvent e):
長按事件;按下了不移動一直按着的時候觸發。
5、onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY):
滑動手勢事件;按下了滑動一點距離後,在ACTION_UP時纔會觸發參數:
e1 第1個ACTION_DOWN 事件並且只有一個;
e2 最後一個ACTION_MOVE 事件 ;
velocityX X軸上的移動速度,像素/秒 ;
velocityY Y軸上的移動速度,像素/秒.
觸發條件:X軸的座標位移大於FLING_MIN_DISTANCE,且移動速度大於FLING_MIN_VELOCITY個像素/秒
6、onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY):
在屏幕上拖動事件。無論是用手拖動view,或者是以拋的動作滾動,都會多次觸發,這個方法在ACTION_MOVE動作發生時就會觸發拋:
手指觸動屏幕後,稍微滑動後立即鬆開:
onDown-----》onScroll----》onScroll----》onScroll----》………----->onFling
拖動:
onDown------》onScroll----》onScroll------》onFiling
- // 首先需要創建GestureDetector對象,實現OnGestureListener接口:下面這個語句寫的有點問題
- GestureDetector mGestureDetector = new GestureDetector(this);
- // 解決長按屏幕後無法拖動的現象:
- mGestureDetector.setIsLongpressEnabled(false);
- // 接管目標View 的 onTouchEvent 方法,在待監聽View的onTouchEvent方法中添加如下實現:
- boolean consume = mGestureDetector.onTouchEvent(event);
- return consume;
三、View的滑動
要實現View的滑動,就必須監聽用戶的觸摸事件,並根據事件傳入的座標,動態且不斷地改變View的座標,從而實現View跟隨用戶觸摸的滑動而滑動。
不管採用哪一種方式,其實現的思想基本是一致的:
當觸摸View時,系統記下當前觸摸點的座標,
當手指移動時,系統記下移動後的觸摸點座標,從而獲取到相對前一次座標點的偏移量,並通過偏移量來修改View的座標。
這樣不斷重複,從而實現滑動過程。
三種方法實現View的滑動:
(1)View本身提供的scrollTo/scrollBy。
(2)通過動畫給View施加平移效果來實現滑動。
(3)通過改變View的LayoutParams使得View重新佈局從而實現滑動。
(4)layout()方法。
(5)offsetLeftAndRight()與offsetTopAndBottom()。
(6)Scroller彈性滑動
1、使用scrollTo/scrollBy:(操作簡單,適合對View內容的滑動)
scrollTo、scrollBy方法移動的是View的content內容,即讓View的內容移動,
如果在ViewGroup中使用scrollBy、scrollTo方法,那麼移動的將是所有的子View,
但如果在View中使用,那麼移動的將是View的內容,
例如對TextView這個View而言,文本就是它的內容,對於ImageView而言,drawable就是它的內容,但TextView和ImageView本身的View卻沒有移動。
所以寫的時候應該這樣寫:在View所在的ViewGroup中來使用:
- ((View)getParent()).scrollBy(offsetX, offsetY);
(1)源碼位置在:sources\android\view\View.java
- /**
- * Set the scrolled position of your view. This will cause a call to
- * {@link #onScrollChanged(int, int, int, int)} and the view will be
- * invalidated.
- * @param x the x position to scroll to
- * @param y the y position to scroll to
- */
- /*
- * 實現的是基於所傳遞參數的絕對滑動,到x和y的地方
- * */
- public void scrollTo(int x, int y) {
- if (mScrollX != x || mScrollY != y) {
- int oldX = mScrollX;
- int oldY = mScrollY;
- mScrollX = x;
- mScrollY = y;
- invalidateParentCaches();
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- if (!awakenScrollBars()) {
- postInvalidateOnAnimation();
- }
- }
- }
-
- /**
- * Move the scrolled position of your view. This will cause a call to
- * {@link #onScrollChanged(int, int, int, int)} and the view will be
- * invalidated.
- * @param x the amount of pixels to scroll by horizontally
- * @param y the amount of pixels to scroll by vertically
- */
- /*
- * 實現的是基於所傳遞參數的相對滑動
- * */
- public void scrollBy(int x, int y) {
- scrollTo(mScrollX + x, mScrollY + y);
- }
(2)其中的mScrollX和mScrollY,可以通過getScrollX和getScrollY來獲得。
- /**
- * Return the scrolled top position of this view. This is the top edge of
- * the displayed part of your view. You do not need to draw any pixels above
- * it, since those are outside of the frame of your view on screen.
- *
- * @return The top edge of the displayed part of your view, in pixels.
- */
- public final int getScrollY() {
- return mScrollY;
- }
在滑動的過程中,mScrollX的值總是等於View的左邊緣和View內容左邊緣在水平方向的距離,
而mScrollY的值總是等於View上邊緣和View內容上邊緣在豎直方向的距離。
View邊緣指的是View的位置,由四個頂點組成,
而View內容邊緣指的是View中內容的邊緣。
scrollTo和scrollBy只能改變View內容的位置而不能改變View在佈局中的位置。假位移啦!!!!!!
這個圖裏面主要注意它們的那個正負值吶。

2、使用動畫:(操作簡單,適用於沒有交互的View和複雜動畫效果的View)
(1)使用動畫來移動View,主要是操作View的translationX和translationY屬性。
(2)動畫包括傳統的View動畫和屬性動畫。
(3)注意點:同上面的scrollBy 和scrollTo一樣,View動畫是對View的影像做操作,它並不能真正改變View的位置參數,包括寬高。
並且如果希望動畫後的狀態得以保留還必須將 fillAfter屬性設置爲true,否則動畫完成後其動畫結果會消失。
(4)屬性動畫並不存在(3)這樣的情況。
(5)情況(3)會導致一個嚴重的後果,就是移動後不能帶着它的點擊事件一起移動。那怎麼辦呢?有兩種方法:
第一種:使用屬性動畫。
第二種:我們可以在新的位置上預先創建一個和目標Button一模一樣的Button,它們連onClick事件也一樣。所以移動後,設置顯示和隱藏,來間接達到移動的目的。
(6)複雜效果用動畫。
3、改變佈局參數LayoutParams:(操作稍微複雜,適用於有交互的View)
(1)改變Button的參數:主要改變的是margin屬性。
- MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
- params.width += 100;
- params.leftMargin += 100;
- mButton.requestLayout();
- // 或者mButton.setLayoutParams(params);
- params.leftMargin = getLeft() + offsetX;
- params.topMargin = getTop() + offsetY;
- LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)getLayoutParams();
用這個params的時候需要考慮父佈局的類型,當然還可以使用ViewGroup.MarginLayoutParams來實現這個功能,效果是一樣的,並且更加方便,不需要考慮父佈局的類型:
- ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();
(2)在Button的左邊放置一個空的View,這個空View的默認寬度爲0。當我們需要向右移動Button時,只需要重新設置空View的寬度即可。
4、layout方法:
通過調用View的layout方法,給layout設置新的值:(使用相對父容器的座標)
- @Override
- public boolean onTouchEvent(MotionEvent event) {
-
- // 每次進來獲取以下現在的座標值:
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- // 記錄觸摸點座標:
- lastX = x;
- lastY = y;
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- int offsetX = x - lastX;
- int offsetY = y - lastY;
- // 在當前left、top、right、bottom的基礎上加上偏移量(注意這個左上右下的順序)
- layout(getLeft() + offsetX,
- getTop() + offsetY,
- getRight() + offsetX,
- getBottom() + offsetY,
- );
- break;
- }
- case MotionEvent.ACTION_UP: {
- break;
- }
- default:
- break;
- }
-
- return true;
- }
下面使用(絕對座標,相對屏幕的座標):
- @Override
- public boolean onTouchEvent(MotionEvent event) {
-
- // 每次進來獲取以下現在的座標值:
- int rawX = (int) event.getRawX();
- int rawY = (int) event.getRawY();
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- // 記錄觸摸點座標:
- lastX = rawX;
- lastY = rawY;
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- int offsetX = rawX - lastX;
- int offsetY = rawY - lastY;
- // 在當前left、top、right、bottom的基礎上加上偏移量(注意這個左上右下的順序)
- layout(getLeft() + offsetX,
- getTop() + offsetY,
- getRight() + offsetX,
- getBottom() + offsetY,
- );
- /*
- * 這裏是必須要注意的一點
- * 使用絕對座標系,在每次執行完ACTION_DOWN的邏輯後,
- * 一定要重新設置初始座標,這樣才能準確的獲取偏移量。
- * */
- lastX = rawX;
- lastY = rawY;
- break;
- }
- case MotionEvent.ACTION_UP: {
- break;
- }
- default:
- break;
- }
-
- return true;
- }
5、offsetLeftAndRight()與offsetTopAndBottom()
這個方法相當於系統提供了一個對左右、上下移動的API的封裝。
當計算出偏移量後,只需要使用如下代碼就可以完成View的重新佈局,
效果與使用Layout方法一樣,
代碼如下所示:
- // 同時對left和right進行偏移:
- offsetLeftAndRight(offsetX);
- // 同時對top和bottom進行偏移:
- offsetTopAndBottom(offsetY);
6、Scroller彈性滑動
Scroller的工作機制:Scroller本身並不能實現View的滑動,它需要配合View的computeScroll方法才能完成彈性滑動的效果,它不斷地讓View重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出View當前的滑動位置,知道了滑動位置就可以通過scrollTp方法來完成View的滑動。就這樣,View的每一次重繪都會導致View進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動。
使用Scroller類通常需要三個步驟:
(1)初始化Scroller:
- Scroller scroller = new Scroller(mContext);
(2)重寫computeScroll()方法,實現模擬滑動:
它是scroller的核心,系統在繪製View的時候會在draw()方法中調用該方法。
這個方法實際上就是使用的scrollTo方法,再結合Scroller對象,幫助獲取到當前的滾動值。
我們可以通過不斷地瞬間移動一個小的距離來實現整體上的平滑移動效果:
- @Override
- public void computeScroll(){
- super.computeScroll();
- // 判斷Scroller是否執行完畢
- if(mScroller.computeScrollOffest()){
- ((View)getParent()).scrollTo(
- mScroller.getCurrX(),
- mScroller.getCurrY()
- );
- // 通過重繪來不斷調用computeScroll:
- invalidate();
- }
- }
Scroller類提供了computeScrollOffset()方法來判斷是否完成了整個滑動,同時也提供了
getCurrX()、getCurrY()方法來獲得當前的滑動座標。
還有一個注意點就是invalidate()方法,因爲只能在computeScroll()方法中獲取模擬過程中的scrollX和scrollY座標。
但computeScroll()方法是不會自動調用的,只能通過invalidate()-> draw() -> computeScroll()來間接調用computeScroll()方法,
所以需要在上面的代碼中調用invalidate()方法,實現循環獲取scrollX和scrollY的目的。
而當模擬過程結束以後,scroller.computeScrollOffset()方法會返回false,從而中斷循環,完成整個平滑移動過程。
(3)startScroll開啓模擬過程:
有兩個重載方法:
- public void startScroll(int startX, int startY, int dx, int dy, int duration)
-
- public void startScroll(int startX, int startY, int dx, int dy)
在獲取座標時,通常可以使用
getScrollX()和getScrollY()方法來獲取父視圖中content所滑動到的點的座標,不過還是要注意正負值,和scrollTo、scrollBy一樣正負值相反。
(4)具體的使用:
就是在ACTION_UP的時候,寫上面的(1)(2)就可以啦,但是要記得再次調用invalidate()來通知View進行重繪。
(5)注意點:
因爲scrollTo/scrollBy的滑動過程是瞬間完成的,所以爲了用戶體驗,需要設置彈性滑動。
Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。
- Scroller scroller = new Scroller(mContext);
-
- // 緩慢滾動到指定位置:
- private void smoothScrollTo(int destX, int destY){
- int scrollX = getScrollX();
- int delta = destX - scrollX;
- // 1000ms內滑向destX,效果就是慢慢滑動
- mScroller.startScroll(scrollX, 0, delta, 0, 1000);
- invalidate();
- }
-
- @Override
- public void computeScroll(){
- if(mScroller.computeScrollOffest()){
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY();
- postInvalidate();
- }
- }
所以呢,結合上面的(1)(2)(3)(4),我們規整爲(5),就是說第一行scroller是必須創建的,下面的computeScroll方法是需要重寫的,中間的smoothScrollTo方法是我們自己寫的,裏面主要是要調用startScroll方法,隨後又調用了invalidate方法,這樣就可以循環往復的一直調用了:computeScroll()方法是不會自動調用的,只能通過invalidate()-> draw() -> computeScroll()來間接調用computeScroll()方法。
四、彈性滑動
三種方法:
(1)使用Scoller。
(2)通過動畫。
(3)使用延時策略
1、使用Scoller:
(1)剛剛已經寫過了下面的這兩個函數:這兩個方法寫在活動中:
- Scroller scroller = new Scroller(mContext);
-
- //緩慢滾動到指定位置:
- private void smoothScrollTo(int destX, int destY){
- int scrollX = getScrollX();
- int delta = destX - scrollX;
- // 1000ms內滑向destX,效果就是慢慢滑動
- mScroller.startScroll(scrollX, 0, delta, 0, 1000);
- invalidate();//通知View進行重繪
- }
-
- @Override
- public void computeScroll(){
- if(mScroller.computeScrollOffest()){
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- postInvalidate();
- }
- }
但其實現的重點在於startCroll方法和computeScrollOffest方法的實現。
(2)源碼地址:sources\android\widget\Scoller.java
(3)其實在startScroll中什麼都沒有做,它只是保存了我們傳遞的幾個參數:
- /**
- * Start scrolling by providing a starting point, the distance to travel,
- * and the duration of the scroll.
- *
- * @param startX Starting horizontal scroll offset in pixels. Positive
- * numbers will scroll the content to the left.
- * @param startY Starting vertical scroll offset in pixels. Positive numbers
- * will scroll the content up.
- * @param dx Horizontal distance to travel. Positive numbers will scroll the
- * content to the left.
- * @param dy Vertical distance to travel. Positive numbers will scroll the
- * content up.
- * @param duration Duration of the scroll in milliseconds.
- */
- public void startScroll(int startX, int startY, int dx, int dy, int duration) {
- mMode = SCROLL_MODE;
- mFinished = false;
- mDuration = duration;
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- // 滑動起點
- mStartX = startX;
- mStartY = startY;
- // 滑動終點
- mFinalX = startX + dx;
- mFinalY = startY + dy;
- mDeltaX = dx;
- mDeltaY = dy;
- // 滑動時間,內容的滑動而不是位置的滑動
- mDurationReciprocal = 1.0f / (float) mDuration;
- }
(4)真正的滑動實現是在startScroll下面的 invalidate()方法中。
invalidate方法會導致View重繪,View重繪會調用draw方法,在View的draw方法中又會去調用computeScroll方法,computeScroll方法在View中是一個空實現(它在View.java中),因此需要我們自己去重寫實現。
具體過程:當View重繪後會在draw方法中調用computeScroll方法,而computeScroll方法又會去向Scroller獲取當前的scrollX 和scrollY;然後通過scrollTo方法實現滑動;接着又調用postInvalidate方法來進行第二次重繪,這一次重繪和上一次重繪過程一樣的,還是會導致computeScroll方法被調用;然後繼續向Scroller獲取當前的scrollX和scrollY,並通過scrollTo方法滑動到新的位置,如此反覆,直到整個滑動過程結束。
- /**
- * Call this when you want to know the new location. If it returns true,
- * the animation is not yet finished.
- */
- /*
- * 這個方法會根據事件的流逝來計算出當前scrollX和scrollY的值。
- * 根據時間流逝的百分比來算出scrollX和scrollY改變的百分比並計算出當前的值
- * 它返回true表示滑動還沒有結束,false表示滑動已經結束。
- * */
- public boolean computeScrollOffset() {
- if (mFinished) {
- return false;
- }
-
- int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
-
- if (timePassed < mDuration) {
- switch (mMode) {
- case SCROLL_MODE:
- float x = timePassed * mDurationReciprocal;
-
- if (mInterpolator == null)
- x = viscousFluid(x);
- else
- x = mInterpolator.getInterpolation(x);
-
- mCurrX = mStartX + Math.round(x * mDeltaX);
- mCurrY = mStartY + Math.round(x * mDeltaY);
- break;
- case FLING_MODE:
- final float t = (float) timePassed / mDuration;
- final int index = (int) (NB_SAMPLES * t);
- float distanceCoef = 1.f;
- float velocityCoef = 0.f;
- if (index < NB_SAMPLES) {
- final float t_inf = (float) index / NB_SAMPLES;
- final float t_sup = (float) (index + 1) / NB_SAMPLES;
- final float d_inf = SPLINE_POSITION[index];
- final float d_sup = SPLINE_POSITION[index + 1];
- velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
- distanceCoef = d_inf + (t - t_inf) * velocityCoef;
- }
-
- mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
-
- mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
- // Pin to mMinX <= mCurrX <= mMaxX
- mCurrX = Math.min(mCurrX, mMaxX);
- mCurrX = Math.max(mCurrX, mMinX);
-
- mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
- // Pin to mMinY <= mCurrY <= mMaxY
- mCurrY = Math.min(mCurrY, mMaxY);
- mCurrY = Math.max(mCurrY, mMinY);
-
- if (mCurrX == mFinalX && mCurrY == mFinalY) {
- mFinished = true;
- }
-
- break;
- }
- }
- else {
- mCurrX = mFinalX;
- mCurrY = mFinalY;
- mFinished = true;
- }
- return true;
- }
是不是很神奇?切
2、通過動畫:
動畫本身就是一種漸近的過程,因此通過它來實現的滑動天然就具有彈性效果。
(1)下面的代碼可以讓一個View的內容在100ms內向左移動100像素:
- ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).satrt();
(2)我們可以利用動畫的特性來實現一些動畫不能實現的效果。我們可以在動畫的每一幀到來時獲取動畫完成的比例,然後再根據這個比例計算出當前View所要滑動的距離。
- final int startX = 0;
- final int deltaX = 100;
- ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
- animator.addUpdateListener(new AnimatorUpdateListener(){
- @Override
- public void onAnimationUpdate(ValueAnimator animator){
- float fraction = animator.getAnimatedFraction();
- mButton1.scrollTo(startX + (int)(deltaX * fraction), 0);
- }
- });
- animator.start();
在這裏例子中,我們並沒有在ofInt方法中直接對目標對象進行移動,而是在下面的scrollTo中進行實際的移動,這個思想和上面的Scroller滑動思想是相同的。
3、使用延時策略:
延時策略的工作機制:通過發送一系列延時消息從而達到一種漸近式的效果。
(1)兩種方式:Handler或View的postDelayed方法,或使用線程的sleep方法。
(2)其實還是要計算滑動過程中的百分比的。這裏有一個小例子:
- private static final int MESSAGE_SCROLL_TO = 1;
- private static final int FRAME_COUNT = 30;
- private static final int DELAYED_TIME = 33;
-
- private int mCount = 0;
-
- private Handler mHandler = new Handler(){
- public void handleMessage(Message msg){
- switch(msg.what){
- case MESSAGE_SCROLL_TO:
- mCount++;
- if(mCount <= FRAME_COUNT){
- float fraction = mCount / (float) FRAME_COUNT;
- int scrollX = (int) (fraction * 100);
- mButton.scrollTo(scrollX, 0);
- mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
- }
- break;
-
- }
-
- default:
- break;
- }
- };
- };
五、View的事件分發機制
點擊事件的事件分發,就是對MotionEvent事件的分發過程,即當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View。
1、點擊事件的分發過程由三個很重要的方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發,dispatch派分。如果事件能夠傳遞給當前View,這個方法就一定會被調用,
返回結果受當前View的 onTouchEvent 和下級 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法的內部調用,用來判斷是否攔截某個事件,如果當前View 攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,
返回結果表示是否攔截當前事件。true表示攔截。
public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法中調用,用來處理點擊事件,
返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View再也無法接收到該事件的後續事件。
就是說如果消耗了down,還可以消耗move,up等,如果不消耗,後續的就都沒有了。
但如果不消耗,該事件序列就要交由上一層來處理。
它們三者的關係用僞代碼表示的話就是這樣的:是不是很簡單的樣子?
- public boolean dispatchTouchEvent(MotionEvent ev) {
-
- boolean consume = false;
-
- if (onInterceptTouchEvent(ev)) {
- consume = onTouchEvent(ev);
- } else {
- consume = child.dispatchTouchEvent(ev);
- }
-
- return consume;
- }
2、OnTouchListener 和 OnTouchEvent 的關係
如果一個View並沒有給它設置OnTouchListener,也就不存在什麼問題了,就按上面的程序走。
但如果有設置了OnTouchListener,那麼它裏面的 onTouch 方法就會被調用。
這時候問題就來了,如果 onTouch 方法返回的是 false,則當前View的onTouchEvent 方法會被調用;
但如果onTouch 方法返回的是true,那麼當前View的 onTouchEvent 方法就不會被調用了!
所以說呢,給View設置的 OnTouchListener 的優先級要比 onTouchEvent 要高,
兩者只會有一個返回true,先詢問 OnTouchListener,不行再去看看 OnTouchEvent。
在onTouchEvent方法中,還可能設置有 OnClickListener,那麼它的 onClick 方法會被調用,
也就是說,只有 onTouchEvent被調用了,onClick 纔有調用的機會,所以說onClick的優先級是最低的。
3、事件傳遞順序
Activity -> Window -> View
雖然是從上往下的傳遞,但是當一個View 的 onTouchEvent 返回false(參考下面Tips中的(4)),那麼它的父容器的 onTouchEvent 將會被調用,如果還返回false就以此類推的往上推,直到Activity。
4、
5、事件分發的源碼解析
1、Activity對點擊事件的分發過程:
(1)源碼位置在:sources\android\app\Activity.java
(2)這裏是一個事件發生時最先到達的地方。
(3)Activity調用它的dispatchTouchEvent來進行事件派發。
(4)看第二個if語句,首先Activity將事件交給Windows,然後Windows會調用它的superDispatchTouchEvent,如果成功了,說明Activity下面的子處理了該事件,返回true,否則就會由下級傳回來來調用Activity最下面的那個onTouchEvent來處理事件。
- /**
- * Called to process touch screen events. You can override this to
- * intercept all touch screen events before they are dispatched to the
- * window. Be sure to call this implementation for touch screen events
- * that should be handled normally.
- *
- * @param ev The touch screen event.
- *
- * @return boolean Return true if this event was consumed.
- */
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- // 交給下級Window去處理
- if (getWindow().superDispatchTouchEvent(ev)) {
- // 下級處理成功,返回false。
- return true;
- }
- // 下級處理失敗,退回來自己在onTouchEvent中處理。
- return onTouchEvent(ev);
- }
2、Window對點擊事件的分發過程:
(1)源碼位置在:sources\android\view\Window.java
(2)由Activity傳遞到Window,然後Window要將事件傳遞給ViewGroup。
(3)Window是一個抽象類,其中的方法都是抽象方法,所以superDispatchTouchEvent 也是一個抽象方法。
- /**
- * Used by custom windows, such as Dialog, to pass the touch screen event
- * further down the view hierarchy. Application developers should
- * not need to implement or call this.
- *
- */
- public abstract boolean superDispatchTouchEvent(MotionEvent event);
(4)Window的唯一實現類是 PhoneWindow。唯一的!!!
- /**
- * Abstract base class for a top-level window look and behavior policy. An
- * instance of this class should be used as the top-level view added to the
- * window manager. It provides standard UI policies such as a background, title
- * area, default key processing, etc.
- *
- * <p>The only existing implementation of this abstract class is
- * android.policy.PhoneWindow, which you should instantiate when needing a
- * Window. Eventually that class will be refactored and a factory method
- * added for creating Window instances without knowing about a particular
- * implementation.
- */
(5)關於PhoneWindow,我並沒有找到它的源碼,呵呵。位置應該在:sources\android\policy\PhoneWindow.java 。看吧,PhoneWindow 又把事件分發的任務給了DecorView。
- public boolean superDispatchTouchEvent(MotionEvent event){
- return mDecor.superDispatchTouchEvent(event);
- }
3、DecorView對點擊事件的分發過程:
反正最後是從DecorView傳給了View了,過程先略。
4、頂級View對點擊事件的分發過程:
我這裏需要加一下我一直沒搞懂的陳述:
(1)就是在ViewGroup中的 dispatchTouchEvent 方法,其實所有的事件序列中的事件包括ACTION_DOWN、ACTION_MOVE和ACTION_UP都會進入到這裏來進行事件的分配。
(2)對於ACTION_DOWN,如果ViewGroup攔截了ACTION_DOWN,就會導致 mFirstTouchTarget == null,interception == true;這樣的話當其他的後續的事件到來時,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 語句無法進入,所以ViewGroup攔截了ACTION_DOWN以後,這個事件的後續只能由ViewGroup來處理。
(3)如果ACTION_DOWN是由子元素攔截的,那麼 mFirstTouchTarget != null,interception == false,這樣呢當其他後續的事件到來時,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 語句還是可以進入的。然後可以執行到onInterceptTouchEvent 方法,默認的ViewGroup的這個方法是都返回false的,也就是ViewGroup不攔截任何的事件,並且這也意味着,一旦某個子元素攔截了ACTION_DOWN,那後續的事件序列也都交給這個子元素來處理了。但是我們在寫自己的ViewGroup例如LinearLayout時,就可以重寫onInterceptTouchEvent方法,然後讓它可以在例如ACTION_MOVE的時候返回ture,這樣ViewGroup就可以實現沒有攔截ACTION_DOWN並交給了子元素,但是卻攔截了後面的ACTION_MOVE,這也就是下面在滑動衝突中講到的外部攔截法。
是不是很神奇呀!
頂級View一般是一個ViewGroup,所以我們去看ViewGroup。
(1)源碼位置在:sources\android\view\ViewGroup.java
(2)下面我們將一直在ViewGroup的超長dispatchTouchEvent函數中講解!
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
- }
-
- boolean handled = false;
- if (onFilterTouchEventForSecurity(ev)) {
- final int action = ev.getAction();
- final int actionMasked = action & MotionEvent.ACTION_MASK;
-
-
- /*
- * 當新的一輪點擊到來的時候,從ACTION_DOWN開始的,做一些初始化的工作:
- * */
- // Handle an initial down.
- if (actionMasked == MotionEvent.ACTION_DOWN) {
- // Throw away all previous state when starting a new touch gesture.
- // The framework may have dropped the up or cancel event for the previous gesture
- // due to an app switch, ANR, or some other state change.
- /*
- * 至少我知道在這個函數中最終將mFirstTouchTarget設爲null。
- * mFirstTouchTarget代表的就是一個事件序列中第一個攔截的對象,
- * 所以這裏需要重置。
- * */
- cancelAndClearTouchTargets(ev);
- /*
- * 如果事件是ACTION_DOWN,
- * ViewGroup就會在resetTouchState中重置下面的FLAG_DISALLOW_INTERCEPT標誌位。
- * 重置的方式是這樣的:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- * */
- resetTouchState();
- }
-
-
- // Check for interception.
- /*
- * 這個標識很重要,因爲它一旦被標誌位true,意味着下面的各種if語句都進不去了,
- * 意味着本ViewGroup攔截了該事件,並且後續的事件序列直接由該ViewGroup處理,
- * 而不是進入各種if中判斷是否需要攔截。
- * */
- final boolean intercepted;// 攔截標識
- /*
- * 這個if中需要滿足兩個條件:
- * (1)actionMasked == MotionEvent.ACTION_DOWN:
- * 該事件是否爲點擊下按事件時成立,就是說新的一輪事件到來
- * (2)mFirstTouchTarget != null:
- * 當ViewGroup不攔截事件並將事件交給子元素處理時,成立,mFirstTouchTarget指向這個子元素。
- * 而且在ViewGroup中,默認onInterceptTouchEvent返回false,它是不攔截任何事件的,
- * 但是在LinearLayout中可能就會攔截啊,可以改寫啊。
- * 而且,當第二個條件成立時,此時發生的事件序列就是ACTION_MOVE或者ACTION_UP,都會進入到這個if語句中。
- * */
- /*
- * 所以說呢,當子元素成功攔截了事件或者下按事件發生的時候就會進入if語句。
- * 所以說呢,如果子元素沒有處理,並且是move和up發生的時候就無法進入該if語句。
- * 但爲什麼這樣設定呢,因爲如果子元素沒有處理的話,事件序列中的其他事件就會直接由ViewGroup來處理了,
- * 不需要來這裏來判斷一下到底要不要攔截事件了。那如果是move和up也是同樣的,不需要來這裏來判斷要不要攔截事件。
- * */
- /*
- * 也就相當於說,一個事件,第一次因爲ACTION_DOWN進入這裏,然後ViewGroup判斷是否來攔截。
- * 之後在子元素成功處理後,因爲子元素是可以通過FLAG_DISALLOW_INTERCEPT標誌位來干預父元素的事件分發過程,所以又來這裏來要看是否攔截。
- * */
- /*
- * 爲什麼總說一旦父元素攔截ACTION_DOWN以後其他的事件序列就只能由父元素來處理呢?
- * 是因爲如果父元素攔截了ACTION_DOWN,那麼mFirstTouchTarget == null
- * 當ACTION_MOVE和ACTION_UP到來的時候,這條if語句就不會進入了,
- * 然後intercepted = true;表示事件序列由父元素全攔截了。
- * */
- if (actionMasked == MotionEvent.ACTION_DOWN
- || mFirstTouchTarget != null) {
- /*
- * 通常事件傳遞過程是由外向內的,
- * 但是通過 requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,
- * 不過ACTION_DOWN事件除外。
- * 干預表現在子元素已經攔截了事件,
- * 但是可以通過requestDisallowInterceptTouchEvent來控制
- * ACTION_MOVE和ACTION_UP能不能夠進入到這裏來。
- * */
- /*
- * FLAG_DISALLOW_INTERCEPT一旦設置後,ViewGroup將無法攔截處理ACTION_DOWN以外的其他點擊事件了。
- * 因爲在事件分發時,ACTION_DOWN會重置FLAG_DISALLOW_INTERCEPT標誌位,表示另一次事件開始。
- * */
- /*
- * 子View干涉ViewGroup的過程:
- * 初始化:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- * 在子View中FLAG_DISALLOW_INTERCEPT被重置,也就是要去幹擾,
- * 然後mGroupFlags & FLAG_DISALLOW_INTERCEPT爲1
- * 然後(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 爲true
- * 然後disallowIntercept爲true
- * 然後導致if (!disallowIntercept)無法進入。
- * */
- /*
- * FLAG_DISALLOW_INTERCEPT標誌位有什麼用呢?
- * 當面對滑動衝突時,我們可以考慮用這種方法去解決問題。
- * */
- final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
- if (!disallowIntercept) {
- /*
- * 所以說onInterceptTouchEvent並不是每次事件都會被調用的。
- * 而dispatchTouchEvent卻會在每次都調用。
- * 對於原始的ViewGroup,onInterceptTouchEvent會返回false,
- * 但是對於你自己寫的LinearLayout,則可以修改這個函數,
- * 讓它對ACTION_DOWN、ACTION_MOVE、ACTION_UP做出不同的選擇。
- * */
- intercepted = onInterceptTouchEvent(ev);
- ev.setAction(action); // restore action in case it was changed
- } else {
- intercepted = false;
- }
- } else {
- // There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches.
- /*
- * 就是說沒有子元素mFirstTouchTarget,而且事件也不是ACTION_DOWN,
- * 沒人管那就只能自己攔截了。
- * */
- intercepted = true;
- }
-
-
- // Check for cancelation.
- final boolean canceled = resetCancelNextUpFlag(this)
- || actionMasked == MotionEvent.ACTION_CANCEL;
-
-
- // Update list of touch targets for pointer down, if needed.
- final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
- TouchTarget newTouchTarget = null;
- boolean alreadyDispatchedToNewTouchTarget = false;
-
- /*
- * 當ViewGroup不攔截事件的時候,intercepted=false,事件會向下分發由它的子View進行處理
- * 所以說一旦ViewGroup攔截了事件,intercepted=true,
- * 意味着事件序列中的任何事件都不再會傳給子元素了,由父元素全權處理。
- * 所以intercepted=true一定要謹慎設置。
- * */
- if (!canceled && !intercepted) {
- if (actionMasked == MotionEvent.ACTION_DOWN
- || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
- || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
- final int actionIndex = ev.getActionIndex(); // always 0 for down
- final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
- : TouchTarget.ALL_POINTER_IDS;
-
-
- // Clean up earlier touch targets for this pointer id in case they
- // have become out of sync.
- removePointersFromTouchTargets(idBitsToAssign);
- final int childrenCount = mChildrenCount;
- if (newTouchTarget == null && childrenCount != 0) {
- final float x = ev.getX(actionIndex);
- final float y = ev.getY(actionIndex);
- // Find a child that can receive the event.
- // Scan children from front to back.
- final View[] children = mChildren;
-
-
- final boolean customOrder = isChildrenDrawingOrderEnabled();
-
- /*
- * 遍歷ViewGroup的所有子元素,判斷子元素是否能夠接收到點擊事件。
- * */
- for (int i = childrenCount - 1; i >= 0; i--) {
- final int childIndex = customOrder ?
- getChildDrawingOrder(childrenCount, i) : i;
- final View child = children[childIndex];
- /*
- * 判斷子元素是否能夠接收到點擊事件:
- * (1)canViewReceivePointerEvents:子元素是否在播動畫。
- * (2)isTransformedTouchPointInView:點擊事件的座標是否落在子元素的區域內。
- * */
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- continue;
- }
-
-
- /*
- * 如果上面那個if語句沒有成立,說明這個子元素是可以攔截事件的,
- * 所以新的TouchTarget出現了,就是這個子元素。
- * */
- newTouchTarget = getTouchTarget(child);
- if (newTouchTarget != null) {
- // Child is already receiving touch within its bounds.
- // Give it the new pointer in addition to the ones it is handling.
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- break;
- }
-
-
- resetCancelNextUpFlag(child);
- /*
- * 這個子元素已經攔截該事件了,現在要子元素傳遞給它自己的子元素去分派這個事件了:
- * dispatchTransformedTouchEvent實際上調用的就是子元素的dispatchTouchEvent方法。
- * 下面的第三個參數中child一定不爲null,所以child的dispatchTouchEvent一定會被調用。
- * 子元素的dispatchTouchEvent返回true,
- * 意味着dispatchTransformedTouchEvent也返回ture,
- * 表示事件被子元素分發成功,並break跳出循環。
- * */
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
- // Child wants to receive touch within its bounds.
- mLastTouchDownTime = ev.getDownTime();
- mLastTouchDownIndex = childIndex;
- mLastTouchDownX = ev.getX();
- mLastTouchDownY = ev.getY();
- /*
- * 分發成功後,在addTouchTarget會對mFirstTouchTarget進行賦值
- * */
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
- alreadyDispatchedToNewTouchTarget = true;
- /*
- * 分發成功,跳出循環
- * */
- break;
- }
- }
- }
-
-
- if (newTouchTarget == null && mFirstTouchTarget != null) {
- // Did not find a child to receive the event.
- // Assign the pointer to the least recently added target.
- newTouchTarget = mFirstTouchTarget;
- while (newTouchTarget.next != null) {
- newTouchTarget = newTouchTarget.next;
- }
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- }
- }
- }
-
-
- /*
- * 有兩種情況遍歷所有的子元素後事件也沒有處理:
- * (1)ViewGroup根本沒有子元素
- * (2)子元素的dispatchTouchEvent都返回了false。
- * 這種情況下只能ViewGroup自己來處理事件了。
- * */
- // Dispatch to touch targets.
- if (mFirstTouchTarget == null) {
- // No touch targets so treat this as an ordinary view.
- /*
- * 注意第三個參數:null,在上面變量子元素的時候這裏放的是child。
- * 如果是null,dispatchTransformedTouchEvent內部就會調用:
- * super.dispatchTouchEvent(event);
- * 很顯然,這裏就轉到了View的dispatchTouchEvent(event)方法,即點擊事件開始交由View來處理。在View中有onTouchEvent。
- * 其實父元素ViewGroup的onTouchEvent就是指的是View中的onTouchEvent方法,它自己這裏是沒有的。因爲ViewGroup是繼承View的!!!!
- * */
- handled = dispatchTransformedTouchEvent(ev, canceled, null,
- TouchTarget.ALL_POINTER_IDS);
- } else {
- // Dispatch to touch targets, excluding the new touch target if we already
- // dispatched to it. Cancel touch targets if necessary.
- TouchTarget predecessor = null;
- TouchTarget target = mFirstTouchTarget;
- while (target != null) {
- final TouchTarget next = target.next;
- if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
- handled = true;
- } else {
- final boolean cancelChild = resetCancelNextUpFlag(target.child)
- || intercepted;
- if (dispatchTransformedTouchEvent(ev, cancelChild,
- target.child, target.pointerIdBits)) {
- handled = true;
- }
- if (cancelChild) {
- if (predecessor == null) {
- mFirstTouchTarget = next;
- } else {
- predecessor.next = next;
- }
- target.recycle();
- target = next;
- continue;
- }
- }
- predecessor = target;
- target = next;
- }
- }
-
-
- // Update list of touch targets for pointer up or cancel, if needed.
- if (canceled
- || actionMasked == MotionEvent.ACTION_UP
- || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
- resetTouchState();
- } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
- final int actionIndex = ev.getActionIndex();
- final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
- removePointersFromTouchTargets(idBitsToRemove);
- }
- }
-
-
- if (!handled && mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
- }
- return handled;
- }
5、View對點擊事件的處理過程
(1)源碼位置在:sources\android\view\View.java
(2)在上面的頂級View中,如果頂級View沒有處理事件,而頂級View的子元素也沒有人處理這個事件,那就會到這裏來由View來處理事件。
(3)View對點擊事件的處理過程稍微簡單一些。
(4)注意,這裏的View不包含ViewGroup。只是簡單的單個View的處理,因爲他沒有子元素因此不能向下傳遞事件,所以它只能自己處理事件。
(5)下面對View源碼中的dispatchTouchEvent進行分析:
- /**
- * Pass the touch screen motion event down to the target view, or this
- * view if it is the target.
- *
- * @param event The motion event to be dispatched.
- * @return True if the event was handled by the view, false otherwise.
- */
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onTouchEvent(event, 0);
- }
-
- if (onFilterTouchEventForSecurity(event)) {
- //noinspection SimplifiableIfStatement
- /*
- * 首先會判斷有沒有設置OnTouchListener。
- * 如果OnTouchListener中的onTouch方法返回true,那麼onTouchEvent方法就不會調用,
- * 這樣做的好處是方便外界處理點擊事件。
- * */
- ListenerInfo li = mListenerInfo;
- if (li != null
- && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- return true;
- }
-
- /*
- * 優先級低於OnTouchListener
- * */
- if (onTouchEvent(event)) {
- return true;
- }
- }
-
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
- }
- return false;
- }
(6)下面對View源碼中的onTouchEvent方法進行分析:
- /**
- * Implement this method to handle touch screen motion events.
- * <p>
- * If this method is used to detect click actions, it is recommended that
- * the actions be performed by implementing and calling
- * {@link #performClick()}. This will ensure consistent system behavior,
- * including:
- * <ul>
- * <li>obeying click sound preferences
- * <li>dispatching OnClickListener calls
- * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
- * accessibility features are enabled
- * </ul>
- *
- * @param event The motion event.
- * @return True if the event was handled, false otherwise.
- */
- public boolean onTouchEvent(MotionEvent event) {
- final int viewFlags = mViewFlags;
-
- /*
- * 當View處於不可用狀態下時,View照樣會消耗點擊事,
- * 但它並不對事件做出任何的反映
- * */
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
- if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
- setPressed(false);
- }
-
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
- }
-
- /*
- * 如果View設置有代理,那麼還會執行mTouchDelegate的onTouchEvent方法,
- * 這個onTouchEvent的工作機制看起來和OnTouchListener類似,這裏我們不做研究
- * */
- if (mTouchDelegate != null) {
- if (mTouchDelegate.onTouchEvent(event)) {
- return true;
- }
- }
-
- /*
- * 這裏是對點擊事件的具體處理。
- * 可以發現的是View的CLICKABLE和LONG_CLICKABLE只要有一個爲true,
- * 那麼這個View就消耗這個事件,即onTouchEvent返回ture,不管他是不是DISABLE狀態。
- * 這個證明了前面(8)(9)(10)的結論。
- * */
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
- switch (event.getAction()) {
- /*
- * 當up事件發生時,就會觸發performClick()方法。
- * */
- case MotionEvent.ACTION_UP:
- boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
- if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
- // take focus if we don't have it already and we should in
- // touch mode.
- boolean focusTaken = false;
- if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
- focusTaken = requestFocus();
- }
-
- if (prepressed) {
- // The button is being released before we actually
- // showed it as pressed. Make it show the pressed
- // state now (before scheduling the click) to ensure
- // the user sees it.
- setPressed(true);
- }
-
- if (!mHasPerformedLongPress) {
- // This is a tap, so remove the longpress check
- removeLongPressCallback();
-
- // Only perform take click actions if we were in the pressed state
- if (!focusTaken) {
- // Use a Runnable and post this rather than calling
- // performClick directly. This lets other visual state
- // of the view update before click actions start.
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- if (!post(mPerformClick)) {
- /*
- * 如果View設置了OnClickListener,
- * 那麼performClick()方法內部會調用它的onClick方法
- * */
- performClick();
- }
- }
- }
-
- if (mUnsetPressedState == null) {
- mUnsetPressedState = new UnsetPressedState();
- }
-
- if (prepressed) {
- postDelayed(mUnsetPressedState,
- ViewConfiguration.getPressedStateDuration());
- } else if (!post(mUnsetPressedState)) {
- // If the post failed, unpress right now
- mUnsetPressedState.run();
- }
- removeTapCallback();
- }
- break;
-
- case MotionEvent.ACTION_DOWN:
- mHasPerformedLongPress = false;
-
- if (performButtonActionOnTouchDown(event)) {
- break;
- }
-
- // Walk up the hierarchy to determine if we're inside a scrolling container.
- boolean isInScrollingContainer = isInScrollingContainer();
-
- // For views inside a scrolling container, delay the pressed feedback for
- // a short period in case this is a scroll.
- if (isInScrollingContainer) {
- mPrivateFlags |= PFLAG_PREPRESSED;
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- } else {
- // Not inside a scrolling container, so show the feedback right away
- setPressed(true);
- checkForLongClick(0);
- }
- break;
-
- case MotionEvent.ACTION_CANCEL:
- setPressed(false);
- removeTapCallback();
- removeLongPressCallback();
- break;
-
- case MotionEvent.ACTION_MOVE:
- final int x = (int) event.getX();
- final int y = (int) event.getY();
-
- // Be lenient about moving outside of buttons
- if (!pointInView(x, y, mTouchSlop)) {
- // Outside button
- removeTapCallback();
- if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
- // Remove any future long press/tap checks
- removeLongPressCallback();
-
- setPressed(false);
- }
- }
- break;
- }
- return true;
- }
-
- return false;
- }
六、View的滑動衝突
1、常見的滑動衝突場景:
(1)外部滑動方向和內部滑動方向不一致。
(2)外部滑動方向和內部滑動方向一致。
(3)上面兩種情況的嵌套。
2、滑動衝突的處理規則:
具體來說:就是根據滑動是水平滑動還是豎直滑動來判斷到底由誰來攔截事件。
也就是說可以根據滑動過程中兩個點之間的座標就可以得出到底是水平滑動還是豎直滑動。
對於場景(1)如下解決辦法:(一左一右,或者一上一下)
(1)可以根據滑動路徑和水平方向的夾角。
(2)可以根據水平方向和豎直方向的距離差。
(3)可以根據水平方向和豎直方向的速度差。
對於場景(2)如下解決辦法:(同上同下,或者同左同右)
一般需要在業務上尋找突破點。
比如業務上有規定:當初與某種狀態時需要外部View相應用戶的滑動,而處於另一種狀態時需要內部View來響應View的滑動。
對於場景(3)也只能從業務上尋找突破點。
3、滑動衝突的解決方式:
(1)外部攔截法:
所謂外部攔截法就是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突的問題,這種方法比較符合點擊事件的分發機制。
外部攔截法需要重寫父容器的 onInterceptTouchEvent 方法,在內部做出相應的攔截即可。
下面給出僞代碼:
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
-
- boolean intercepted = false;
-
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- int action = event.getAction();
- switch (action) {
-
- case MotionEvent.ACTION_DOWN:
- // 必須爲fasle,不然父類容器攔截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
- }
(2)內部攔截法:
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素。
如果子元素需要此事件就消耗,否則就交給父容器進行處理。
這種方法和Android中的事件分發機制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起來比外部攔截法要稍微複雜。
下面提供僞代碼,主要是重寫了子元素的 dispatchTouchEvent 方法:
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
-
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- switch (event.getAction()) {
-
- case MotionEvent.ACTION_DOWN: {
- // 這裏的意思是設置讓父容器無法攔截ACTION_DOWN事件:
- parent.requestDisallowInterceptTouchEvent(true);
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- int deltaX = x - mLastX;
- int deltaY = y - mLastY;
- if(父容器需要此類點擊事件){
- // 如果需要的話就讓父類容器可以接收ACTION_MOVE事件。
- parent.requestDisallowInterceptTouchEvent(false);
- }
- break;
- }
- case MotionEvent.ACTION_UP: {
-
- break;
- }
- default:
- break;
- }
-
- mLastX = x;
- mLastY = y;
- return true;
- }
當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動而且也不同有改動。
除了子元素需要做處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,
(這裏ACTION_DOWN不能讓父元素默認攔截,因爲一旦父元素攔截,剩下的指令序列就都由父元素來處理了)
這樣當子元素調用 parent.requestDisallowInterceptTouchEvent(false); 方法時,父元素才能繼續攔截所需的事件。
父元素去哪裏改呢,當然是 onInterceptTouchEvent 方法啦。
- public boolean onInterceptTouchEvent(MotionEvent event) {
- int action = event.getAction();
- if(action == MotionEvent.ACTION_DOWN) {
- return false;
- } else {
- return true;
- }
- }
我們去源碼中看一下ViewGroup中的 requestDisallowInterceptTouchEvent 這個方法吧:是通過操控disallowIntercept 來達到目的的。
- /**
- * {@inheritDoc}
- */
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-
- if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
- // We're already in this state, assume our ancestors are too
- return;
- }
-
- if (disallowIntercept) {
- mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
- } else {
- mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- }
-
- // Pass it up to our parent
- if (mParent != null) {
- mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
- }
- }
(3)舉例:在一個水平佈局的LinearLayout中添加三個並列的ListView,所以父容器左右移動,子容器上下移動,造成衝突。
首先看一下Activity中的初始化代碼:
- package com.ryg.chapter_3;
-
- import java.util.ArrayList;
- import com.ryg.chapter_3.R;
- import com.ryg.chapter_3.ui.HorizontalScrollViewEx;
- import com.ryg.chapter_3.utils.MyUtils;
-
- import android.app.Activity;
- import android.graphics.Color;
- import android.os.Bundle;
- import android.util.Log;
- import android.view.GestureDetector;
- import android.view.LayoutInflater;
- import android.view.VelocityTracker;
- import android.view.View;
- import android.view.ViewConfiguration;
- import android.view.ViewGroup;
- import android.widget.AdapterView;
- import android.widget.ArrayAdapter;
- import android.widget.LinearLayout;
- import android.widget.ListView;
- import android.widget.TextView;
- import android.widget.Toast;
- import android.widget.AdapterView.OnItemClickListener;
-
- public class DemoActivity_1 extends Activity {
- private static final String TAG = "DemoActivity_1";
-
- /*
- * HorizontalScrollViewEx mListContainer是父容器,是自定義View。
- * 下面的代碼中向父容器中添加了三個ListView。
- * */
- 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++) {
- /*
- * 這個layout就是一個TextView和ListView的組合佈局,它的父容器是mListContainer
- * */
- 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));
- /*
- * 爲layout中的TextView和ListView添加內容:
- * */
- createList(layout);
- mListContainer.addView(layout);
- }
- }
-
- /*
- * 爲layout中的TextView和ListView添加內容,
- * ListView中的每個Item又是一個TextView文本。
- * */
- 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);
- listView.setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view,
- int position, long id) {
- Toast.makeText(DemoActivity_1.this, "click item",
- Toast.LENGTH_SHORT).show();
-
- }
- });
- }
-
- }
下面採用外部攔截法來解決滑動衝突問題:
我們只需要修改父容器需要攔截事件的條件即可。對於本例來說,父容器的攔截條件就是滑動過程中水平距離差比豎直距離差大,在這種情況下,父容器就攔截當前點擊事件,根據這一條件進行相應修改(正常情況下父容器的onInterceptTouchEvent都是默認返回false不攔截的),修改後的HorizontalScrollViewEx父容器的onInterceptTouchEvent方法如下所示:
- @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: {
- /*
- * 這裏必須是false,不然父容器攔截了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: {
- /*
- * 這裏也必須是false,不然如果是子容器攔截了上面的事件,子容器將接收不到up事件,就無法結束了
- * */
- intercepted = false;
- break;
- }
- default:
- break;
- }
-
- Log.d(TAG, "intercepted=" + intercepted);
- mLastX = x;
- mLastY = y;
- mLastXIntercept = x;
- mLastYIntercept = y;
-
- return intercepted;
- }
下面給出HorizontalScrollViewEx的完整代碼:
- package com.ryg.chapter_3.ui;
-
-
- import android.content.Context;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.view.VelocityTracker;
- import android.view.View;
- import android.view.ViewGroup;
- import android.widget.Scroller;
-
-
- 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;
-
-
- public HorizontalScrollViewEx(Context context) {
- super(context);
- init();
- }
-
-
- public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
-
- public HorizontalScrollViewEx(Context context, AttributeSet attrs,
- int defStyle) {
- super(context, attrs, defStyle);
- init();
- }
-
-
- 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: {
- <span style="white-space:pre"> </span>/*
- <span style="white-space:pre"> </span> * 這裏必須是false,不然父容器攔截了ACTION_DOWN以後,子容器再也接收不到任何事件了
- <span style="white-space:pre"> </span> * */
- 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: {
- <span style="white-space:pre"> </span>/*
- <span style="white-space:pre"> </span> * 這裏也必須是false,不然如果是子容器攔截了上面的事件,子容器將接收不到up事件,就無法結束了
- <span style="white-space:pre"> </span> * */
- 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.abortAnimation();
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- int deltaX = x - mLastX;
- int deltaY = y - mLastY;
- scrollBy(-deltaX, 0);
- break;
- }
- /*
- * 要根據移動的距離來判斷當前顯示哪個ListView。
- * */
- 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;
- }
-
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int measuredWidth = 0;
- int measuredHeight = 0;
- final int childCount = getChildCount();
- measureChildren(widthMeasureSpec, heightMeasureSpec);
-
-
- int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
- int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
- int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
- if (childCount == 0) {
- setMeasuredDimension(0, 0);
- } else if (heightSpecMode == MeasureSpec.AT_MOST) {
- final View childView = getChildAt(0);
- measuredHeight = childView.getMeasuredHeight();
- setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
- } else if (widthSpecMode == MeasureSpec.AT_MOST) {
- final View childView = getChildAt(0);
- measuredWidth = childView.getMeasuredWidth() * childCount;
- setMeasuredDimension(measuredWidth, heightSpaceSize);
- } else {
- final View childView = getChildAt(0);
- measuredWidth = childView.getMeasuredWidth() * childCount;
- measuredHeight = childView.getMeasuredHeight();
- setMeasuredDimension(measuredWidth, measuredHeight);
- }
- }
-
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int childLeft = 0;
- final int childCount = getChildCount();
- mChildrenSize = childCount;
-
-
- for (int i = 0; i < childCount; i++) {
- final View childView = getChildAt(i);
- if (childView.getVisibility() != View.GONE) {
- final int childWidth = childView.getMeasuredWidth();
- mChildWidth = childWidth;
- childView.layout(childLeft, 0, childLeft + childWidth,
- childView.getMeasuredHeight());
- childLeft += childWidth;
- }
- }
- }
-
-
- /*
- * 彈性滑動
- * */
- private void smoothScrollBy(int dx, int dy) {
- mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
- invalidate();
- }
-
-
- /*
- * 彈性滑動
- * */
- @Override
- public void computeScroll() {
- if (mScroller.computeScrollOffset()) {
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- postInvalidate();
- }
- }
-
-
- @Override
- protected void onDetachedFromWindow() {
- mVelocityTracker.recycle();
- super.onDetachedFromWindow();
- }
- }
下面採用內部攔截法來解決滑動衝突問題:
我們只需要修改ListView的dispatchTouchEvent方法中的父容器的攔截邏輯,同時讓父容器攔截ACTION_MOVE和ACTION_UP事件即可。爲了重寫ListView的dispatchTouchEvent方法,我們必須自定義一個ListView,稱爲ListViewEx,然後對內部攔截法的模板代碼進行修改:
- package com.ryg.chapter_3.ui;
-
- import android.content.Context;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.widget.ListView;
-
- public class ListViewEx extends ListView {
- private static final String TAG = "ListViewEx";
-
- private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;
-
- // 分別記錄上次滑動的座標:
- private int mLastX = 0;
- private int mLastY = 0;
-
- public ListViewEx(Context context) {
- super(context);
- }
-
- public ListViewEx(Context context, AttributeSet attrs) {
- super(context, attrs);
- }