View的事件體系小結

View的事件體系小結

一、view的基礎概念

(1)啥爲View?
View爲Android中所有控件的基類(控件:Button、TextView等),它是界面層控件的一種抽象,我們日常所用的View以及ViewGroup都是繼承於view。
View樹結構:已知View和ViewGroup都是繼承於View,ViewGroup裏面可以包含其他子View,這些子View又可以爲其他ViewGroup,以此類推可形成View樹的結構,這個結構有利於我們去理解View的事件分發機制。
(2)View的位置參數
View的位置主要由其四頂點決定
top:View的左上角縱座標、left:View的左上角橫座標、right:View的右下角橫座標、bottom:View的右下角縱座標。需注意這幾個座標都是相對於當前View的父View來說的。

Android中提供了對應的方法來獲取四個值:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
//view的寬度
width = Right - Left
//view的高度
height = Bottom - Top

這裏還有幾個值需要注意的值(其座標都是相對於當前view的父容器來說的)
x、y:分別爲view的Left和Top變化的後的座標值。
translationX、translationY:爲View左上角相對於父容器的偏移量。

x = Left + translationX
y = top + translationY

(3)MotionEvent(觸摸事件)
主要爲三種事件:
Action_Down:手指剛接觸屏幕
Action_Move:手指在屏幕上移動
Action_Up:手指離開屏幕瞬間

Android提供兩鍾方法來獲取點擊事件發生的座標:
getX/getY:返回相對於當前View左上角的x、y座標。
getRawX/getRawY:返回相對於手機屏幕左上角的x、y座標。(全屏滑動使用)

這裏還得提到另一個屬性:TouchSlop(系統所能識別的滑動的最小距離)
獲取這個常量的方法:

ViewConfiguration. get(getContext()).getScaledTouchSlop()。

這個常量定義在frameworks/base/core/res/res/values/config.xml文件中

<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>

(4)VelocityTracker、GestureDetector、Scroller(粗略介紹)
1、VelocityTracker:顧名思義速度追蹤器,用來獲得手機在屏幕上滑動的速度。
使用方法
(1)在View的onTouchEvent加上兩行代碼來記錄當前單擊事件的速度,至於onTouchEvent這個方法,後續將會討論到。

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

(2)獲取速度,通過以下api來實現

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在使用getXVelocity()和getYVelocity()獲取速度之前,需要調用velocityTracker.computeCurrentVelocity(1000);來計算速度。速度爲矢量,所以有方向,手指順着座標系正方向來移動,所得到的值爲正值。
(3)不使用它時,需要通過以下API來回收

velocityTracker.clear();
velocityTracker.recycle();

2、GestureDetector:手勢識別器,用於檢測用戶單擊、滑動、雙擊等等動作。
使用方法

//(1)新建手勢識別器對象
GestureDetector mGestureDetector = new GestureDetector(this);

//(2)接管目標View的onTouchEvent方法
boolean flag = mGestureDetector.onTouchEvent(event);
return flag;

下面列舉幾種常用到的方法:(1)onSingleTapUp(檢測單擊事件) (2)onDoubleTap(檢測雙擊時間) (3)onLongPress(檢測長按事件)。

3、Scroller:彈性滑動對象,滑動過程有滑動效果,增加用戶體驗。
前因:使用scrollTo、scrollBy進行滑動時,都是瞬間完成,體驗不佳。scrollTo、scrollBy這兩個方法,後面會有具體的解釋。
使用方法

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.computeScrollOffset()) {
	scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
	postInvalidate();
	}
}

二、view的滑動實現

view的滑動方法主要有三種:
(1)scrollTo/scrollBy
以下是上述兩個方法的實現代碼:

//scrollTo方法的實現
public void scrollTo(int x,int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;//mScrollX的值爲View左邊緣和View內容左邊緣水平方向的距離
        int oldY = mScrollY;//mScrolly的值爲View上邊緣和View內容上邊緣水平方向的距離
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX,mScrollY,oldX,oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}
//scrollBy方法的實現
public void scrollBy(int x,int y) {
    scrollTo(mScrollX + x,mScrollY + y);
}        

注意點
(1)scrollTo和scrollBy只能改變view的內容的位置,不能改變view在佈局中的位置。
(2)view的左邊緣在view內容左邊緣的右邊時,mScrollX爲正值,反之爲負值。
(3)view的上邊緣在view內容上邊緣的下邊時,mScrollY爲正值,反之爲負值。

(2)使用動畫
使用View動畫來操作view,主要就是操作View的translationX和translationY屬性(移動的還是View的內容),除非用屬性動畫,才能真正移動View。
View動畫的使用:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"  android:zAdjustment="normal" >
<translate android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100" />
</set>

View動畫是對View的影像做操作,想要保留動畫後的狀態,需要把fillAfter屬性設爲true。

屬性動畫的使用:

ObjectAnimator.ofFloat(View,"translationX",0,100).setDuration(100).start();

