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,至於原因,上面已經提及過了。