Android Touch事件之二:dispatchTouchEvent()和onTouchEvent()篇

2015-12-01 15:06:14

Android Touch事件第一篇:Touch事件在父ViewGroup和子View之间的传递简单分析了事件的传递流程,这次深入了解下dispatchTouchEvent()和onTouchEvent()这两个方法。

1. 上篇中提到Touch事件是由系统传入的,那么到底是怎么回事?系统是怎么传入的呢?到底是系统的那个模块具体做的?事件究竟是如何到达我们自己编写的View呢?

首先要明白一件事,我们自己的Activity,通过setContentView()设置了一个布局xml文件,一般该布局xml文件的Root View肯定是一个ViewGroup,在上一篇的Demo中,Root View就是MyLinear1,上篇中提到了Touch事件传入了MyLinear1,当时没有解决的问题是,事件是怎样传入MyLinear1的?

现在来梳理一下。其实,Touch事件从手机屏幕(硬件)-->Linux驱动层-->Android系统层-->Window(大家先这么理解即可,这其中具体细节,我也不清楚,等有时间了再研究吧),每一个Activity中都有一个Window,Window也是有布局的,我们自己写的Root View,其实并不是一个Activity的根View,那么我们来找找Activity真正的根View。

2. 完善一下上篇中的Demo,下面是MainActivity的代码,其他不变。

MainActivity.java

 1 public class MainActivity extends Activity {
 2     @Override
 3     protected void onCreate(Bundle savedInstanceState) {
 4         super.onCreate(savedInstanceState);
 5         setContentView(R.layout.activity_main);
 6     }
 7 
 8     @Override
 9     public boolean dispatchTouchEvent(MotionEvent event) {
10         Log.e("David--MainActivity", "dispatchTouchEvent-" + MyLinear1.getS(event.getAction()));
11         return super.dispatchTouchEvent(event);
12     }
13 
14     @Override
15     public boolean onTouchEvent(MotionEvent event) {
16         Log.e("David--MainActivity", "onTouchEvent-" + MyLinear1.getS(event.getAction()));
17         return super.onTouchEvent(event);
18     }
19 }

下面来看一下日志:

点击非MyLienar1的区域

1 12-01 17:05:30.782 E/David--MainActivity(25309): dispatchTouchEvent-Down
2 12-01 17:05:30.782 E/David--MainActivity(25309): onTouchEvent-Down
3 12-01 17:05:30.812 E/David--MainActivity(25309): dispatchTouchEvent-Move
4 12-01 17:05:30.812 E/David--MainActivity(25309): onTouchEvent-Move
5 12-01 17:05:30.862 E/David--MainActivity(25309): dispatchTouchEvent-Move
6 12-01 17:05:30.862 E/David--MainActivity(25309): onTouchEvent-Move
7 12-01 17:05:30.872 E/David--MainActivity(25309): dispatchTouchEvent-Up
8 12-01 17:05:30.872 E/David--MainActivity(25309): onTouchEvent-Up

可见,Touch事件最先传到Activity的,由于点击的地方,没有其他View,所以Touch事件没有继续向下传递。