(3)通過佈局參數
例:

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;// 保持button原本的大小,不被擠壓變形。
params.leftMargin += 100;//左外邊距增加100px
mButton1.requestLayout();

三種方式對比:
1、scrollTo、scrollBy:對View內容的移動,操作簡單,適合無交互的View。
2、動畫:(1)View動畫:對View內容的移動,適合無交互的View,可以實現相對複雜的效果 。 (2)屬性動畫:操作View的屬性,適合有交互的View,可以實現複雜的效果。
3、改變佈局參數:適合有交互的View,但是操作比較複雜。

三、彈性滑動的實現

使用彈性活動的方法主要有三種:
(1)通過Scroller

Scroller scroller = new Scroller(mContext);//創建對象
// 緩慢滾動到指定位置
private void smoothScrollTo(int destX,int destY) {
    int scrollX = getScrollX();//mScrollX的值爲View左邊緣和View內容左邊緣水平方向的距離
    int deltaX = destX -scrollX;
   // 1000ms內滑向destX
    mScroller.startScroll(scrollX,0,deltaX,0,1000);//只是用來保存數據
    invalidate();
}

startScroll()方法的具體實現:(可以得知,只起到保存數據的作用)

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;
}

startScroll方法只是起到保存數據的作用。invalidate方法纔是真正實現View的彈性滑動,其原因是:invalidate會導致View的重繪,所以會調用View的draw方法,View的draw方法又會調用computeScroll()方法,接下來看computeScroll()的具體實現。

//這是一個空方法,需要自己來實現
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

由上述函數可發現,最後還是調用了scrollTo方法,來實現View的滑動,然後再調用postInvalidate()來實現重繪,並沒有看到彈性是在哪裏實現,所以我們把問題定位到mScroller.computeScrollOffset()上。接下來來看下computeScrollOffset()的一個實現。

public boolean computeScrollOffset() {
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() -mStartTime);//經過的時間
    if (timePassed < mDuration) {
        switch (mMode) {
            case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed *
            mDurationReciprocal);//插值器,根據時間流逝的百分比計算出動畫改變的百分比
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        }
    }
	return true;
}

通過上述函數可得知,在一段時間內,computeScrollOffset函數會根據時間的流逝計算出View當前移動到哪個位置,所以當前View不會出現瞬間移動到的情況,彈性滑動實現。

(2)通過動畫
前因:總所周知,動畫本來就是隨着時間流逝慢慢播放的,所以其本身以實現彈性滑動的效果

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

這裏的動畫只是一個粗略的講解,有興趣可以關注稍後寫的關於動畫的小結。

(3)通過延時操作
實現原理:通過不斷髮送延時消息來更新UI,從而實現View的彈性滑動。
實現方法:使用Handler或者View的postDelay方法
示例:通過Handler來實現

private static final int MESSAGE = 1;
private static final int COUNT = 50;
private int mCount = 0;
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
    switch (msg.what) {
        case MESSAGE: {
            mCount++;
            if (mCount <= COUNT) {
                float fraction = mCount / (float) COUNT;
                int scrollX = (int) (fraction * 100);
                mButton1.scrollTo(scrollX,0);
                mHandler.sendEmptyMessageDelayed(MESSAGE,
                50);
            }
            break;
        }
        default:
        break;
        }
    };
};

總結:其實以上三種方法的本質都是一樣的,要想實現彈性滑動,就不能讓View的滑動瞬間完成,通過給View設置一定的時間慢慢移動,彈性滑動效果實現。

四、View的事件分發機制

1、點擊事件的傳遞規則
首先要了解點擊事件,先要了解事件分發過程中的三個重要方法:
(1)public boolean dispatchTouchEvent(MotionEvent ev)
事件傳給當前View,則此方法一定會被調用,至於這個方法返回false或者返回true,由當前View的onTouchEvent和下級View(如果有下級View的話)的dispatchTouchEvent影響,表示是否消耗當前事件。
(2)public boolean onInterceptTouchEvent(MotionEvent event)(此方法存在於ViewGroup中)
此方法表示是否攔截某事件,如果攔截某事件,那麼在同一事件序列中(例如:down-move-move-up),此方法不會被再次調用,返回的結果表示是否攔截當前事件。
(3)public boolean onTouchEvent(MotionEvent event)
用來處理點擊事件,如果不消耗,在同一事件序列中,當前View無法再接收到事件。

優先級問題:onTouchListener>onTouchEvent>onClickListener

補充幾個概念
(1)事件序列:手指從接觸到屏幕,到離開屏幕,這個過程所產生的一系列事件。以down開始,以move結束。
(2)正常情況下,一旦某個View攔截了某事件,那麼這個事件序列都會交給這個View來處理,除非這個View又在onTouchevent把事件拋出。
(3)如果當前View不消耗除ACTION_DOWN以外的事件,此點擊事件會消失,父元素的onTouchEvent也不會被調用,當前View可以接收到後續事件,消失的點擊事件最後會交給Activity來處理。
(4)View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true。
(5)onClick會發生的前提是當前View是可點擊的,並且它收到了down和up的事件

