Android坐标系

说到Android坐标系其实就是一个三维坐标,Z轴向上,X轴向右,Y轴向下。这三维坐标的点处理就能构成Android丰富的界面或者动画等效果,所以Android坐标系在整个Android界面中算是盖楼房的尺寸草图,下面我们就来看看这些相关的概念。

2-1 Android屏幕区域划分

我们先看一副图来了解一下Android屏幕的区域划分如下:

Android 坐标系及动画移动坐标相关集合_第1张图片

Android屏幕的区域划分

通过上图我们可以很直观的看到Android对于屏幕的划分定义。下面我们就给出这些区域里常用区域的一些坐标或者度量方式。如下:

  • //获取屏幕区域的宽高等尺寸获取
DisplayMetrics metrics = new DisplayMetrics();getWindowManager().getDefaultDisplay().getMetrics(metrics);int widthPixels = metrics.widthPixels;int heightPixels = metrics.heightPixels;
  • //应用程序App区域宽高等尺寸获取
Rect rect = new Rect();getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
  • //获取状态栏高度
Rect rect= new Rect();getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);int statusBarHeight = rectangle.top;
  • //View布局区域宽高等尺寸获取
Rect rect = new Rect();getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);

特别注意:上面这些方法最好在Activity的onWindowFocusChanged ()方法或者之后调运,因为只有这时候才是真正的显示OK,不懂的可以看我之前关于setContentView相关的博客。

2-2 Android View绝对相对坐标系

上面我们分析了Android屏幕的划分,可以发现我们平时开发的重点其实都在关注View布局区域,那么下面我们就来细说一下View区域相关的各种坐标系。先看下面这幅图:

Android 坐标系及动画移动坐标相关集合_第2张图片

View区域相关的各种坐标系

通过上图我们可以很直观的给出View一些坐标相关的方法解释,不过必须要明确的是上面这些方法必须要在layout之后才有效,如下:

View的静态坐标方法解释
getLeft()返回View自身左边到父布局左边的距离
getTop()返回View自身顶边到父布局顶边的距离
getRight()返回View自身右边到父布局左边的距离
getBottom()返回View自身底边到父布局顶边的距离
getX()返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变。
getY()返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变。

同时也可以看见上图中给出了手指触摸屏幕时MotionEvent提供的一些方法解释,如下:

MotionEvent坐标方法解释
getX()当前触摸事件距离当前View左边的距离
getY()当前触摸事件距离当前View顶边的距离
getRawX()当前触摸事件距离整个屏幕左边的距离
getRawY()当前触摸事件距离整个屏幕顶边的距离

上面就解释了你在很多代码中看见各种getXXX方法进行数学逻辑运算判断的含义。不过上面只是说了一些相对静止的Android坐标点关系,下面我们来看看几个和上面方法紧密相关的View方法。如下:

View宽高方法解释
getWidth()layout后有效,返回值是mRight-mLeft,一般会参考measure的宽度(measure可能没用),但不是必须的。
getHeight()layout后有效,返回值是mBottom-mTop,一般会参考measure的高度(measure可能没用),但不是必须的。
getMeasuredWidth()返回measure过程得到的mMeasuredWidth值,供layout参考,或许没用。
getMeasuredHeight()返回measure过程得到的mMeasuredHeight值,供layout参考,或许没用。

上面解释了自定义View时各种获取宽高的一些含义,下面我们再来看看关于View获取屏幕中位置的一些方法,不过这些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。如下图:

Android 坐标系及动画移动坐标相关集合_第3张图片

这里写图片描述

下面我们就给出上面这幅图涉及的View的一些坐标方法的结果(结果采用使用方法返回的实际坐标,不依赖上面实际绝对坐标转换,上面绝对坐标只是为了说明例子中的位置而已),如下:

View的方法上图View1结果上图View2结果结论描述
getLocalVisibleRect()(0,0 - 410, 100)(0, 0 - 410, 470)获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。
getGlobalVisibleRect()(30, 100 - 440, 200)(30, 250 - 440, 720)获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。
getLocationOnScreen()(30, 100)(30, 250)坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。
getLocationInWindow()(30, 100)(30, 250)如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。

到此常用的相关View的静态坐标获取处理的方法和含义都已经叙述完了,下面我们看看动态的一些解释(所谓动静只是我个人称呼而已)。

2-3 Android View动画相关坐标系

其实在我们使用动画时,尤其是补间动画时,你会发现其中涉及很多坐标参数,一会儿为相对的,一会儿为绝对的,你可能会各种蒙圈。那么不妨看下《Android应用开发之所有动画使用详解 》这篇博客,这里面详细介绍了关于Android动画相关的坐标系统,这里不再累赘叙述。

2-4 Android View滑动相关坐标系

关于View提供的与坐标息息相关的另一组常用的重要方法就是滚动或者滑动相关的,下面我们给出相关的解释(特别注意:View的scrollTo()和scrollBy()是用于滑动View中的内容,而不是改变View的位置;改变View在屏幕中的位置可以使用offsetLeftAndRight()和offsetTopAndBottom()方法,他会导致getLeft()等值改变。),如下:

View的滑动方法| 效果及描述
-:---|:---
offsetLeftAndRight(int offset)|水平方向挪动View,offset为正则x轴正向移动,移动的是整个View,getLeft()会变的,自定义View很有用。
offsetTopAndBottom(int offset)|垂直方向挪动View,offset为正则y轴正向移动,移动的是整个View,getTop()会变的,自定义View很有用。
scrollTo(int x, int y)|将View中内容(不是整个View)滑动到相应的位置,参考坐标原点为ParentView左上角,x,y为正则向xy轴反方向移动,反之同理。
scrollBy(int x, int y)|在scrollTo()的基础上继续滑动xy。
setScrollX(int value)|实质为scrollTo(),只是只改变Y轴滑动。
setScrollY(int value)|实质为scrollTo(),只是只改变X轴滑动。
getScrollX()/getScrollY()|获取当前滑动位置偏移量。

