Android View 的事件體系


android 系統雖然提供了很多基本的控件,如Button、TextView等,但是很多時候系統提供的view不能滿足我們的需求,此時就需要我們根據自己的需求進行自定義控件。這些控件都是繼承自View的。

一、android 控件架構

android 中的控件在界面上都會占一塊巨型區域,主要分為兩類:ViewGroup和View控件。ViewGroup作為父控件可以包含多個View控件,並管理他們,但其也是繼承自View。通過Viewgroup,整個控件界面形成了View的控件樹,如圖1所示。

上層控件負責字控件的測量與繪制,並傳遞交互事件。在Activity 中 通過findViewById()方法,就是以樹的 深度優先遍歷 查找對應的控件。控件樹的頂部都有一個ViewParent 對象,對整個視圖進行控制。

 

如上面圖2 所示,是android 界面的架構圖,在activity 中 通過 setContentView() 方法設置為根布局。在每一個Activity 中 都包含一個Window 對象,它由 PhoneWindow來實現,PhoneWindow 講一個 DecorView 設置為整個應用窗口的根View。DecorView 作為窗口界面的頂層視圖,封裝了一些窗口操作的通用方法,所有View 的監聽事件,都通過WindowManagerService 進行接收,並通過Activity 對象來回調相應的onClickListener。對於DecorView,他由TitleView 和ContentView 組成,
前者承載的是一個Actionbar,后者承載的是一個FrameLayout

二、View 的基本知識

2.1、View 的位置參數

view 的位置由兩個頂點坐標來決定,主要是左上(left,top)和右下(right,bottom)坐標。他們是相對於View 的父容器來說的,是相對坐標。如下圖他們的關系:

 

1
2
width = right - left;
height = bottom - top;

在View 中都有相應的方法來獲取他們的值。從android 3.0開始,View增加了幾個參數:x,y,translationX和translationY,其中x、y表示View 左上角的坐標,而translationX和translationY是View左上角相對於父容器的偏移量。同樣的,他們也有相應的get/set方法,translationX和translationY的默認值均為0。

1
2
x = left = translationX;
y = right + translationY;

 

在View 的移動過程中top和left 表示原始左上角坐標,並不會改變。

2.2、MotionEvent和TouchSlop

(1)、MotionEvent
在手指觸摸屏幕后,會有一系列的事件,主要事件類型有:

1
2
3
4
5
ACTION_DOWN // 收支接觸
ACTION_MOVE // 手指在屏幕上移動
ACTION_UP //手指從屏幕松開
ACTION_CANCEL //取消
...

使用 MotionEvent 對象獲取的 點擊的 x 和 y ,使用 getX / getY 獲取的是相對於當前View左上角的x和y,而 getRawX / getRawY 獲取的是相對於手機屏幕左上角的坐標。

(2)、TouchSlop
TouchSlop 是系統所能識別的最小滑動距離,小於它則視未發生滑動,他和設備相關,在不同的設備上獲取的值不同。通過ViewConfiguration.get(getContext()).getScaledTouchSlop(); 獲取。

2.3、VelocityTracker、GestureDetector和Scroller

(1)、VelocityTracker
速度追蹤,用於追蹤滑動過程中的速度,包括水平和豎直速度。如下具體使用步驟:

1
2
3
4
5
6
7
8
9
//1.在 onTouchEvent 方法中追蹤當前事件的速度
VelocityTracker tracker = VelocityTracker.obtain();
tracker.addMovement(event);
//2.獲取當前速度
tracker.computeCurrentVelocity(1000);//計算速度
int xVelocity = (int) tracker.getXVelocity();
int yVelocity = (int) tracker.getYVelocity();
//3.在不需要的時候重置回收內存
tracker.recycle();

(2)、GestureDetector
手勢檢測,用於檢測單擊、滑動、長按、雙擊等手勢。
在使用時,首先要實現 GestureDetector.OnGestureListener接口,如果需要雙擊,則需實現 GestureDetector.OnDoubleTapListener 接口;

1
2
3
4
//設置監聽
mGestureDetector = new GestureDetector(this);
//避免長按后無法拖動,自己測試時發現不設置,長按后也可以拖動
mGestureDetector.setIsLongpressEnabled(false);

然后在 onTouchEvent 添加如下代碼:

1
2
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

GestureDetector 類中的 OnGestureListener 接口和 OnDoubleTapListener 接口相關實現方法說明:

方法名 描述 所屬接口
onDown 手指輕觸,一個ACTION_DOWN 觸發 OnGestureListener
onShowPress 手指輕觸,尚未松開或者拖動 OnGestureListener
onSingleTapUp 單擊:手指(輕觸后)松開,伴隨一個ACTION_UP觸發 OnGestureListener
onScroll 拖動:手指按下並拖動,一個ACTION_DOWN,多個ACTION_MOVE OnGestureListener
onLongPress 長按 OnGestureListener
onFling 快速滑動:按下快速滑動並松開 OnGestureListener
     
onSingleTapConfirmed 嚴格單擊:這個只可能是單擊,不會是雙擊中的一次單擊 OnDoubleTapListener
onDoubleTap 雙擊 :與onSingleTapConfirmed 不共存 OnDoubleTapListener
onDoubleTapEvent 表示發生了雙擊行為 OnDoubleTapListener

(3)、Scroller
彈性滑動,可是實現有過度效果的滑動,View 的 ScrollTo/ScrollBy 都是瞬間滑動完成的。

三、View 的滑動