点击MyLienar1

 1 12-01 17:07:44.622 E/David--MainActivity(25309): dispatchTouchEvent-Down
 2 12-01 17:07:44.622 E/David--MyLinear(25309): dispatchTouchEvent-Down
 3 12-01 17:07:44.622 E/David--MyLinear(25309): onInterceptTouchEvent-Down
 4 12-01 17:07:44.622 E/David--C---------------(25309): dispatchTouchEvent-Down
 5 12-01 17:07:44.622 E/David--C---------------(25309): onTouchEvent-Down
 6 12-01 17:07:44.622 E/David--C---------------(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true
 7 12-01 17:07:44.622 E/David--MyLinear(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true
 8 12-01 17:07:44.682 E/David--MainActivity(25309): dispatchTouchEvent-Move
 9 12-01 17:07:44.682 E/David--MyLinear(25309): dispatchTouchEvent-Move
10 12-01 17:07:44.682 E/David--MyLinear(25309): onInterceptTouchEvent-Move
11 12-01 17:07:44.682 E/David--C---------------(25309): dispatchTouchEvent-Move
12 12-01 17:07:44.682 E/David--C---------------(25309): onTouchEvent-Move
13 12-01 17:07:44.682 E/David--C---------------(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true
14 12-01 17:07:44.682 E/David--MyLinear(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true
15 12-01 17:07:44.682 E/David--MainActivity(25309): dispatchTouchEvent-Up
16 12-01 17:07:44.682 E/David--MyLinear(25309): dispatchTouchEvent-Up
17 12-01 17:07:44.682 E/David--MyLinear(25309): onInterceptTouchEvent-Up
18 12-01 17:07:44.682 E/David--C---------------(25309): dispatchTouchEvent-Up
19 12-01 17:07:44.682 E/David--C---------------(25309): onTouchEvent-Up
20 12-01 17:07:44.682 E/David--C---------------(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true
21 12-01 17:07:44.682 E/David--MyLinear(25309): dispatchTouchEvent() super.dispatchTouchEvent(event) = true

有两点:1、Touch事件是经由Activity传入的;2、事件被MyLinear1和它的子View获取,没有MainActivity什么事,MainActivity的onTouchEvent事件没有被调用;

如果MainActivity的dispatchTouchEvent不调用super.dispatchTouchEvent(),无论直接返回false还是true,事件不向Activity的子View传递,所以可以证明,Touch事件向下传递是在super.dispatchTouchEvent()中。我们去看看Activity.java中的dispatchTouchEvent()方法。

1     public boolean dispatchTouchEvent(MotionEvent ev) {
2         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3             onUserInteraction(); (1)
4         }
5         if (getWindow().superDispatchTouchEvent(ev)) { (2)
6             return true;
7         }
8         return onTouchEvent(ev); (3)
9     }

(1)这是一个空方法,有Touch操作时候,首先调用的就是这个方法。如果你想知道用户是否有和你的Activity有交互,可以在这里作文章~有兴趣的可以研究下,毕竟对于收集用户的交互行为是很有意义的;

(3)如果Activity没有View想要处理Touch事件,那就调用Activity的onTouchEvent()方法。

(2)getWindow()获取的是android.view.Window,决定一个窗口外观和行为的最顶级的抽象类,作为top-level view被添加到WindowManager。Window只有一个子类PhoneWindow,看看这名字,就知道有多霸气了。getWindow()获取到的就是PhoneWindow了,我们看看PhoneWindow的superDispatchTouchEvent(ev)方法,代码如下:

1     @Override
2     public boolean superDispatchTouchEvent(MotionEvent event) {
3         return mDecor.superDispatchTouchEvent(event);
4     }

这里有个mDecor对象,看看这是神马鬼

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { ***

到这里终于真相大白了,Activity真正的的Root View就是DecorView,继承自FrameLayout,是一个ViewGroup哦~那么调用DecorView的dispatchTouchEvent就是在调用ViewGroup的dispatchTouchEvent了,这个圈太绕了~

梳理一下:

我们的Demo中,真正的View层次结构是DecorView-->MyLinear1-->CustomView。证据如下:

看到了吧,根View是DecorView,包含了NavigationBar、ActionBar和FrameLayout,最底下的就是我们自己的MyLinear1和CustomView了。

总结:

    当用户在一个Activity上点击或者滑动时,Android系统收到Touch事件,然后分发给Activity的Root View PhoneWindow.DecorView,DecorView是我们自己的View层级的ViewGroup,剩下的就和上篇文章中分析的一样了。

3. id/content的FrameLayout

仔细看一下上图中的第二道红线,有个FrameLayout,前面分析提到过,DecorView是一个GroupView,包含了NavigationBar、ActionBar和FrameLayout,而且这个FrameLayout是“id/content”,它和我们平常使用的setContentView有什么关系呢?

Activity.java中的setContentView()方法:

 1     /**
 2      * Set the activity content from a layout resource.  The resource will be
 3      * inflated, adding all top-level views to the activity.
 4      *
 5      * @param layoutResID Resource ID to be inflated.
 6      *
 7      * @see #setContentView(android.view.View)
 8      * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 9      */
10     public void setContentView(int layoutResID) {
11         getWindow().setContentView(layoutResID);
12         initWindowDecorActionBar();
13     }

getWindow()获取的是PhoneWindow,这点前面提到过。至于initWindowDecorActionBar()可以理解为初始化ActionBar相关的东西,不在咱们的探究范围内。进入PhoneWindow的setContentView()方法。

 1     @Override
 2     public void setContentView(int layoutResID) {
 3         // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
 4         // decor, when theme attributes and the like are crystalized. Do not check the feature
 5         // before this happens.
 6         if (mContentParent == null) {
 7             installDecor();
 8         } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 9             mContentParent.removeAllViews();
10         }
11 
12         if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
13             final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
14                     getContext());
15             transitionTo(newScene);
16         } else {
17             mLayoutInflater.inflate(layoutResID, mContentParent);(1)
18         }
19         final Callback cb = getCallback();
20         if (cb != null && !isDestroyed()) {
21             cb.onContentChanged();
22         }
23     }

从代码中(1)可以看出,如果mContentParent存在,那么直接将我们自己的View解析到mContentParent。如果mContentParent==null,那么就installDecor,我们先看看mContentParent的定义:

1     // This is the top-level view of the window, containing the window decor.
2     private DecorView mDecor;
3 
4     // This is the view in which the window contents are placed. It is either
5     // mDecor itself, or a child of mDecor where the contents go.
6     private ViewGroup mContentParent;
7 
8     private ViewGroup mContentRoot;

mDecor:top-level View;

mContentParent:注定要被替换的ViewGroup,说白了,就是被我们自己写的布局xml文件替换掉。

mContentRoot:Activity的布局xml文件(不是我们自己继承的MainActivity)

进入installDecor()方法:

 1     private void installDecor() {
 2         if (mDecor == null) {
 3             mDecor = generateDecor();
 4             mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
 5             mDecor.setIsRootNamespace(true);
 6             if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
 7                 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
 8             }
 9         }
10         if (mContentParent == null) {
11             mContentParent = generateLayout(mDecor);

如果mDecor==null,初始化mDecor。如果mContentParent==null,同样做初始化的操作,Decor类后面再说,先看generateLayout()方法。

 1    protected ViewGroup generateLayout(DecorView decor) {
 2         // Apply data from current theme.
 3         // 省略一些代码,主要是做theme判断的,比如Activity是否要显示成Float,是否有ActionBar什么的
 4 
 5         // Inflate the window decor.
 6         // 生成Window Decor
 7         int layoutResource;
 8         int features = getLocalFeatures();
 9         // System.out.println("Features: 0x" + Integer.toHexString(features));
10         if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
11             layoutResource = R.layout.screen_swipe_dismiss;
12         } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
13             if (mIsFloating) {
14                 TypedValue res = new TypedValue();
15                 getContext().getTheme().resolveAttribute(
16                         R.attr.dialogTitleIconsDecorLayout, res, true);
17                 layoutResource = res.resourceId;
18             } else {
19                 layoutResource = R.layout.screen_title_icons; (1)
20             }
21             // XXX Remove this once action bar supports these features.
22             removeFeature(FEATURE_ACTION_BAR);
23             // System.out.println("Title Icons!");
24         }
25 26         mDecor.startChanging();
27 
28         View in = mLayoutInflater.inflate(layoutResource, null);
29         decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
30         mContentRoot = (ViewGroup) in; (2)
31 
32         ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
33         if (contentParent == null) {
34             throw new RuntimeException("Window couldn't find content container view");
35         }
36 
37         // Remaining setup -- of background and title -- that only applies
38         // to top-level windows.
39         if (getContainer() == null) {
40             final Drawable background;
41             if (mBackgroundResource != 0) {
42                 background = getContext().getDrawable(mBackgroundResource);
43             } else {
44                 background = mBackgroundDrawable;
45             }
46             mDecor.setWindowBackground(background);
47 
48             final Drawable frame;
49             if (mFrameResource != 0) {
50                 frame = getContext().getDrawable(mFrameResource);
51             } else {
52                 frame = null;
53             }
54             mDecor.setWindowFrame(frame);
55 
56             mDecor.setElevation(mElevation);
57             mDecor.setClipToOutline(mClipToOutline);
58 
59             if (mTitle != null) {
60                 setTitle(mTitle);
61             }
62 
63             if (mTitleColor == 0) {
64                 mTitleColor = mTextColor;
65             }
66             setTitleColor(mTitleColor);
67         }
68 
69         mDecor.finishChanging();
70 
71         return contentParent;
72     }

上面的方法中只保留了关键代码

(1) 这是最常见的一种Activity布局,当然还有其他的布局xml文件,有兴趣的自己研究吧,看一下screen_title_icons.xml文件

  1 <?xml version="1.0" encoding="utf-8"?>
  2 <!-- Copyright (C) 2006 The Android Open Source Project
  3 
  4      Licensed under the Apache License, Version 2.0 (the "License");
  5      you may not use this file except in compliance with the License.
  6      You may obtain a copy of the License at
  7   
  8           http://www.apache.org/licenses/LICENSE-2.0
  9   
 10      Unless required by applicable law or agreed to in writing, software
 11      distributed under the License is distributed on an "AS IS" BASIS,
 12      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13      See the License for the specific language governing permissions and
 14      limitations under the License.
 15 -->
 16 
 17 <!--
 18 This is the basic layout for a screen, with all of its features enabled.
 19 -->
 20 
 21 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 22     android:fitsSystemWindows="true"
 23     android:orientation="vertical"
 24     android:layout_width="match_parent"
 25     android:layout_height="match_parent">
 26     <!-- Popout bar for action modes -->
 27     <ViewStub android:id="@+id/action_mode_bar_stub"
 28               android:inflatedId="@+id/action_mode_bar"
 29               android:layout="@layout/action_mode_bar"
 30               android:layout_width="match_parent"
 31               android:layout_height="wrap_content"
 32               android:theme="?attr/actionBarTheme"/>
 33     <RelativeLayout android:id="@android:id/title_container"
 34         style="?android:attr/windowTitleBackgroundStyle"
 35         android:layout_width="match_parent"
 36         android:layout_height="?android:attr/windowTitleSize">
 37         <!-- The title background has 9px left padding. -->
 38         <ImageView android:id="@android:id/left_icon"
 39             android:visibility="gone"
 40             android:layout_marginEnd="9dip"
 41             android:layout_width="16dip"
 42             android:layout_height="16dip"
 43             android:scaleType="fitCenter"
 44             android:layout_alignParentStart="true"
 45             android:layout_centerVertical="true" />
 46         <ProgressBar android:id="@+id/progress_circular"
 47             style="?android:attr/progressBarStyleSmallTitle"
 48             android:visibility="gone"
 49             android:max="10000"
 50             android:layout_centerVertical="true"
 51             android:layout_alignParentEnd="true"
 52             android:layout_marginStart="6dip"
 53             android:layout_width="wrap_content"
 54             android:layout_height="wrap_content" />
 55         <!-- There are 6dip between this and the circular progress on the right, we
 56              also make 6dip (with the -3dip margin_left) to the icon on the left or
 57              the screen left edge if no icon. This also places our left edge 3dip to
 58              the left of the title text left edge. -->
 59         <ProgressBar android:id="@+id/progress_horizontal"
 60             style="?android:attr/progressBarStyleHorizontal"
 61             android:layout_width="match_parent"
 62             android:layout_height="wrap_content"
 63             android:layout_marginStart="-3dip"
 64             android:layout_toStartOf="@android:id/progress_circular"
 65             android:layout_toEndOf="@android:id/left_icon"
 66             android:layout_centerVertical="true"
 67             android:visibility="gone"
 68             android:max="10000" />
 69         <LinearLayout
 70             android:layout_width="match_parent"
 71             android:layout_height="match_parent"
 72             android:orientation="horizontal"
 73             android:layout_toStartOf="@id/progress_circular"
 74             android:layout_toEndOf="@android:id/left_icon"
 75             >
 76             <TextView android:id="@android:id/title"
 77                 style="?android:attr/windowTitleStyle"
 78                 android:layout_width="0dip"
 79                 android:layout_height="match_parent"
 80                 android:layout_weight="1"
 81                 android:background="@null"
 82                 android:fadingEdge="horizontal"
 83                 android:scrollHorizontally="true"
 84                 android:gravity="center_vertical"
 85                 android:layout_marginEnd="2dip"
 86                 />
 87             <!-- 2dip between the icon and the title text, if icon is present. -->
 88             <ImageView android:id="@android:id/right_icon"
 89                 android:visibility="gone"
 90                 android:layout_width="16dip"
 91                 android:layout_height="16dip"
 92                 android:layout_weight="0"
 93                 android:layout_gravity="center_vertical"
 94                 android:scaleType="fitCenter"
 95                 />
 96             </LinearLayout>
 97     </RelativeLayout>
 98     <FrameLayout android:id="@android:id/content"
 99         android:layout_width="match_parent"
100         android:layout_height="0dip"
101         android:layout_weight="1"
102         android:foregroundGravity="fill_horizontal|top"
103         android:foreground="?android:attr/windowContentOverlay" />
104 </LinearLayout>

这是一个典型的Activity的布局,绿色部分的代码,就是一个ViewGroup,是用来放置我们自己的xml布局文件的View Container,现在知道为什么我们需要setContentView(***)了吗?因为容器的id就是content~

(2)处,mContentRoot就是就是Activity的View;

(3)处,contentParent也就是mContentParent,才是真正放置我们自己View的ViewGroup。

总结:至此,关于Activity根View的问题已经解释清楚了。同时也搞明白了系统事件到底是怎么传递到MyLinear1的~

4. 回到本篇的标题,仔细研究一下View.java中的dispatchTouchEvent()和onTouchEvent()到底是怎样工作的。

原文地址:https://www.cnblogs.com/wlrhnh/p/5011148.html