关于Android View的scrollBy()和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的该方法注释里明确说明了调运他会触发onScrollChanged()和invalidated()方法,那我们就将矛头转向invalidated()方法触发的draw()过程,draw()过程中最终其实会触发下面的invalidate()方法,如下:

public void invalidate(int l, int t, int r, int b) {    final int scrollX = mScrollX;    final int scrollY = mScrollY;    //scroller时为何参数和坐标反向的真实原因    invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);}

核心就在这里,相信不用我解释大家也知道咋回事了,自行脑补。

scrollTo()和scrollBy()方法特别注意:如果你给一个ViewGroup调用scrollTo()方法滚动的是ViewGroup里面的内容,如果想滚动一个ViewGroup则再给他嵌套一个外层,滚动外层即可。

3 View中还有一些其他与坐标获取相关的方法

关于view获取自身坐标的方法和点击事件中坐标的获取,网上也有一些博客,写的不是很完整,现在系统的来讲一下。

其实只要把下面这张图看明白就没问题了。

Android 坐标系及动画移动坐标相关集合_第4张图片

View中还有一些其他与坐标获取相关的方法

涉及到的方法一共有下面几个:

  • view获取自身坐标:getLeft(),getTop(),getRight(),getBottom()
  • view获取自身宽高:getHeight(),getWidth()
  • motionEvent获取坐标:getX(),getY(),getRawX(),getRawY()

首先是view的几个方法,
获取自身的宽高的这两个方法很清楚,不用多说,获取坐标的这几个就有点混乱了。
根据上面的图应该会比较容易明白,图中屏幕上放了一个ViewGroup布局,里面有个View控件

getTop:获取到的,是view自身的顶边到其父布局顶边的距离
 getLeft:获取到的,是view自身的左边到其父布局左边的距离
 getRight:获取到的,是view自身的右边到其父布局左边的距离
 getBottom:获取到的,是view自身的底边到其父布局顶边的距离

这些方法获取到的数据可以用在什么地方呢?比如要实现一个自定义的特殊布局,像http://blog.csdn.net/singwhatiwanna/article/details/42614953 这里要实现的是一个水波纹特效布局,该布局内的任何控件点击后都会出现波纹效果

那么在点击了布局内的一个控件之后,就要通过不断刷新布局,去更新这个控件上面的波纹半径,为了节省资源,每次刷新布局都时候不会整个布局都刷新,而只是通过

postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);  

在布局的画布上每次只去更新点击事件所点击的对应的控件的位置,那么这里就可以用view的那四个方法,分别获取自身的四条边对应的坐标.从而让布局去刷新重绘。

当然博客中是使用绝对坐标去计算的,因为这里实现的是一个布局,可能里面还会嵌套另外的布局,经过多次嵌套之后所获取到的值,是相对于控件直接对应的父布局(这个布局有可能已经是我们重写的布局的子布局了)的距离,这样去刷新的区域肯定是不准确的,所以博客里面使用相对屏幕的绝对坐标计算需要刷新的控件区域。

如果这里自定义的不是布局,而只是一个控件的话,就可以通过以上方法获取到坐标,然后要求自己所在的布局去重绘这一区域就可以了。当然这只是一种思路,其实没必要去要求布局重绘,完全可以直接view自身重绘就可以了。

然后是motionEvent的方法:

getX():获取点击事件相对控件左边的x轴坐标,即点击事件距离控件左边的距离
 getY():获取点击事件相对控件顶边的y轴坐标,即点击事件距离控件顶边的距离
 getRawX():获取点击事件相对整个屏幕左边的x轴坐标,即点击事件距离整个屏幕左边的距离
 getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离
这些方法可以用在什么地方呢?

getRawX和getRawY在之前那篇博客里广泛使用了,可以去那里看用法,getX()和getY()这两个方法在对view进行自定义的时候可能用的会比较多。

scrollTo()和scrollBy()方法特别注意:如果你给一个ViewGroup调用scrollTo()方法滚动的是ViewGroup里面的内容,如果想滚动一个ViewGroup则再给他嵌套一个外层,滚动外层即可。

1.view.getTranslationX计算的是该view的偏移量。初始值为0,向左偏移值为负,向右偏移值为正。

2.view.getX相当于该view距离父容器左边缘的距离,等于getLeft+getTranslationX。

 

android view的多种移动方式

目前为止大致有这几种方法可以移动view:
**1、setTranslationX/Y
2、scrollTo/scrollBy
3、offsetTopAndBottom/offsetLeftAndRight
4、平移动画
5、设置margin
**

主要是验证一些属性:
1、getX()、getY()
2、getScrollX() 、getScrollY()
3、getTranslationX() 、getTranslationY()
4、getLeft()、 getTop()、 getRight()、 getBottom()(坐标位置是否改变)
5、点击事件触发区域是否改变
6、是否会影响同层级的其他view的位置
7、超过父View是否绘制

现在主要把他们用一张表列出来:

Android 坐标系及动画移动坐标相关集合_第5张图片

稍微整理一下他们各自特点:

setTranslationX/Y

  • getX getY 会变
  • getTranslationXY会变
  • 点击事件的位置也变了但是不会超过父布局
  • 会超过边界到同级View的区域去(被覆盖或者覆盖别人)
  • 这个方法的底层实现主要是通过metrix矩阵变换来的,坐标位置没有改变(跟offset不同,它是通过坐标位置改变)