實現View的滑動主要有如下三種方式:

  1. scrollTo /scrollBy :適合對view 的內容改變;
  2. 動畫: 主要用於沒有交互的View 和實現復雜的動畫效果;
  3. 改變布局參數:操作稍微復雜,適合有交互的View 。

3.1. 通過View 的 ScrollTo/ScrollBy 方法

View 源碼中的相關實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 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
*/
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);
}

其中 scrollBy 調用的是 scrollTo,它實現了使用當前位置的相對滑動,而scrollTo 是基於所傳參數的絕對滑動。在滑動過程中,mScrollX 的值等於 View 左邊緣 和 View 內容左邊緣在水平方向的距離,而 mScrollY 則是View 上邊緣和 View 內容上邊緣在豎直方向的距離。他們都是以像素單位。如果從 左往右/從上往下 滑動,mScrollX/mScrollY 為正。

scrollBy 和 scrollTo 只能改變 View 內容的位置而不能改變View 在布局中的位置。

如下滑動過程中,mScrollX/mScrollY 取值情況:

 

 

3.2. 使用動畫

通過動畫為View 添加平移效果,View 的 tanslationX 和 tanslationY 屬性,可以采用傳統的動畫和屬性動畫。
動畫不能真正的改變 View 的位置,只是移動的是他的影像,如果在新位置有點擊事件,則無效。但是在android 3.0以后屬性動畫解決了該問題。

3.3. 改變布局參數

通過改變View 的LayoutParams 使得 View 重新布局實現滑動。
這里以 把 Button 水平移動 100px 為例。可以改變 Button 的 marginLeft ,或者在其左邊放一個寬度為0 的view,當要平移時改變他的寬度,使其被擠到右邊(加入Button的父布局為LinearLayout),實現滑動。

如下是改變 LayoutParams的方式:

1
2
3
4
5
 ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)
mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.setLayoutParams(params); // 或者 mButton.requestLayout();

四、彈性滑動

為了避免 滑動的生硬,可以采用彈性滑動,提高用戶體驗。這里主要有 :Scroller、動畫、延時三種方式。

如下是 Scroller 的典型使用,主要是 invalidate方法起的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Scroller mScroller = new Scroller(context);

/**
* 滑動到指定位置
*
* @param destX X 滑動距離
* @param destY Y 滑動距離
*/
private void smoothScrollTo(int destX, int destY) {
//滑動起點X
int scrollX = getScrollX();
//滑動起點Y
int scrollY = getScrollY();
//1000 ms內慢慢滑向 (destX,destY)
mScroller.startScroll(scrollX, scrollY, destX, destY, 1000);
//重繪
invalidate();
}
/**
* 使View 不斷重繪
*/
@Override
public void computeScroll() {
/**
* computeScrollOffset 方法通過時間流逝百分比計算 scrollX和scrollY
* 返回true 表示滑動未結束
*/
if (mScroller.computeScrollOffset()) {
//滑動到當前位置,通過小幅度滑動實現彈性滑動
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//再次重繪
postInvalidate();
}
}

如下 Scroller 的滑動原理(相關方法的調用過程)

 

 

對於延時達到彈性滑動,主要是利用 了Handler 或者 View 的 postDelayed 方法,或者線程的 sleep方法。

五、View 的事件分發機制

在 view 中事件分發十分重要,了解他的原理,對我們理解View 和解決滑動沖突都十分重要。

  1. 所有的Touch事件都封裝到 MotionEvent 里面;
  2. 事件處理包括三種情況,分別為:傳遞—-dispatchTouchEvent()函數、攔截——onInterceptTouchEvent()函數、消費—-onTouchEvent()函數和OnTouchListener
  3. 事件類型分為 ACTION_DOWN, ACTION_UP, ACTION_MOVE , ACTION_POINTER_DOWN, ACTION_POINTER_UP , ACTION_CANCEL 等,每個事件都是以 ACTION_DOWN 開始 ACTION_UP 結束。

用下面偽代碼表示事件分發過程及其關系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//事件分發
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
//是否被攔截
if (onInterceptTouchEvent(event))
{
//被攔截,處理事件
consume = onTouchEvent(event);
} else {
//未被攔截,向下分發
consume = childView.dispatchTouchEvent(event);
}
return consume;
}

事件傳遞的基本流程

  • 事件都是從Activity.dispatchTouchEvent()開始傳遞;
  • 事件由父View傳遞給子View,ViewGroup可以通過onInterceptTouchEvent()方法對事件攔截,停止其向子view傳遞;
  • 如果事件從上往下傳遞過程中一直沒有被停止,且最底層子View沒有消費事件,事件會反向往上傳遞,這時父View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最后會到Activity的onTouchEvent()函數;
  • 如果View沒有對ACTION_DOWN進行消費,之后的其他事件不會傳遞過來,也就是說ACTION_DOWN必須返回true,之后的事件才會傳遞進來;
  • OnTouchListener優先於onTouchEvent()對事件進行消費。

六、View 的滑動沖突

滑動沖突的出現是由於內外兩個view都是可以滑動的,如 ScrollView 中嵌套 ListView 。常見的滑動沖突場景有:

  • 場景一:內外滑動方向不一致;
  • 場景二:內外滑動方向一致;
  • 場景三:上述兩種場景的嵌套。

對於場景一,可以根據水平豎直方向的滑動距離差判斷是哪種滑動,進行相應的攔截;場景二,可以通過自己的業務制定相應的處理規則,然后進行處理;場景三則結合前兩種進行。

有些滑動沖突是采用了不合理的布局導致,可以更換布局,而有些則必須通過自定義控件重寫攔截和分發事件處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM