[Android] 谈一下自定义View的流程

抛出

可以从:

  1. 自定义View的步骤;
    2.自定义View的注意事项;
    3.自定义ViewGroup的步骤以及注意事项;
    4.一些特殊需要注意的地方;
    以上几方面进行。

Answer1:

要想充分理解自定义View的流程,就必须对View的绘制流程有深刻理解,下面说几点:

DecorView被加载到Window中

  • ActivitystartActivity开始,最终调用到ActivityThreadhandleLaunchActivity方法来创建Activity,首先,会调用performLaunchActivity方法,内部会执行ActivityonCreate方法,从而完成DecorViewActivity的创建。然后,会调用handleResumeActivity,里面首先会调用performLaunchActivity去执行ActivityonResume()方法,执行完成后悔得到一个ActivityClientRecord对象,然后通过r.window.getDecorView()的方式得到DecorView,然后会通过a.getWIndowManager()得到WindowManager,最终调用其addView()方法将DecorView加进去。
  • WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobaladdView()方法中不仅会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView通过root.setView()DecorView加载到Window中,这里的ViewRootImplViewRoot的实现类,是连接WindowManagerDecorView的纽带。View的三大流程均是通过ViewRoot来完成的。

了解绘制的整体流程

绘制会从根视图ViewRootperformTraversals()方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。

理解MeasureSpec

MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSizeMeasureSpec是View类的一个静态内部类,它来说明应该如何测量这个View。它有三种测量模式,如下
1.EXACTLY:精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值。
2.AT_MOST:最大值测量模式,当视图的宽高指定为wrap_parent时生效, 此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
3.UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
MeasureSpec通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法, 打包方法为makeMeasureSpec,解包方法为getMode和getSize
普通ViewmeasureSpce的创建规则如下:

image

对于DecorView而言,它的measureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

View绘制流程之Measure

  • 首先,在ViewGroup中的measureChildren()方法中会遍历测量ViewGroup中所有的View,当View的可见性处于GONE状态时,不对其进行测量。
  • 然后,测量某个指定的View时,根据父容器的MeasureSpec和子ViewLayoutParams登信息计算子View的MeasureSpec。
  • 最后,将计算出的MeasureSpec传入View的measure方法,这里ViewGroup没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程中的onMeasure方法需要各个子类去实现,不同的VIewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法,(setMeasureDimension方法用于设置View的测量宽高,如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高)

getSuggestMinimumWidth分析

如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。

自定义View时手动处理wrap_content时的情形

直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent,此时,可以在wrap_content的情况下(对应MeasureSpec.AT_MOST)指定内部宽/高(mWidth和mHeight)。

LinearLayout的onMeasure方法实现解析(这里仅分析measureVertical核心源码)

系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。

在Activity中获取某个View的宽高

由于View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没有测量完毕,那么获得的宽/高就是0,所以在onCreateonStartonResume中均无法正确得到某个View的宽高信息,解决方式如下:

  • Activity/View#onWindowFocusChanged:此时View已经初始化完毕,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChange也会频繁地调用。
  • view.post(runable):通过post可以将一个runable投递到消息队列的尾部,初始化好了然后等待Looper调用此runable的时候,View也已经初始化好了。
  • ViewTreeObserver#addOnGlobalLayoutListener:当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调。
  • View.measure(int widthMeasureSpec,int heightMeasureSpec)match_parent时不知道parentSize的大小,测不出具体数值时,直接makeMeasureSpec固定值,然后调用view.measure就可以了;wrap_content时,在最大化模式下,用View理论上能支持的最大值去构造MeasureSpec是合理的。

View的绘制流程之Layout

首先,会通过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置,然后回执行onLayout空方法,子类如果是ViewGroup类型,则重写这个方法,实现VIewGroup中所有View控件布局流程。

LinearLayout的onLayout方法实现解析(layoutVertical核心代码)

其中会遍历调用每个子View的setChildFrame方法为子元素确定对应的位置。其中的childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置。
注意:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍早一些,在一些特殊情况下则两者不相等:

  • 重写View的layout方法,使最终宽度总比测量宽/高大100px。
  • View需要多次measure才能确定自己的测量宽/高,在前几次测量的过程中,其得出的测量宽/高可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

View的绘制流程之Draw

Draw的基本流程

绘制基本上可以分为六个步骤:

  • 实现绘制View的背景;
  • 如果需要的话,保存canvas的图层,为fading做准备;
  • 然后,绘制View的内容;
  • 接着,绘制View的子View;
  • 如果需要的话,绘制View的fading边缘并回复图层;
  • 最后,绘制View的装饰(例如滚动条等等);
setWillNotDraw的作用

如果一个View不需要绘制任何内容,那么设置这个标记位位true以后,系统会进行相应的优化。

  • 默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。
  • 当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。
  • 当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。

RequestLayout、onLayout、onDraw、onDrawChild区别与联系?

  • requestLayout()方法:会导致调用measure()过程和layout()过程,将会根据标记位判断是否onDraw
  • onLayout()方法:如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。
  • onDraw()方法:绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)
  • drawChild():去重新回调每个子视图的draw()方法。

invalidate()和postInvalidate()的区别?

invalidate()postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。
更详细内容查看这里

Answer2:

大多数自定义View要么在onDraw方法中画点东西,和在onTOuchEvent中处理触摸事件。

自定义View的步骤:
  • onMeasure,可以不重写,不重写的话就要在外面指定宽高,建议重写;
  • onDraw,看情况重写,如果需要画东西就要重写;
  • onTouchEvent,也是看情况,如果要做能跟手指交互的View,就重写。
自定义View注意事项:
  • 如果有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源;
  • 某些比较重量级的资源,可以重写onDetachedFormWindow方法, 并在此方法中释放;
  • onDrawonTouchEvent方法中都应尽量避免创建对象,过多操作可能会造成卡顿;

如果现有ViewGroup的排版或者行为不满足当前需求,就可以自定义ViewGroup

自定义ViewGroup的步骤:
  • onMeasure(必须),在这里测量每一个子View,还有处理自己的尺寸;
  • onLayout(必须),在这里对子View进行布局;
  • 如果有自己的触摸事件,需要重写onInterceptTouchEventonTouchEvent

自定义ViewGroup注意事项:

  • 如果想在ViewGroup中画点东西,又没有在布局中设置background的话,会画不出来,这时候需要调用setWillNotDraw方法,并设置为false;
  • 如果有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源;
  • 如果有自己的触摸事件,又不影响View的行为,需要重写onInterceptTouchEvent并在里面去判断哪些行为是自己需要的,哪些是不需要的。
  • 某些比较重量级的资源,可以重写onDetachedFromWindow方法,并在此方法中释放;

参考链接:
每日一问 | 谈一下自定义 View 的流程

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