scrollTo/scrollBy

  • getScrollXY 会变
  • 点击事件还是在原位置 (跟动画类似)
  • 但是内容区域变了(如果超出自己的区域 就显示不出来)
  • 它只是内容区域的移动,本身view是不移动的
  • scrollBy的x y 是相对移动的值
  • scrollTo的x y 是绝对移动的值

offsetTopAndBottom/offsetLeftAndRight

  • 上下左右坐标会变 (主要是通过坐标位置的改变产生移动效果)
  • getXY会变
  • 点击事件的位置也变了
  • 会超过边界到别人的区域去(被覆盖或者覆盖别人)
  • 它的offY是相对移动的值

平移动画

  • 点击事件还是在原位置
  • 如果setFillAfter位置保留 但是其他任何坐标位置没有改变 再次点击从原位置重新开始移动

设置margin

  • 如果父View为wrap的话,设置margin可以移动,但是可能会对同级view造成影响(比如在linear中或者relative中有关联关系)

下面是验证过程:(前方高能,多图预警!!!!!最重要的东西都罗列在前面了,没时间不用往下看了)

Android 坐标系及动画移动坐标相关集合_第6张图片

Android 坐标系及动画移动坐标相关集合_第7张图片

默认情况log

setTranslationXY:

Android 坐标系及动画移动坐标相关集合_第8张图片

Android 坐标系及动画移动坐标相关集合_第9张图片

Android 坐标系及动画移动坐标相关集合_第10张图片

指定了父布局

Android 坐标系及动画移动坐标相关集合_第11张图片

不能超过父布局,会显示不出来

Android 坐标系及动画移动坐标相关集合_第12张图片

会超过边界到同层view的区域去(被覆盖或者覆盖别人)

  • getX getY 会变
  • getTranslationXY会变
  • 点击事件的位置也变了但是不会超过父布局
  • 会超过边界到同层view的区域去(被覆盖或者覆盖别人--取决于先后顺序)

scrollBy:

Android 坐标系及动画移动坐标相关集合_第13张图片

Android 坐标系及动画移动坐标相关集合_第14张图片

offsetTopAndBottom offsetLeftAndRight:

Android 坐标系及动画移动坐标相关集合_第15张图片

Android 坐标系及动画移动坐标相关集合_第16张图片

动画+setFillAfter(true):

Android 坐标系及动画移动坐标相关集合_第17张图片

Android 坐标系及动画移动坐标相关集合_第18张图片

margin:

Android 坐标系及动画移动坐标相关集合_第19张图片

 

Android 坐标系及动画移动坐标相关集合_第20张图片

组合


比如先多点几次 offset ,然后再margin会立马回到(原位置+margin)后的状态

Android 坐标系及动画移动坐标相关集合_第21张图片

Android 坐标系及动画移动坐标相关集合_第22张图片

说明:margin的平移效果是以view在父View中的位置和margin值决定的,是父View通过计算margin值之后,重新给你排的位置,实现的移动。当我们设置margin之后,会触发requestLayout,所以父VIew又重新给它排了位置。


如果,我先offset几次,然后再点击动画,动画会在原来的基础上,继续平移。

说明:动画不是根据位置来移动的,可能是根据一个metrix的矩阵变换来实现平移的(请指正)

如果,先scrollBy,然后再动画、offset和其他移动方法,

Android 坐标系及动画移动坐标相关集合_第23张图片

 

Android 坐标系及动画移动坐标相关集合_第24张图片


说明:其他的平移方法,都是对于view本身在做移动,而不像scrollBy只是对其内容进行平移

 

先理解一个概念:

事件:在android中,点击屏幕是时,产生的长按,点击,滑动,双击,多指操作等,构成了android中的事件响应。

如:ACTION_DOWN

        ACTION_MOVE

ACTION_UP

所有的操作事件首先必须执行的是按下操作(ACTION_DOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTION_MOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

视图:android中的所有视图都继承于View,ViewGroup也继承View。如图:


先附上自己的理解到的事件分发概要框架有些细节木有写出来,(先不要看,跳过这个私人思维导图。建议看完下面全文再来看这个不够清晰的私人思维导图):

    

      

给出一个清晰的思维导图大神博客:清晰的事件拦截图

下面是事件分发的总结:

参考自:大神博客

1、ViewGroup的事件分发机制

从上层来分析(不去搞Linux内核,硬件),与用户直接进行交互的是activity,所以,当用户点击屏幕时,发生了啥?

看了大神博客,是执行分发事件的方法,dispatchTouchEvent()


    public boolean dispatchTouchEvent(MotionEvent ev) {  
      
            //如果是按下状态就调用onUserInteraction()方法,onUserInteraction()方法  
            //是个空的方法, 我们直接跳过这里看下面的实现  
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
                onUserInteraction();  
            }  
              
            if (getWindow().superDispatchTouchEvent(ev)) {  
                return true;  
            }  
              
            //getWindow().superDispatchTouchEvent(ev)返回false,这个事件就交给Activity  
            //来处理, Activity的onTouchEvent()方法直接返回了false  
            return onTouchEvent(ev);  
    }  

这个方法中我们还是比较关心getWindow()的superDispatchTouchEvent()方法,getWindow()返回当前Activity的顶层窗口Window对象,我们直接看Window API的superDispatchTouchEvent()方法

    /** 
         * Used by custom windows, such as Dialog, to pass the touch screen event 
         * further down the view hierarchy. Application developers should 
         * not need to implement or call this. 
         * 
         */  
        public abstract boolean superDispatchTouchEvent(MotionEvent event);  
