Android、View视图与坐标系
View的滑动和属性动画
从源码解析View的事件分发机制
View的工作流程
Android自定义view
从源码解析View的事件分发机制
View的事件分发机制
在讲到View的事件分发机制之前要首先了解
一下Activity的组成,然后从源码的角度来分析View的事件分发机制。
源码解析Activity的构成
点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity,所以我们首先要了解一下Activity的构成。当我们写Activity时会调用setContentView()方法来加载布局。现在来看看
setContentView()方法是怎么实现的,代码如下所示:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
这里调用了getWindow().setContentView(layoutResID),getWindow()指的是什么呢?接着往下看,getWindow()返回mWindow:
public Window getWindow() {
return mWindow;
}
那这个mWindow又是什么呢?我们继续查看代码,最终在Activity的attach()方法中发现了mWindow,代码如下所示:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);
...
}
而getWindow()又指的是PhoneWindow。所以来看看PhoneWindow的setContentView()方法,代码如下所示:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//1
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent,
layoutResID, getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
原来mWindow指的就是PhoneWindow,PhoneWindow是继承抽象类Window的,这样就知道了getWindow()得到的是一个PhoneWindow,因为Activity中setContentView()方法调用的是getWindow().setContentView(layoutResID)。
挑关键的接着看,看看上面代码注释1处installDecor()方法里面做了什么,代码如下所示:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor();//1
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
...
}
在前面的代码中没发现什么,紧接着查看上面代码注释1处的generateDecor方法里做了什么:
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
这里创建了一个DecorView,这个DecorView就是Activity中的根View。接着查看DecorView的源码,发现DecorView是PhoneWindow类的内部类,并且继承了FrameLayout。我们再回到installDecor()方法中,
查看generateLayout(mDecor)做了什么:
protected ViewGroup generateLayout(DecorView decor) {
...
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
...
}
PhoneWindow的generateLayout()方法比较长,这里只截取了一小部分关键的代码,其主要内容就是根据不同的情况加载不同的布局给layoutResource。现在查看上面代码注释1处的布局R.layout.screen_title,这个文件在frameworks,它的代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
上面的ViewStub是用来显示Actionbar的。下面的两个FrameLayout:一个是title,用来显示标题;另一
个是content,用来显示内容。看到上面的源码,大家就知道了一个Activity包含一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根 View,而这个DecorView又将屏幕划分为两个区域:一个是 TitleView,另一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的,
源码解析View的事件分发机制
当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEvent在View中的层级传递过程就是点击事件分发。在了解了什么是事件分发后,我们还需要了解事件分发的3个重要方法。点击事件有3个重要的方法,它们分别是:
- dispatchTouchEvent(MotionEvent ev)——用来进行事件的分发。
- onInterceptTouchEvent(MotionEvent ev)——用来进行事件的拦截,在dispatchTouchEvent()中调用,
需要注意的是View没有提供该方法。 - onTouchEvent(MotionEvent ev)——用来处理点击事件,在dispatchTouchEvent()方法中进行调用。
View的事件分发机制
当点击事件产生后,事件首先会传递给当前的 Activity,这会调用 Activity 的dispatchTouchEvent()方法,当然具体的事件处理工作都是交由Activity中的PhoneWindow来完成的,然后PhoneWindow再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给根ViewGroup。所以,我们从ViewGroup的
dispatchTouchEvent()方法开始分析,代码如下所示:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//2
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
return handled;
}
这里首先判断事件是否为DOWN事件,如果是,则进行初始化,resetTouchState方法中会把mFirstTouchTarget的值置为null。这里为什么要进行初始化呢?原因就是一个完整的事件序列是以DOWN开始,以UP结束的。所以如果是DOWN事件,那么说明这是一个新的事件序列,故而需要初始化之前的状态。接着往下看,上面代码注释1处的条件如果满足,则执行下面的句子,mFirstTouchTarget 的意义是:当前 ViewGroup是否拦截了事件,如果拦截了,mFirstTouchTarget=null;如果没有拦截并交由子View来处理,则mFirstTouchTarget!=null。假设当前的 ViewGroup 拦截了此事件,mFirstTouchTarget!=null 则为false,如果这时触发ACTION_DOWN事件,则会执行 onInterceptTouchEvent(ev) 方法;如果触发的是ACTION_MOVE、ACTION_UP事件,则不再执行onInterceptTouchEvent(ev)方法,而是直接设置
intercepted=true,此后的一个事件序列均由这个ViewGroup处理。再往下看,上面代码注释2 处又出现了一个FLAG_DISALLOW_INTERCEPT标志位,它主要是禁止ViewGroup 拦截除了DOWN之外的事件,一般通
过子View的requestDisallowInterceptTouchEvent来设置。所以总结一下就是,当ViewGroup要拦截事件的时候,那么后续的事件序列都将交给它处理,而不用再调用onInterceptTouchEvent()方法了。所以,onInterceptTouchEvent()方法并不是每次事件都会调用的。接下来查看onInterceptTouchEvent()方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
onInterceptTouchEvent()方法默认返回false,不进行拦截。如果想要让ViewGroup拦截事件,那么应该在自定义的ViewGroup中重写这个方法。接着来看看dispatchTouchEvent()方法剩余的部分源码,如下
所示:
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//1
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child) ||
!isTransformedTouchPointInView(x, y, child, null)) {//2
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child,idBitsToAssign)) {//3
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
...
}
在上面代码注释1处我们看到了for循环。首先遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意这个for循环是倒序遍历的,即从最上层的子View开始往内层遍历。接着往下看注释2处的代码,其意思是判断触摸点位置是否在子View的范围内或者子View是否在播放动画。如果均不符合则执行continue语句,表示这个子View不符合条件,开始遍历下一个子View。接下来查看注释3处的dispatchTransformedTouchEvent方法做了什么,代码如下所
示:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
如果有子View,则调用子View的dispatchTouchEvent(event)方法。如果ViewGroup没有子View,则调
用super.dispatchTouchEvent(event)方法。ViewGroup是继承View的,再来查看View的dispatchTouchEvent(event):
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
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;
}
}
}
我们看到如果OnTouchListener不为null并且onTouch方法返回true,则表示事件被消费,就不会执行onTouchEvent(event);否则就会执行onTouchEvent(event)。可以看出OnTouchListener中的onTouch()方法优先级要高于onTouchEvent(event)方法。下面再来看看onTouchEvent()方法的部分源码:
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
return true;
}
return false;
}
从上面的代码可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回true消耗这个事件。CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按点击,可以通过View的setClickable和setLongClickable方法来设置,也可以通过View的
setOnClickListenter和setOnLongClickListener来设置,它们会自动将View设置为CLICKABLE和LONG_CLICKABLE。接着在ACTION_UP事件中会调用performClick()方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {//1
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
从上面代码注释 1 处可以看出,如果 View 设置了点击事件 OnClickListener,那么它的onClick()方法
就会被执行。View事件分发机制的源码分析就讲到这里了,接下来介绍点击事件分发的传递规则。
点击事件分发的传递规则
由前面事件分发机制的源码分析可知点击事件分发的这3个重要方法的关系,用伪代码来简单表示:
public boolean dispatchTouchEvent(MotionEvent event){
boolean result = false;
if (onInterceptHoverEvent(event)){
result = super.onTouchEvent(event);
}else {
result = child.dispatchTouchEvent(event);
}
return result;
}
onInterceptTouchEvent方法和onTouchEvent方法都在dispatchTouchEvent方法中调用。现在我们根据这段
伪代码来分析一下点击事件分发的传递规则。
首先讲一下点击事件由上而下的传递规则,当点击事件产生后会由 Activity 来处理,传递给
PhoneWindow,再传递给DecorView,最后传递给顶层的ViewGroup。一般在事件传递中只考虑 ViewGroup
的 onInterceptTouchEvent 方法,因为一般情况下我们不会重写 dispatchTouchEvent()方法。对于根
ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGroup的onInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理;如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则这个事件会交给它的子元素的dispatchTouchEvent()来处理,如此反复下去。如果传递给底层的View,View是没有子View的,就会调用View的dispatchTouchEvent()方法,一般情况下最终会调用View的onTouchEvent()方法。
接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其onTouchEvent()方法返回true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理;如果父View的onTouchEvent()仍旧返回false,则继续传递给该父View的父View处理,如此反复下去。
以上内容摘自《Android进阶之光》