文章目錄
1、簡介
首先,簡單介紹一下大家比較熟悉的Android View構成,如下圖所示。
一個App可以有多個Activity。Activity是Android的四大組件之一,四大組件ABCS即A-Activity,B-BroadcastReceiver,C-ContentProvider,S-Service。每個Activity都有一個Window,Window是個abstract類,目前的唯一實現類爲PhoneWindow,PhoneWindow是一種policy,從名字來看,可能還會有WatchWindow、TvWindow、CarWindow等。另外,還有一種特殊的Window,懸浮窗,這裏不作詳細介紹。PhoneWindow中的top-level View是DecorView,DecorView實際上是ViewGroup。DecorView包含DecorCaptureView和ContentRoot,ContentRoot便是各種父子關係的View樹。下圖是相關的class圖。
2、View與ViewGroup
本文主要介紹View及ViewGroup。View是個基本的UI控件,在屏幕上佔據一個矩形區域,負責描畫和處理事件。一些Widget如TextView、ImageView等是View的子類。ViewGroup也是View的子類,ViewGroup作爲View的容器本身不可見,常見的Layout如RelativeLayout、LinearLayout等便是ViewGroup的子類。下圖是相關的class圖。
3、UiThread
View和ViewGroup使用了UiThread註解,表示必須在UI線程即主線程中使用,包括構造函數以及其它所有的函數調用。相關代碼如下。
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {}
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {}
4、屬性
Android定義了一些屬性,可以在xml中設置,也可以在java中使用。以TextView Widget的text屬性爲例,如下例子在xml中將text設置爲“Hello World!”,text的xmlns爲andriod,然後在java中使用R.id獲取TextView對象後,通過getText()獲取text屬性,通過setText()設置text屬性爲“xxx”。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
package com.example.view;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends Activity {
private static final String TAG = "ViewTest";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.text_view);
String text = textView.getText().toString();
Log.d(TAG, "MainActivity onCreate xml text:" + text);
textView.setText("xxx");
text = textView.getText().toString();
Log.d(TAG, "MainActivity onCreate set text:" + text);
}
}
Log輸出如下。
2019-01-14 14:16:09.856 10278-10278/com.example.view D/ViewTest: MainActivity onCreate xml text:Hello World!
2019-01-14 14:16:09.857 10278-10278/com.example.view D/ViewTest: MainActivity onCreate set text:xxx
下面我們來分析一下xml與java是如何把TextView的text屬性聯繫在一起的。TextView在xml中可以設置哪些屬性,可以在源碼frameworks/base/core/res/res/values/attrs.xml中看到,格式固定,內容如下。
<declare-styleable name="TextView">
<!-- Text to display. -->
<attr name="text" format="string" localization="suggested" />
</declare-styleable>
在java中,通過getText()得知,text屬性值保存在一個類型爲CharSequence的mText成員變量中,代碼如下所示。
// Do not update following mText/mSpannable/mPrecomputed except for setTextInternal()
@ViewDebug.ExportedProperty(category = "text")
@UnsupportedAppUsage
private @Nullable CharSequence mText;
@ViewDebug.CapturedViewProperty
public CharSequence getText() {
return mText;
}
setText()有5個版本,如下所示。
@android.view.RemotableViewMethod
public final void setText(CharSequence text) {}
public void setText(CharSequence text, BufferType type) {}
public final void setText(char[] text, int start, int len) {}
@android.view.RemotableViewMethod
public final void setText(@StringRes int resid) {}
public final void setText(@StringRes int resid, BufferType type) {}
setText()最終都是通過setTextInternal()實現的,從中可以確定text屬性值確實是保存在了mText成員變量中,代碼如下所示。
// Update mText and mPrecomputed
private void setTextInternal(@Nullable CharSequence text) {
mText = text;
mSpannable = (text instanceof Spannable) ? (Spannable) text : null;
mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
}
下面來看一下TextView的構造函數中是如何解析xml中設置的text屬性的。在構造函數中,通過R.styleable.TextView獲取TypedArray類型的AttributeSet,然後通過R.styleable.TextView_text獲取text屬性值,默認爲空字符串,代碼如下所示。
public TextView(Context context) {
this(context, null);
}
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@SuppressWarnings("deprecation")
public TextView(
Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setTextInternal("");
final Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.TextView_text:
textIsSetFromXml = true;
mTextId = a.getResourceId(attr, ResourceId.ID_NULL);
text = a.getText(attr);
break;
}
}
a.recycle();
BufferType bufferType = BufferType.EDITABLE;
// ...
setText(text, bufferType);
}
下面是TextView的構造函數中解析text屬性的時序圖。
我們可以根據TextView的text屬性的用法,自定義屬性,format支持integer、string、boolean、float、fraction、enum、color、dimension、flags、reference十種格式。首先在res/values/attrs.xml中定義屬性,代碼如下所示。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="name" format="string" />
<attr name="number" format="integer" />
</declare-styleable>
</resources>
同樣在java中解析自定義的屬性,代碼如下所示。
package com.example.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class CustomView extends View {
private static final String TAG = "ViewTest";
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
String name = array.getString(R.styleable.CustomView_name);
int number = array.getInt(R.styleable.CustomView_number, 0);
array.recycle();
Log.d(TAG, "CustomView name:" + name);
Log.d(TAG, "CustomView number:" + number);
}
}
layout中對於CustomView和自定義屬性的用法如下代碼所示。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.view.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:name="Tom"
app:number="18" />
</android.support.constraint.ConstraintLayout>
Log輸出如下。
2019-01-14 14:16:09.844 10278-10278/com.example.view D/ViewTest: CustomView name:Tom
2019-01-14 14:16:09.844 10278-10278/com.example.view D/ViewTest: CustomView number:18
在java中獲取xml中的一個View對象,通過id獲取,id是一個View的身份,用於區分不同的View。除此之外,還有個tag屬性,tag用來記載View的額外信息,使用tag的好處是方便存取,而不用將這些信息放在獨立於View的其它結構中。
5、focus
focus表示UI交互的元素,可以是Activity、Window、View,擁有focus即表示當前元素可以與UI進行交互,如響應input事件。下圖先列出View和ViewGroup中與focus相關的函數,只是列出了函數名和返回值類型,沒有列出具體的參數。
從函數名大致可以明白這個函數的作用。其中,onWindowFocusChanged表示View所在Window的focus發生變化時會調用這個函數,onFocusChanged表示View的focus發生變化時會調用這個函數,還可以通過setOnFocusChangeListener設置View的focus變化的Listener以監聽focus變化。另外,isFocused表示View是否擁有focus,isFocusable表示View是否支持focus。
下面分析View中onWindowFocusChanged的源碼實現。onWindowFocusChanged是dispatchWindowFocusChanged調用的,在onWindowFocusChanged中,主要作了三件事情:一是在Window沒有focus時設置press狀態爲false,這個很容易理解,同時移除LongPress和Tap的Callback,因爲LongPress和Tap屬於Gesture,而Gesture依賴於press狀態;二是設置InputMethodManager對應的focus狀態,設置爲focusIn或focusOut;最後通過refreshDrawableState刷新View狀態。源碼如下所示。
public void dispatchWindowFocusChanged(boolean hasFocus) {
onWindowFocusChanged(hasFocus);
}
public void onWindowFocusChanged(boolean hasWindowFocus) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (!hasWindowFocus) {
if (isPressed()) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
imm.focusOut(this);
}
removeLongPressCallback();
removeTapCallback();
onFocusLost();
} else if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
imm.focusIn(this);
}
refreshDrawableState();
}
View中onFocusChanged的源碼實現與onWindowFocusChanged類似,不同的是添加了CallSuper註解,表示View的子類重寫onFocusChanged時需要調用父類即View本身的onFocusChanged另外還通過註冊的監聽focus的Listener通知focus變化。源碼片段如下所示。
@CallSuper
protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
@Nullable Rect previouslyFocusedRect) {
// ...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnFocusChangeListener != null) {
li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
}
// ...
}
6、Listener
View中提供了很多Listener,如上面提到的OnFocusChangeListener,用於監聽View變化,這些Listener保存在一個ListenerInfo的class中,如下代碼所示。
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
@UnsupportedAppUsage
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
protected OnScrollChangeListener mOnScrollChangeListener;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
@UnsupportedAppUsage
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
@UnsupportedAppUsage
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/
protected OnContextClickListener mOnContextClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
@UnsupportedAppUsage
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
@UnsupportedAppUsage
private OnKeyListener mOnKeyListener;
@UnsupportedAppUsage
private OnTouchListener mOnTouchListener;
@UnsupportedAppUsage
private OnHoverListener mOnHoverListener;
@UnsupportedAppUsage
private OnGenericMotionListener mOnGenericMotionListener;
@UnsupportedAppUsage
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
OnCapturedPointerListener mOnCapturedPointerListener;
private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners;
}
@UnsupportedAppUsage
ListenerInfo mListenerInfo;
這些Listener一般都有getter/setter函數,如下代碼中的OnFocusChangeListener。
@UnsupportedAppUsage
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
public OnFocusChangeListener getOnFocusChangeListener() {
ListenerInfo li = mListenerInfo;
return li != null ? li.mOnFocusChangeListener : null;
}
public void setOnFocusChangeListener(OnFocusChangeListener l) {
getListenerInfo().mOnFocusChangeListener = l;
}
Listener中的註解UnsupportedAppUsage表示不公開到SDK,通過SDK無法直接使用這些內容。
7、Visibility
View的Visibility有三種狀態:visible、invisible和gone。其中,visible表示可見,invisible表示不可見,gone也表示不可見,更深層次的意思是invisible雖然不可見但仍佔據layout空間,而gone則是徹底的不可見,不佔據layout空間。也就說是,在某些情況下,gone會導致其它View重新佈局,如LinearLayout。Visibility操作的函數如下代碼所示。
@ViewDebug.ExportedProperty(mapping = {
@ViewDebug.IntToString(from = VISIBLE, to = "VISIBLE"),
@ViewDebug.IntToString(from = INVISIBLE, to = "INVISIBLE"),
@ViewDebug.IntToString(from = GONE, to = "GONE")
})
@Visibility
public int getVisibility() {
return mViewFlags & VISIBILITY_MASK;
}
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
在View的屬性或狀態中,並不是將這些屬性或狀態直接保存在某個變量中,而是通過一個Flag記錄,對這個Flag進行位操作,這是一種好的編程思維。
當View的Visibility發生變化時,會調用onVisibilityChanged,如果是View所在Window的Visibility發生變化時會調用onWindowVisibilityChanged。View在Window中,當View添加到Window中時會調用onAttachedToWindow,當View從Window中移除時會調用onDetachedFromWindow。另外,當從xml中加載layout時,加載完成後,會調用onFinishInflate。
8、Geometry
View是個矩形,包括x和y、width和height兩對基本屬性,單位爲pixel,座標原點在屏幕左上角,一般是相對座標,即子View相對於父View的位置。View的這兩對Geometry屬性又與其它的屬性相關,下面逐個介紹。
-
left:Child View左邊距離Parent View左邊的大小。
-
right:Child View右邊距離Parent View左邊的大小。
-
top:Child View上邊距離Parent View上邊的大小。
-
bottom:Child View下邊距離Parent View上邊的大小。
-
elevation:Child View在Z軸方向上相對於Parent View的深度。
-
translationX:View移動時相對於left的位置。
-
translationY:View移動時相對於top的位置。
-
translationZ:View移動時相對於elevation的位置
-
x:View在x軸方向上的實際位置,x = left + translationX。
-
y:View在y軸方向上的實際位置,y = top + translationY。
-
z:View在z軸方向上的實際位置,z = elevation + translationZ。
-
width:View的實際寬度,width = right - left。
-
height:View的實際高度,height = bottom - top。
-
measuredWidth:measure階段的測量值,與width可能不同。
-
measuredHeight:measure階段的測量值,與height可能不同。
-
paddingLeft:Child View與Parent View在左邊保留的最小間歇。
-
paddingRight:Child View與Parent View在右邊保留的最小間歇。
-
paddingTop:Child View與Parent View在上邊保留的最小間歇。
-
paddingBottom:Child View與Parent View在下邊保留的最小間歇。
-
paddingBegin:從左到右佈局時,等於paddingLeft,否則等於paddingRight。
-
paddingEnd:同paddingBegin相反。
另外,ViewGoup中添加了margin屬性,類似於padding屬性,不同的是,padding屬性影響的是Child,margin屬性影響的是自身。
上面的位置屬性都是相對的,有對應的getter/setter函數,如果獲取絕對座標即相對於屏幕原點的大小,可通過下面的三個函數獲取。
- getLocationInWindow
- getLocationOnScreen
- getGlobalVisibleRect
另外,對於MotionEvent來說,getX、getY返回相對座標,getRawX、getRawY返回絕對座標。
9、Measure、Layout、Draw
Android View的三大流程是measure、layout和draw,對應的都有個Callback即onMeasure、onLayout和onDraw,下圖先從時序上大致瞭解下這三大流程。
首先來看看measure。measure即測量,目的是測量一個View應該有多大,子View的大小受父View大小的影響,這個影響通過MeasureSpec指定。MeasureSpec有三種模式,其中UNSPECIFIED表示父View對子View沒有任何約束,EXACTLY表示父View限制了子View的大小爲具體的值,如xml文件中指定的具體的值或match_parent,AT_MOST表示父View指定了子View的最大值,如xml文件中指定的wrap_content。ViewGroup在測量時,通過measureChildren循環遍歷所有的子View,當子View的狀態不爲GONE時,通過measureChild測量這個子View的大小,這時需要通過getChildMeasureSpec獲得MeasureSpec,然後調用子View的measure進行測量。View的measure,主要的工作就是調用了onMeasure,onMeasure是個真正測量的地方,我們可以Override這個onMeasure做我們想做的事情,最後測量結果保存在了mMeasuredWidth和mMeasuredHeight。
layout即佈局,在measure階段之後,用於指定View的位置和大小。layout過程中,具體代碼實現由setFrame完成,當layout變化時,會調用onSizeChanged以及onLayout和相關的監聽Layout變化的Listener。我們可以Override onLayout,如RelativeLayout的onLayout,它會循環遍歷所有的子View,調用子View的layout函數。
draw即描畫,在ViewGroup中由dispatchDraw發起,然後通過drawChild調到View的draw函數,一個View如何渲染在draw函數指定,依賴於View所屬的layer類型和是否硬件加速。draw包括如下七個步驟。
- 描畫背景,drawBackground。
- 如果必要,保存canvas的layout用以fading繪製。
- 描畫View內容,onDraw。
- 描畫孩子,dispatchDraw。
- 如果必要,繪製fading並恢復之前保存的layer。
- 描畫裝飾,如scrollbar,onDrawForeground。
- 描畫默認的高亮focus,drawDefaultFocusHighlight。
10、Event
接收事件最簡單的方法是通過形如setOnXxxListener的函數設置對應事件的監聽者,常用的有如下幾個函數。
- setOnTouchListener:Touch事件,最基本的事件,包括down、up、move、cancel等。
- setOnClickListener:Click事件,即快速touch down、up。
- setOnLongClickListener:Long Click事件,即touch down、keep touch、up。
- setOnDragListener:Drag事件,即touch down、move。
- setOnKeyListener:Key事件。
此外,還有如下幾個設置事件監聽者的函數。
- setOnGenericMotionListener
- setOnContextClickListener
- setOnHoverListener
- setOnCapturedPointerListener
當我們自定義View時,可以Override形如OnXxxEvent的函數,也可以接收對應的事件,有如下幾個函數。
- onTouchEvent
- onDragEvent
- onKeyDown
- onKeyUp
- onKeyLongPress
- onKeyMultiple
- onKeyPreIme
- onKeyShortcut
- onTrackballEvent
- onGenericMotionEvent
- onHoverEvent
- onCancelPendingInputEvents
- onFilterTouchEventForSecurity
- onCapturedPointerEvent
在ViewGroup中,有如下幾個Event相關的函數。
- onInterceptTouchEvent:攔截Touch事件。
- onInterceptHoverEvent:攔截Hover事件。
- setMotionEventSplittingEnabled:設置事件分發策略,是否同時派發到多個子View。
事件如何派發?事件派發的相關函數爲dispatchXxxEvent,以dispatchTouchEvent爲例,下面的類圖列出了相關的函數。
ViewGroup的dispatchTouchEvent用於事件分發,決定把事件分發給哪個View或ViewGroup,onInterceptTouchEvent表示是否攔截事件,如果攔截事件,事件將不再往ViewGroup的孩子分發。View的dispatchTouchEvent用於事件執行,首先派發給Listener,沒有Listener或Listener不消費時,將事件發送到onTouchEvent。所有函數都有個boolean類型的返回值,返回true表示消費事件,返回false表示不消費事件。
下面分析事件分發的流程。
- dispatchTouchEvent是從ViewRootImpl的InputStage的ViewPostIme階段調過來的。
直接調用的是dispatchPointerEvent,然後根據Event類型決定調用dispatchTouchEvent還是dispatchGenericMotionEvent。源碼如下所示。
@UnsupportedAppUsage
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
- 接着是ViewGroup的dispatchTouchEvent。在dispatchTouchEvent的開始,首先會通過InputEventConsistencyVerifier進行事件一致性驗證,函數結束時如果事件還沒有被消費,也會通知InputEventConsistencyVerifier,代碼如下所示。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
// ...
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
- 接着,所有的代碼處理都在onFilterTouchEventForSecurity返回true的情況下執行,這是一種安全策略,當Window處於Obscured狀態時可以屏蔽事件,代碼如下。
/**
* Filter the touch event to apply security policies.
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
-
接着,對touch down事件進行特殊處理,因爲down是一切事件的開始,所以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. cancelAndClearTouchTargets(ev); resetTouchState(); }
-
接着,判斷是否攔截事件,當事件爲touch down或者有FirstTouchTarget時,在允許攔截事件的情況下就會調用onInterceptTouchEvent,進而判斷是否攔截了事件;當事件非touch down且有FirstTouchDown時,必然要攔截這個事件,因爲事件要發給touch down事件的接收者。然後,判斷事件是否cancel,是否可以分發事件到各個孩子。代碼如下所示。
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
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.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 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;
-
接着是事件真正分發的邏輯,前提是事件沒有cancel也沒有被攔截。如果事件被攔截,則通過super.dispatchTouchEvent進行處理。可以分發的事件有三種類型,一是touch down事件,二是hover事件,三是多個手指操作且允許事件分發到不同孩子的情況。事件分發時,循環遍歷所有的孩子,通過private函數dispatchTransformedTouchEvent真正實現事件分發。在dispatchTransformedTouchEvent中,其實就是調用了super.dispatchTouchEvent或child.dispatchTouchEvent。前面分析了ViewGroup的dispatchTouchEvent,下面分析View的dispatchTouchEvent。
-
View的dispatchTouchEvent中,其實就是把事件發送給Listener或onTouchEvent,代碼如下。
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
- Listener是使用者自己註冊實現的,下面看看View的onTouchEvent做了哪些事情。如果View註冊了Touch Delegate,事件首先會發給Delegate,代碼如下所示。
public boolean onTouchEvent(MotionEvent event) {
// ...
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// ...
}
/**
* Sets the TouchDelegate for this View.
*/
public void setTouchDelegate(TouchDelegate delegate) {
mTouchDelegate = delegate;
}
然後,是對touch down、up、move、cancel事件的處理。至此,事件處理結束。下面是事件分發的流程圖。
下面再簡單介紹下Android中相關的Gesture。Gesture有兩種用法,一種是GestureDector,用以識別MotionEvent爲哪些Gesture,支持的Gesture如下。
public class GestureDetector {
public interface OnGestureListener {
boolean onDown(MotionEvent e);
void onShowPress(MotionEvent e);
boolean onSingleTapUp(MotionEvent e);
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
void onLongPress(MotionEvent e);
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
boolean onContextClick(MotionEvent e);
}
}
另一種Gesture是通過GestureLibrary識別的觸摸屏上的軌跡線,詳見android.gesture.Gesture。
11、Scroll
View的Scroll表示可以移動View,準確的來說是移動View中的內容。例如,向右移動View 100個pixel,相當於向左移動View的內容100個pixel。下面列出View類中與Scroll相關主要的幾個變量和函數。
Scroll的x和y分別保存在變量mScrollX和mScrollY中。setScrollX、setScrollY和scrollBy都通過scrollTo實現,源碼如下所示。
public void setScrollX(int value) {
scrollTo(value, mScrollY);
}
public void setScrollY(int value) {
scrollTo(mScrollX, value);
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
當View被Scroll時,會調用onScrollChanged函數,如果通過setOnScrollChangeListener設置監聽Scroll變化的Listener,還會通知這個Listener,通知是在onScrollChanged函數中發出的,而onScrollChanged函數是在scrollTo中調用的,下面是scrollTo的源碼實現。
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();
}
}
}
View在Scroll時,實際上是作了一個動畫。真正的動作發生在postInvalidateOnAnimation,然後通過ViewRootImpl、 Choreographer、DisplayEventReceiver、native完成,時序圖如下。
12、Animation
View提供了播放動畫的接口,下面列出函數名。
- startAnimation:播放動畫。
- setAnimation:設置動畫。
- clearAnimation:取消動畫。
- getAnimation:獲取動畫。
- postInvaidateOnAnimation:用於Display下一幀時的動畫。
- postOnAnimation:指定下一個動畫幀時需要做的事情。
- postOnAnimationDelayed:指定下一個動畫幀時需要做的事情,有延遲。
- onAnimationStart:動畫開始時調用。
- onAnimationEnd:動畫結束時調用。
下面是Animation相關的類圖。
可以看出Animation支持的動畫種類有限,包括alpha、平移、縮放和旋轉,以及組合動畫AnimationSet,是Android早期的產品,後期又退出了更強大的Animator動畫框架。兩者相比,從版本兼容性來說,Animation更好;從效率來說,Animator使用了反射,效率稍差;從應用角度來說,Animator可以用於任意對象,應用範圍更廣;從動畫效果來看,Animator會更新View的實際位置,而Animation只是提供了一種動畫效果,實際位置保持不變。
下面是Animator相關的類圖。
13、佈局
Android有各種各樣的佈局,這些佈局都繼承自ViewGroup類,下面是幾種主要佈局的類圖。
- LinearLayout:線性佈局,包括水平方向和垂直方向。
- GridLayout:網格佈局。
- AbsoluteLayout:絕對佈局。
- RelativeLayout:相對佈局。
- FrameLayout:幀佈局,在z軸方向上的佈局,上面的會覆蓋下面的。
上面的這些佈局都繼承自ViewGroup,必定會包含一些Child,對這些Child的管理通過ViewManager和ViewParent完成,文章開頭列出了相關的類圖。