这个是个抽象方法,所以我们直接找到其子类来看看superDispatchTouchEvent()方法的具体逻辑实现,Window的唯一子类是PhoneWindow,我们就看看PhoneWindow的superDispatchTouchEvent()方法

public boolean superDispatchTouchEvent(KeyEvent event) {  
        return mDecor.superDispatcTouchEvent(event);  
}
里面直接调用DecorView类的superDispatchTouchEvent()方法,或许很多人不了解DecorView这个类,DecorView是PhoneWindow的一个final的内部类并且继承FrameLayout的,也是Window界面的最顶层的View对象。耐心看下去


先新建一个项目,取名AndroidTouchEvent,然后直接用模拟器运行项目, MainActivity的布局文件为


            xmlns:tools="http://schemas.android.com/tools"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        tools:context=".MainActivity" >  
      
                    android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:layout_centerHorizontal="true"  
            android:layout_centerVertical="true"  
            android:text="@string/hello_world" />  
      
   
 

利用hierarchyviewer工具来查看下MainActivity的View的层次结构,如下图

我们看到最顶层就是PhoneWindow$DecorView,接着DecorView下面有一个LinearLayout, LinearLayout下面有两个FrameLayout
上面那个FrameLayout是用来显示标题栏的,这个Demo中是一个TextView,当然我们还可以定制我们的标题栏,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我们自定义标题栏的布局XML文件
下面的FrameLayout是用来装载ContentView的,也就是我们在Activity中利用setContentView()方法设置的View,现在我们知道了,原来我们利用setContentView()设置Activity的View的外面还嵌套了这么多的东西


我们来理清下思路,Activity的最顶层窗体是PhoneWindow,而PhoneWindow的最顶层View是DecorView,接下来我们就看DecorView类的superDispatchTouchEvent()方法

    public boolean superDispatchTouchEvent(MotionEvent event) {  
                return super.dispatchTouchEvent(event);  
    }  

在里面调用了父类FrameLayout的dispatchTouchEvent()方法,而FrameLayout(继承于ViewGroup)中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法

    /** 
        * {@inheritDoc} 
        */  
       @Override  
       public boolean dispatchTouchEvent(MotionEvent ev) {  
           final int action = ev.getAction();  
           final float xf = ev.getX();  
           final float yf = ev.getY();  
           final float scrolledXFloat = xf + mScrollX;  
           final float scrolledYFloat = yf + mScrollY;  
           final Rect frame = mTempRect;  
      
           //这个值默认是false, 然后我们可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法  
           //来改变disallowIntercept的值  
           boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
      
           //这里是ACTION_DOWN的处理逻辑  
           if (action == MotionEvent.ACTION_DOWN) {  
            //清除mMotionTarget, 每次ACTION_DOWN都很设置mMotionTarget为null  
               if (mMotionTarget != null) {  
                   mMotionTarget = null;  
               }  
      
               //disallowIntercept默认是false, 就看ViewGroup的onInterceptTouchEvent()方法  
               if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
                   ev.setAction(MotionEvent.ACTION_DOWN);  
                   final int scrolledXInt = (int) scrolledXFloat;  
                   final int scrolledYInt = (int) scrolledYFloat;  
                   final View[] children = mChildren;  
                   final int count = mChildrenCount;  
                   //遍历其子View  
                   for (int i = count - 1; i >= 0; i--) {  
                       final View child = children[i];  
                         
                       //如果该子View是VISIBLE或者该子View正在执行动画, 表示该View才  
                       //可以接受到Touch事件  
                       if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                               || child.getAnimation() != null) {  
                        //获取子View的位置范围  
                           child.getHitRect(frame);  
                             
                           //如Touch到屏幕上的点在该子View上面  
                           if (frame.contains(scrolledXInt, scrolledYInt)) {  
                               // offset the event to the view's coordinate system  
                               final float xc = scrolledXFloat - child.mLeft;  
                               final float yc = scrolledYFloat - child.mTop;  
                               ev.setLocation(xc, yc);  
                               child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                                 
                               //调用该子View的dispatchTouchEvent()方法  
                               if (child.dispatchTouchEvent(ev))  {  
                                   // 如果child.dispatchTouchEvent(ev)返回true表示  
                                //该事件被消费了,设置mMotionTarget为该子View  
                                   mMotionTarget = child;  
                                   //直接返回true  
                                   return true;  
                               }  
                               // The event didn't get handled, try the next view.  
                               // Don't reset the event's location, it's not  
                               // necessary here.  
                           }  
                       }  
                   }  
               }  
           }  
      
           //判断是否为ACTION_UP或者ACTION_CANCEL  
           boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                   (action == MotionEvent.ACTION_CANCEL);  
      
           if (isUpOrCancel) {  
               //如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false  
            //假如我们调用了requestDisallowInterceptTouchEvent()方法来设置disallowIntercept为true  
            //当我们抬起手指或者取消Touch事件的时候要将disallowIntercept重置为false  
            //所以说上面的disallowIntercept默认在我们每次ACTION_DOWN的时候都是false  
               mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
           }  
      
           // The event wasn't an ACTION_DOWN, dispatch it to our target if  
           // we have one.  
           final View target = mMotionTarget;  
           //mMotionTarget为null意味着没有找到消费Touch事件的View, 所以我们需要调用ViewGroup父类的  
           //dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法  
           if (target == null) {  
               // We don't have a target, this means we're handling the  
               // event as a regular view.  
               ev.setLocation(xf, yf);  
               if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                   ev.setAction(MotionEvent.ACTION_CANCEL);  
                   mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               }  
               return super.dispatchTouchEvent(ev);  
           }  
      
           //这个if里面的代码ACTION_DOWN不会执行,只有ACTION_MOVE  
           //ACTION_UP才会走到这里, 假如在ACTION_MOVE或者ACTION_UP拦截的  
           //Touch事件, 将ACTION_CANCEL派发给target,然后直接返回true  
           //表示消费了此Touch事件  
           if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
               final float xc = scrolledXFloat - (float) target.mLeft;  
               final float yc = scrolledYFloat - (float) target.mTop;  
               mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               ev.setAction(MotionEvent.ACTION_CANCEL);  
               ev.setLocation(xc, yc);  
                 
               if (!target.dispatchTouchEvent(ev)) {  
               }  
               // clear the target  
               mMotionTarget = null;  
               // Don't dispatch this event to our own view, because we already  
               // saw it when intercepting; we just want to give the following  
               // event to the normal onTouchEvent().  
               return true;  
           }  
      
           if (isUpOrCancel) {  
               mMotionTarget = null;  
           }  
      
           // finally offset the event to the target's coordinate system and  
           // dispatch the event.  
           final float xc = scrolledXFloat - (float) target.mLeft;  
           final float yc = scrolledYFloat - (float) target.mTop;  
           ev.setLocation(xc, yc);  
      
           if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
               ev.setAction(MotionEvent.ACTION_CANCEL);  
               target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               mMotionTarget = null;  
           }  
      
           //如果没有拦截ACTION_MOVE, ACTION_DOWN的话,直接将Touch事件派发给target  
           return target.dispatchTouchEvent(ev);  
       }  

