Android面试攻略(3)——View相关

系列文章
Android面试攻略(1)——Android基础
Android面试攻略(2)——异步消息处理机制
Android面试攻略(3)——View相关


本篇文章主要涉及View树的绘制流程,事件分发机制。

View树的绘制流程

我们知道,当Activity接收到用户的触摸焦点的时候,它就会被请求去绘制布局。请求其实是在android的Framework层去绘制的,它从根节点开始对布局进行测量和绘制。整个View树的绘图流程是在ViewRoot.Java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为:根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘(draw)。其框架过程如下:


图片来自网络

接下来温习一下整个View树的结构,对每个具体View对象的操作,其实就是个递归的实现。


图片来自网络

measuer

一、MeasureSpec

android给我们提供了一个设计短小却功能强大的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。
测量的模式可以为以下三种:
EXACTLY:即精确值模式,当我们将空间的layout_width属性或layout_height属性指定为具体数值时,比如android:layout_width='100dp',或者指定为match_parent属性时(占据父View的大小),系统使用的是EXACTLY模式。
AT_MOST:即最大值模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子控件或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
UNSPECIFIED:这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大,通常情况下载绘制自定义View时才会使用。
View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。控件可以响应你指定的具体宽高值或者是match_parent属性,而如果要让自定义View支持wrap_content属性,那么久必须重写onMeasure()方法来指定wrap_content时的大小。


二、measuer()过程

ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调
View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:

  1. 设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth) ;
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  1. 如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。整个measure调用流程就是个树形的递归过程。

Android中还有一个特殊的机制,就是当父视图认为子视图给它传递的宽高有异常,它会再次请求子视图去进行测量,如果子视图传递的宽高超过了父视图的约束范围,则父视图会使用一个确切的大小,给子视图设置成AT_MOST或者EXACTLY的形式,再次对子视图进行测量。


三、layout()过程

主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。
具体的调用链如下: host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下

  1. layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局);
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
  1. 如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以看到ViewGroup类中定义了onLayout这个抽象方法,说明这是个必须被重写的方法。


四、draw()过程

由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。
调用流程 :mView.draw()开始绘制,draw()方法实现的功能如下:
1 . 绘制该View的背景
2 . 为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)

  1. 调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
  2. 调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法),dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。

两个容易混淆的方法
invalidate()方法
说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。 一般引起invalidate()操作的函数如下:

  1. 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
  2. setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
  3. setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
  4. setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。

requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。
说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。 一般引起invalidate()操作的函数如下:

  1. 直接调用requestLayout()方法。
  2. setVisibility()方法:
    当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

事件分发机制

一、为什么会有事件分发机制

Android上面的View是树形结构的,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这一问题,就有了事件分发机制。


二、三个重要的事件分发的方法

1. dispatchTouchEvent(事件分发)
当有监听到事件时,首先由Activity进行捕获,进入事件分发处理流程。(因为activity没有事件拦截,View和ViewGroup有)会将事件传递给最外层View的dispatchTouchEvent(MotionEvent ev)方法,该方法对事件进行分发。

  • return true :表示该View内部消化掉了所有事件。
  • return false :事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费(如果本层控件已经是Activity,那么事件将被系统消费或处理)。
  • 如果事件分发返回系统默认的 super.dispatchTouchEvent(ev),事件将分发给本层的事件拦截onInterceptTouchEvent 方法进行处理。

2. onInterceptTouchEvent(事件拦截)

  • return true :表示将事件进行拦截,并将拦截到的事件交由本层控件 的 onTouchEvent 进行处理;
  • return false :则表示不对事件进行拦截,事件得以成功分发到子View。并由子View的dispatchTouchEvent进行处理。
  • 如果返回super.onInterceptTouchEvent(ev),默认表示拦截该事件,并将事件传递给当前View的onTouchEvent方法,和return true一样。

3. onTouchEvent(事件响应)
在dispatchTouchEvent(事件分发)返回super.dispatchTouchEvent(ev)并且onInterceptTouchEvent(事件拦截返回true或super.onInterceptTouchEvent(ev)的情况下,那么事件会传递到onTouchEvent方法,该方法对事件进行响应。

  • 如果return true,表示onTouchEvent处理完事件后消费了此次事件。此时事件终结;
  • 如果return fasle,则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理;
  • 如果return super.onTouchEvent(ev),则表示不响应事件,结果与return false一样。

实例
首先我们重写View中的两个方法

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("lqh", "View onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d("lqh", "View dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }

再重写ViewGroup中的三个方法

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("lqh", "ViewGroup dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("lqh", "ViewGroup onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("lqh", "ViewGroup onTouchEvent");
        return super.onTouchEvent(event);
    }

可以看到,ViewGroup级别比较高,比View多一个方法——onInterceptTouchEvent。这个方法是事件拦截的核心方法。接下来看下我们的布局结构:



我们不修改方法的任何返回值,点击view,然后看Log的输出:

19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onInterceptTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB onInterceptTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: View dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: View onTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB onTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onTouchEvent

可以看见,正常情况下,事件的传递顺序是:
ViewGroupA→ViewGroupB→View
事件的处理顺序是:
View→ViewGroupB→ViewGroupA

在事件传递中,我们只关心onInterceptTouchEvent()方法,而dispatchTouchEvent()方法虽然是事件分发的第一步,但一般情况下,我们不太会去改写这个方法。现在我们修改一下ViewGroupA的onInterceptTouchEvent()的方法,让它的返回值变为true,再来看一下Log:

19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onInterceptTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onTouchEvent

可以看到,ViewGroupA把所有事件都消费了,不再往后面分发事件。同理,我们修改一下ViewGroupB的onInterceptTouchEvent()方法,Log就会是下面这样:

19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onInterceptTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB dispatchTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB onInterceptTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupB onTouchEvent
19750-19750/com.lqh.layoutdemo D/lqh: ViewGroupA onTouchEvent

对onInterceptTouchEvent()有了相应的了解后,那onTouchEvent()其实也是同样的道理,相应的View或者ViewGroup返回ture后,事件将不再向上层ViewGroup传递事件,这里就不再撰述了。


参考资料
Android View树的绘制流程
View事件分发机制源码分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容