文章目录
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完成,文章开头列出了相关的类图。