这个方法相对来说还是蛮长,不过所有的逻辑都写在一起,看起来比较方便,接下来我们就具体来分析一下


我们点击屏幕上面的TextView来看看Touch是如何分发的,先看看ACTION_DOWN

在DecorView这一层会直接调用ViewGroup的dispatchTouchEvent(), 先看18行,每次ACTION_DOWN都会将mMotionTarget设置为null, mMotionTarget是什么?我们先不管,继续看代码,走到25行,  disallowIntercept默认为false,我们再看ViewGroup的onInterceptTouchEvent()方法

public boolean onInterceptTouchEvent(MotionEvent ev) {  
          return false;  
}  

直接返回false, 继续往下看,循环遍历DecorView里面的Child,从上面的MainActivity的层次结构图我们可以看出,DecorView里面只有一个Child那就是LinearLayout, 第43行判断Touch的位置在不在LinnearLayout上面,这是毫无疑问的,所以直接跳到51行, 调用LinearLayout的dispatchTouchEvent()方法,LinearLayout也没有dispatchTouchEvent()这个方法,所以也是调用ViewGroup的dispatchTouchEvent()方法,所以这个方法卡在51行没有继续下去,而是去先执行LinearLayout的dispatchTouchEvent()
LinearLayout调用dispatchTouchEvent()的逻辑跟DecorView是一样的,所以也是遍历LinearLayout的两个FrameLayout,判断Touch的是哪个FrameLayout,很明显是下面那个,调用下面那个FrameLayout的dispatchTouchEvent(),  所以LinearLayout的dispatchTouchEvent()卡在51也没继续下去

继续调用FrameLayout的dispatchTouchEvent()方法,和上面一样的逻辑,下面的FrameLayout也只有一个Child,就是RelativeLayout,FrameLayout的dispatchTouchEvent()继续卡在51行,先执行RelativeLayout的dispatchTouchEvent()方法

执行RelativeLayout的dispatchTouchEvent()方法逻辑还是一样的,循环遍历 RelativeLayout里面的孩子,里面只有一个TextView, 所以这里就调用TextView的dispatchTouchEvent(), TextView并没有dispatchTouchEvent()这个方法,于是找TextView的父类View,在看View的dispatchTouchEvent()的方法之前,我们先理清下上面这些ViewGroup执行dispatchTouchEvent()的思路,我画了一张图帮大家理清下(这里没有画出onInterceptTouchEvent()方法)

                                          


这就是ViewGroup的事件分发机制,会一层一层地把事件一直分发下去,直到目标View。当然,我们也可以在中间的ViewGroup把事件消费掉(重写某些方法就可以,比如onTouchListener,onTouchEvent。返回ture即可。看完下面即可理解),这样事件的分发就会终止。

2、View的Touch事件分发机制

参考自:大神博客   

郭神博客

我们都知道,按钮的点击事件的设置。如下


    button.setOnClickListener(new OnClickListener() {  
        @Override  
        public void onClick(View v) {  
            Log.d("TAG", "onClick execute");  
        }  
    });  
如果想给这个按钮再添加一个touch事件,只需要调用:

 button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
            Log.d("TAG", "onTouch execute, action " + event.getAction());  
            return false;  
        }  
    });  
onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件。那么如果我两个事件都注册了,哪一个会先执行呢?我们来试一下就知道了,运行程序点击按钮,打印结果如下:


可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。

细心的朋友应该可以注意到,onTouch方法是有返回值的,这里我们返回的是false,如果我们尝试把onTouch方法里的返回值改成true,再运行一次,结果如下:

我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。


然后我们来看一下View中dispatchTouchEvent方法的源码:


    public boolean dispatchTouchEvent(MotionEvent event) {  
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                    mOnTouchListener.onTouch(this, event)) {  
                return true;  
            }  
            return onTouchEvent(event);  
        }  
在这个方法里面,先进行了一个判断

第一个条件mOnTouchListener就是我们调用View的setTouchListener()方法设置的

第二个条件是判断View是否为enabled的, View一般都是enabled,除非你手动设置为disabled

