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 |
width = right - left; |
在View 中都有相應的方法來獲取他們的值。從android 3.0開始,View增加了幾個參數:x,y,translationX和translationY,其中x、y表示View 左上角的坐標,而translationX和translationY是View左上角相對於父容器的偏移量。同樣的,他們也有相應的get/set方法,translationX和translationY的默認值均為0。
1 |
x = left = translationX; |
在View 的移動過程中top和left 表示原始左上角坐標,並不會改變。
2.2、MotionEvent和TouchSlop
(1)、MotionEvent
在手指觸摸屏幕后,會有一系列的事件,主要事件類型有:
1 |
ACTION_DOWN // 收支接觸 |
使用 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 |
//1.在 onTouchEvent 方法中追蹤當前事件的速度 |
(2)、GestureDetector
手勢檢測,用於檢測單擊、滑動、長按、雙擊等手勢。
在使用時,首先要實現 GestureDetector.OnGestureListener
接口,如果需要雙擊,則需實現 GestureDetector.OnDoubleTapListener
接口;
1 |
//設置監聽 |
然后在 onTouchEvent 添加如下代碼:
1 |
boolean consume = mGestureDetector.onTouchEvent(event); |
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的滑動主要有如下三種方式:
- scrollTo /scrollBy :適合對view 的內容改變;
- 動畫: 主要用於沒有交互的View 和實現復雜的動畫效果;
- 改變布局參數:操作稍微復雜,適合有交互的View 。
3.1. 通過View 的 ScrollTo/ScrollBy 方法
View 源碼中的相關實現:
1 |
/** |
其中 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 |
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) |
四、彈性滑動
為了避免 滑動的生硬,可以采用彈性滑動,提高用戶體驗。這里主要有 :Scroller、動畫、延時三種方式。
如下是 Scroller
的典型使用,主要是 invalidate方法起的作用。
1 |
Scroller mScroller = new Scroller(context); |
如下 Scroller 的滑動原理(相關方法的調用過程):

對於延時達到彈性滑動,主要是利用 了Handler 或者 View 的 postDelayed 方法,或者線程的 sleep方法。
五、View 的事件分發機制
在 view 中事件分發十分重要,了解他的原理,對我們理解View 和解決滑動沖突都十分重要。
- 所有的Touch事件都封裝到
MotionEvent
里面;- 事件處理包括三種情況,分別為:傳遞—-dispatchTouchEvent()函數、攔截——onInterceptTouchEvent()函數、消費—-onTouchEvent()函數和OnTouchListener;
- 事件類型分為 ACTION_DOWN, ACTION_UP, ACTION_MOVE , ACTION_POINTER_DOWN, ACTION_POINTER_UP , ACTION_CANCEL 等,每個事件都是以 ACTION_DOWN 開始 ACTION_UP 結束。
用下面偽代碼表示事件分發過程及其關系:
1 |
//事件分發 |
事件傳遞的基本流程:
- 事件都是從Activity.dispatchTouchEvent()開始傳遞;
- 事件由父View傳遞給子View,ViewGroup可以通過onInterceptTouchEvent()方法對事件攔截,停止其向子view傳遞;
- 如果事件從上往下傳遞過程中一直沒有被停止,且最底層子View沒有消費事件,事件會反向往上傳遞,這時父View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最后會到Activity的onTouchEvent()函數;
- 如果View沒有對ACTION_DOWN進行消費,之后的其他事件不會傳遞過來,也就是說ACTION_DOWN必須返回true,之后的事件才會傳遞進來;
- OnTouchListener優先於onTouchEvent()對事件進行消費。
六、View 的滑動沖突
滑動沖突的出現是由於內外兩個view都是可以滑動的,如 ScrollView 中嵌套 ListView 。常見的滑動沖突場景有:
- 場景一:內外滑動方向不一致;
- 場景二:內外滑動方向一致;
- 場景三:上述兩種場景的嵌套。
對於場景一,可以根據水平豎直方向的滑動距離差判斷是哪種滑動,進行相應的攔截;場景二,可以通過自己的業務制定相應的處理規則,然后進行處理;場景三則結合前兩種進行。
有些滑動沖突是采用了不合理的布局導致,可以更換布局,而有些則必須通過自定義控件重寫攔截和分發事件處理。