2、深入解析事件分發機制
1、Activity對點擊事件的分發過程:當一個點擊事件發生時,事件是最先傳遞給當前Activity,接下來看看它的一個代碼實現
Activity的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent ev) {
	if (ev.getAction() == MotionEvent.ACTION_DOWN) {
		onUserInteraction();
	 }
	if (getWindow().superDispatchTouchEvent(ev)) {
		return true;
	}
	return onTouchEvent(ev);
}

流程
(1)點擊事件首先傳遞到Activity,然後Activity的dispatchTouchEvent方法被調用。
(2)在上述代碼中可看到做了一個這樣的判斷if (getWindow().superDispatchTouchEvent(ev)) ,這是把事件交給Activity所附屬的Window進行分發。
(3)來看看getWindow().superDispatchTouchEvent(ev)這個方法的一個實現,從代碼上可得知window是一個抽象類,而他的方法superDispatchTouchEvent也是抽象方法。所以要找到它們的具體實現,分析Android源碼可得知,Window在Android中的唯一實現類就是PhoneWindow。
(4)來到PhoneWindow中,找到superDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

從代碼中可得知,將事件交給mDecor來處理,這個mDecor就是DecorView,至於DecorView在我的另一篇博客《View的工作原理小結》有提到,這裏就不再詳解。
(5)現在事件傳遞到DecorView(DecorView本身爲一個ViewGroup)接下來的流程就是常規的事件分發流程。接下來附圖詳解這個流程:

2、FLAG_DISALLOW_INTERCEPT:這個標記位,能讓子View控制父ViewGroup無法攔截除ACTION_DOWN之外點擊事件,爲何除了ACTION_DOWN? 攔截ACTION_DOWN,會重置FLAG_DISALLOW_INTERCEPT這個標誌位,導致這個標誌位無效。所以要想使用這個標誌位阻止父ViewGroup攔截事件,需要讓父ViewGroup不攔截ACTION_DOWN。

//父ViewGroup在攔截ACTION_DOWN後所作的操作。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();//重置標誌位
}

子View通過調用

requestDisallInterceptRouchEvent(boolean disallowIntercept)

來改變這個標誌位。

3、view能否接受點擊事件有兩點來衡量:
(1)子元素是否在播放動畫。
(2)點擊事件的座標是否落在子元素的區域內。

4、View對點擊事件的處理過程(這裏指的是非ViewGroup)
View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
    ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this,event)) {
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

return result;
}

從上面函數可得知,首先會判斷有沒有OnTouchListener,如果onTouchListener中的onTouch方法返回true,那麼View的onTouchEvent就不會被調用。
接下來看看View的onTouchEvent方法

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
}

return (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

從上述可得知,就算View處於不可用的狀態,還是會消耗點擊事件。
接下來看看onTouchEvent中對點擊事件的處理

if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_UP:
		boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
		if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    ...
			if (!mHasPerformedLongPress) {

			removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
				if (!focusTaken) {

					if (mPerformClick == null) {
						mPerformClick = new PerformClick();
 				}
				if (!post(mPerformClick)) {
					 performClick();
					}
				}
		}
...
	}
		 break;
}
...
	return true;
}

首先只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管這個View是不是可用(DISABLE)。然後當ACTION_UP事件發生時,會觸發performClick方法,當View中有設置OnClickListener,performClick方法就會調用onClick方法。

五、View的滑動衝突

起因:我們的佈局經常是View嵌套View,不同的View又接收不同滑動事件,所以哪個滑動由哪個View來處理顯得至關重要。

1、常見的滑動衝突場景(三種)
(1)內外滑動方向不一致
(2)內外滑動方向一致
(3)第一第二兩種情況的混合
處理原則:具體場景,具體分析。判斷在具體情況下,應該由哪個View來處理這個事件。
處理滑動衝突的方法:
1、外部攔截法

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_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,如果ViewGroup攔截了,那麼接下來的整個事件序列都會交給它來處理,所以一般返回爲false,對於ACTION_UP一般ViewGroup都要返回false(不管攔不攔截事件),一但返回true會導致子元素中的onClick事件無法觸發。
2、內部攔截法
內部攔截法是指父容器不攔截任何事件,全部傳給子元素去處理,子元素需要就消耗掉,不然最後還是會傳遞給父容器處理。這種方法需要通過上文所說到的一個標誌位來幫忙實現:FLAG_DISALLOW_INTERCEPT,通過parent.requestDisallowInterceptTouchEvent(true);這個方法來控制父容器不攔截事件,僞代碼如下所示:

//子元素的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.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.dispatchTouchEvent(event);
}

除了子元素需要處理,還要記得父容器不能攔截ACTION_DOWN,至於原因,上面已經提及過了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章