第三个条件就是OnTouchListener接口的onTouch()方法的返回值了,如果调用了setTouchListener()设置OnTouchListener,并且onTouch()方法返回true,View的dispatchTouchEvent()方法就直接返回true,否则就执行View的onTouchEvent() 并返回View的onTouchEvent()的值
现在你了解了View的onTouchEvent()方法和onTouch()的关系了吧,为什么Android提供了处理Touch事件onTouchEvent()方法还要增加一个OnTouchListener接口呢?我觉得OnTouchListener接口是对处理Touch事件的屏蔽和扩展作用吧,屏蔽作用就是事件必须先到达onTouch,由是否在onTouch中消费掉事件(即返回true或者false来决定是否执行onTouchEvent),我就说下扩展吧,比如我们要打印View的Touch的点的坐标,我们可以自定义一个View如下

    public class CustomView extends View {  
          
        public CustomView(Context context, AttributeSet attrs) {  
            super(context, attrs);  
        }  
      
        public CustomView(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
              
            Log.i("tag", "X的坐标 = " + event.getX() + " Y的坐标 = " + event.getY());  
              
            return super.onTouchEvent(event);  
        }  
      
    }  
也可以直接对View设置OnTouchListener接口,在return的时候调用下v.onTouchEvent()
    view.setOnTouchListener(new OnTouchListener() {  
                  
                @Override  
                public boolean onTouch(View v, MotionEvent event) {  
                      
                    Log.i("tag", "X的坐标 = " + event.getX() + " Y的坐标 = " + event.getY());  
                      
                    return v.onTouchEvent(event);  
                }  
      });  

我们再看View的onTouchEvent()方法
    public boolean onTouchEvent(MotionEvent event) {  
          final int viewFlags = mViewFlags;  
      
          if ((viewFlags & ENABLED_MASK) == DISABLED) {  
              return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
          }  
      
          //如果设置了Touch代理,就交给代理来处理,mTouchDelegate默认是null  
          if (mTouchDelegate != null) {  
              if (mTouchDelegate.onTouchEvent(event)) {  
                  return true;  
              }  
          }  
      
          //如果View是clickable或者longClickable的onTouchEvent就返回true, 否则返回false  
          if (((viewFlags & CLICKABLE) == CLICKABLE ||  
                  (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
              switch (event.getAction()) {  
                  case MotionEvent.ACTION_UP:  
                      boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                      if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                          boolean focusTaken = false;  
                          if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                              focusTaken = requestFocus();  
                          }  
      
                          if (!mHasPerformedLongPress) {  
                              removeLongPressCallback();  
      
                              if (!focusTaken) {  
                                  if (mPerformClick == null) {  
                                      mPerformClick = new PerformClick();  
                                  }  
                                  if (!post(mPerformClick)) {  
                                      performClick();  
                                  }  
                              }  
                          }  
      
                          if (mUnsetPressedState == null) {  
                              mUnsetPressedState = new UnsetPressedState();  
                          }  
      
                          if (prepressed) {  
                              mPrivateFlags |= PRESSED;  
                              refreshDrawableState();  
                              postDelayed(mUnsetPressedState,  
                                      ViewConfiguration.getPressedStateDuration());  
                          } else if (!post(mUnsetPressedState)) {  
                              mUnsetPressedState.run();  
                          }  
                          removeTapCallback();  
                      }  
                      break;  
      
                  case MotionEvent.ACTION_DOWN:  
                      if (mPendingCheckForTap == null) {  
                          mPendingCheckForTap = new CheckForTap();  
                      }  
                      mPrivateFlags |= PREPRESSED;  
                      mHasPerformedLongPress = false;  
                      postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                      break;  
      
                  case MotionEvent.ACTION_CANCEL:  
                      mPrivateFlags &= ~PRESSED;  
                      refreshDrawableState();  
                      removeTapCallback();  
                      break;  
      
                  case MotionEvent.ACTION_MOVE:  
                      final int x = (int) event.getX();  
                      final int y = (int) event.getY();  
      
                      //当手指在View上面滑动超过View的边界,  
                      int slop = mTouchSlop;  
                      if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                              (y < 0 - slop) || (y >= getHeight() + slop)) {  
                          // Outside button  
                          removeTapCallback();  
                          if ((mPrivateFlags & PRESSED) != 0) {  
                              removeLongPressCallback();  
      
                              mPrivateFlags &= ~PRESSED;  
                              refreshDrawableState();  
                          }  
                      }  
                      break;  
              }  
              return true;  
          }  
      
          return false;  
      }  
这个方法也是比较长的,我们先看第4行,如果一个View是disabled, 并且该View是Clickable或者longClickable, onTouchEvent()就不执行下面的代码逻辑直接返回true, 表示该View就一直消费Touch事件,如果一个enabled的View,并且是clickable或者longClickable的,onTouchEvent()会执行下面的代码逻辑并返回true,综上,一个clickable或者longclickable的View是一直消费Touch事件的,而一般的View既不是clickable也不是longclickable的(即不会消费Touch事件,只会执行ACTION_DOWN而不会执行ACTION_MOVE和ACTION_UP) Button是clickable的,可以消费Touch事件,但是我们可以通过setClickable()和setLongClickable()来设置View是否为clickable和longClickable。当然还可以通过重写View的onTouchEvent()方法来控制Touch事件的消费与否
我们在看57行的ACTION_DOWN, 新建一个CheckForTap,我们看看CheckForTap是什么

    private final class CheckForTap implements Runnable {  
           public void run() {  
               mPrivateFlags &= ~PREPRESSED;  
               mPrivateFlags |= PRESSED;  
               refreshDrawableState();  
               if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
                   postCheckForLongClick(ViewConfiguration.getTapTimeout());  
               }  
           }  
       }  

原来是个Runnable对象,然后使用Handler的post方法延时ViewConfiguration.getTapTimeout()执行CheckForTap的run()方法,在run方法中先判断view是否longClickable的,一般的View都是false, postCheckForLongClick(ViewConfiguration.getTapTimeout())这段代码就是执行长按的逻辑的代码,只有当我们设置为longClickble才会去执行postCheckForLongClick(ViewConfiguration.getTapTimeout()),这里我就不介绍了
由于考虑到文章篇幅的问题,我就不继续分析View的长按事件和点击事件了,在这里我直接得出结论吧

长按事件是在ACTION_DOWN中执行,点击事件是在ACTION_UP中执行,要想执行长按事件,这个View必须是longclickable的, 也许你会纳闷,一般的View不是longClickable为什么也会执行长按事件呢?我们要执行长按事件必须要调用setOnLongClickListener()设置OnLongClickListener接口,我们看看这个方法的源码

    public void setOnLongClickListener(OnLongClickListener l) {  
         if (!isLongClickable()) {  
             setLongClickable(true);  
         }  
         mOnLongClickListener = l;  
     }  
如果这个View不是longClickable的,我们就调用setLongClickable(true)方法设置为longClickable的,所以才会去执行长按方法onLongClick();

要想执行点击事件,这个View就必须要消费ACTION_DOWN和ACTION_MOVE事件,并且没有设置OnLongClickListener的情况下,如果设置了OnLongClickListener的情况下,需要onLongClick()返回false才能执行到onClick()方法,也许你又会纳闷,一般的View默认是不消费touch事件的,这不是和你上面说的相违背嘛,我们要向执行点击事件必须要调用setOnClickListener()来设置OnClickListener接口,我们看看这个方法的源码就知道了

    public void setOnClickListener(OnClickListener l) {  
         if (!isClickable()) {  
             setClickable(true);  
         }  
         mOnClickListener = l;  
     }  

所以说一个enable的并且是clickable的View是一直消费touch事件的,所以才会执行到onClick()方法


对于View的Touch事件的分发机制算是告一段落了,从上面我们可以得出TextView的dispatchTouchEvent()的返回false的,即不消费Touch事件。我们就要往上看RelativeLayout的dispatchTouchEvent()方法的51行,由于TextView.dispatchTouchEvent()为false, 导致mMotionTarget没有被赋值,还是null, 继续往下走执行RelativeLayout的dispatchTouchEvent()方法, 来到第84行, 判断target是否为null,这个target就是mMotionTarget,满足条件,执行92行的 super.dispatchTouchEvent(ev)代码并返回, 这里调用的是RelativeLayout父类View的dispatchTouchEvent()方法,由于RelativeLayout没有设置onTouchListener, 所以这里直接调用RelativeLayout(其实就是View, 因为RelativeLayout没有重写onTouchEvent())的onTouchEvent()方法 由于RelativeLayout既不是clickable的也是longClickable的,所以其onTouchEvent()方法false, RelativeLayout的dispatchTouchEvent()也是返回false,这里就执行完了RelativeLayout的dispatchTouchEvent()方法

继续执行FrameLayout的dispatchTouchEvent()的第51行,由于RelativeLayout.dispatchTouchEvent()返回的是false, 跟上面的逻辑是一样的, 也是执行到92行的super.dispatchTouchEvent(ev)代码并返回,然后执行FrameLayout的onTouchEvent()方法,而FrameLayout的onTouchEvent()也是返回false,所以FrameLayout的dispatchTouchEvent()方法返回false,执行完毕FrameLayout的dispatchTouchEvent()方法

在上面的我就不分析了,大家自行分析一下,跟上面的逻辑是一样的,我直接画了个图来帮大家理解下(这里没有画出onInterceptTouchEvent()方法)

                           

所以我们点击屏幕上面的TextView的事件分发流程是上图那个样子的,表示Activity的View都不消费ACTION_DOWN事件,所以就不能在触发ACTION_MOVE, ACTION_UP等事件了,具体是为什么?我还不太清楚,毕竟从Activity到TextView这一层是分析不出来的,估计是在底层实现的。

但如果将TextView换成Button,流程是不是还是这个样子呢?答案不是,我们来分析分析一下,如果是Button , Button是一个clickable的View,onTouchEvent()返回true, 表示他一直消费Touch事件,所以Button的dispatchTouchEvent()方法返回true, 回到RelativeLayout的dispatchTouchEvent()方法的51行,满足条件,进入到if方法体,设置mMotionTarget为Button,然后直接返回true, RelativeLayout的dispatchTouchEvent()方法执行完毕, 不会调用到RelativeLayout的onTouchEvent()方法

然后到FrameLayout的dispatchTouchEvent()方法的51行,由于RelativeLayout.dispatchTouchEvent()返回true, 满足条件,进入if方法体,设置mMotionTarget为RelativeLayout,注意下,这里的mMotionTarget跟RelativeLayout的dispatchTouchEvent()方法的mMotionTarget不是同一个哦,因为他们是不同的方法中的,然后返回true

同理FrameLayout的dispatchTouchEvent()也是返回true, DecorView的dispatchTouchEvent()方法也返回true, 还是画一个流程图(这里没有画出onInterceptTouchEvent()方法)给大家理清下

                            

从上面的流程图得出一个结论,Touch事件是从顶层的View一直往下分发到手指按下的最里面的View,如果这个View的onTouchEvent()返回false,即不消费Touch事件,这个Touch事件就会向上找父布局调用其父布局的onTouchEvent()处理,如果这个View返回true,表示消费了Touch事件,就不调用父布局的onTouchEvent()

接下来我们用一个自定义的ViewGroup来替换RelativeLayout,自定义ViewGroup代码如下

    package com.example.androidtouchevent;  
      
    import android.content.Context;  
    import android.util.AttributeSet;  
    import android.view.MotionEvent;  
    import android.widget.RelativeLayout;  
      
    public class CustomLayout extends RelativeLayout {  
          
        public CustomLayout(Context context, AttributeSet attrs) {  
            super(context, attrs, 0);  
        }  
      
        public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
            return super.onTouchEvent(event);  
        }  
      
        @Override  
        public boolean onInterceptTouchEvent(MotionEvent ev) {  
            return true;  
        }  
          
      
    }  

我们就重写了onInterceptTouchEvent(),返回true, RelativeLayout默认是返回false, 然后再CustomLayout布局中加一个Button ,如下图
                                      
我们这次不从DecorView的dispatchTouchEvent()分析了,直接从CustomLayout的dispatchTouchEvent()分析

我们先看ACTION_DOWN 来到25行,由于我们重写了onInterceptTouchEvent()返回true, 所以不走这个if里面,直接往下看代码,来到84行, target为null,所以进入if方法里面,直接调用super.dispatchTouchEvent()方法, 也就是View的dispatchTouchEvent()方法,而在View的dispatchTouchEvent()方法中是直接调用View的onTouchEvent()方法,但是CustomLayout重写了onTouchEvent(),所以这里还是调用CustomLayout的onTouchEvent(), 这个方法返回false, 不消费Touch事件,所以不会在触发ACTION_MOVE,ACTION_UP等事件了,这里我再画一个流程图吧(含有onInterceptTouchEvent()方法的)


                       

好了,就分析到这里吧,差不多分析完了,还有一种情况没有分析到,例如我将CustomLayout的代码改成下面的情形,Touch事件又是怎么分发的呢?

    package com.example.androidtouchevent;  
      
    import android.content.Context;  
    import android.util.AttributeSet;  
    import android.view.MotionEvent;  
    import android.widget.RelativeLayout;  
      
    public class CustomLayout extends RelativeLayout {  
          
        public CustomLayout(Context context, AttributeSet attrs) {  
            super(context, attrs, 0);  
        }  
      
        public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
            return super.onTouchEvent(event);  
        }  
      
        @Override  
        public boolean onInterceptTouchEvent(MotionEvent ev) {  
            if(ev.getAction() == MotionEvent.ACTION_MOVE){  
                return true;  
            }  
            return super.onInterceptTouchEvent(ev);  
        }  
          
      
    }  
我的理解是:当事件来到CustomLayout时,一般来说,onInterceptTouchEvent方法是直接返回false的,现在就多了一些条件而已,即是当用户ACTION_DOWN之后,继续在屏幕ACTION_MOVE的时候(在滑动,还没抬起),就返回true,也就是说当前的CustomLayout把事件拦截了下来,其中的onTouchEvent方法里面的逻辑会得到执行,事件不再向后分发。
附上关于onInterceptTouchEvent,onTouchEvent事件分发图解:大神博客

(一个重要结论,哪里onTouchEvent返回了true,哪里的各种action就会得到执行,其他的就不会执行各种action逻辑,如果返回false,则执行父类的onTouchEvent。如有误,望不惜赐教)


附上大神图解:点击打开链接

这篇文章的篇幅有点长,如果你想了解Touch事件的分发机制,你一定要认真看完,下面来总结一下吧

1.Activity的最顶层Window是PhoneWindow,PhoneWindow的最顶层View是DecorView

2.一个clickable或者longClickable的View会永远消费Touch事件,不管他是enabled还是disabled的

3.View的长按事件是在ACTION_DOWN中执行,要想执行长按事件该View必须是longClickable的,并且不能产生ACTION_MOVE

4.View的点击事件是在ACTION_UP中执行,想要执行点击事件的前提是消费了ACTION_DOWN和ACTION_MOVE,并且没有设置OnLongClickListener的情况下,如设置了OnLongClickListener的情况,则必须使onLongClick()返回false

5.如果View设置了onTouchListener了,并且onTouch()方法返回true,则不执行View的onTouchEvent()方法,也表示View消费了Touch事件,返回false则继续执行onTouchEvent()

6.Touch事件是从最顶层的View一直分发到手指touch的最里层的View,如果最里层View消费了ACTION_DOWN事件(设置onTouchListener,并且onTouch()返回true 或者onTouchEvent()方法返回true)才会触发ACTION_MOVE,ACTION_UP的发生,如果某个ViewGroup拦截了Touch事件,则Touch事件交给ViewGroup处理

7.Touch事件的分发过程中,如果消费了ACTION_DOWN,而在分发ACTION_MOVE的时候,某个ViewGroup拦截了Touch事件,就像上面那个自定义CustomLayout,则会将ACTION_CANCEL分发给该ViewGroup下面的Touch到的View,然后将Touch事件交给ViewGroup处理,并返回true
 

 

更多相关文章

  1. 如何在eclipse的android工程里引用android sdk之外的类和方法
  2. 一张图带你掌握Android Q上InputDispatcher事件分发流程(系统层)
  3. android studio在模拟器上的中文乱码问题解决方法
  4. Android5.x+ 格式化外部存储(u盘, sdcard)的方法
  5. Android 入门第八讲02-WebView的高级用法(Android调用 JS 代码( lo

随机推荐

  1. Android(安卓)MapView 申请apiKey
  2. android:windowSoftInputMode属性使用
  3. Android开发错误——Android(安卓)Studio
  4. [置顶] android中自动匹配框AutoComplete
  5. android服务器问题
  6. ScrollView的使用
  7. android用户界面之TabHost教程实例汇总
  8. widget(2、EditText)
  9. android9.0修复无锁屏时休眠后马上亮屏,ui
  10. android双击返回键退出程序