你可以从:
1. 自定义 View 的步骤;
2. 自定义 View 的注意事项;
3. 自定义 ViewGroup的步骤以及注意事项;
4. 一些特殊需要注意的地方。
进行回答,回答一两点也可以。
对不错答案欢迎点赞哈,会按照点赞排序。
更多问答 >>
-
讨论 | Flutter Kotlin 如果二选一学习,你会怎么选?
2019-05-16 09:04 -
每日一问 | 并发专题 volatile,synchronize,cas,happens before, lost wake up
2019-05-19 20:37 -
2019-05-21 23:59
-
每日一问 思考一下如果捕获一个 Activity页面上所有的点击行为?
2019-05-23 00:14 -
每日一问 你有什么好的学习习惯 或者 不错的 app 推荐给大家?
2019-05-24 11:09 -
2019-05-13 08:30
-
2019-04-11 18:34
-
2019-04-09 23:44
-
2019-05-13 23:38

要想充分理解自定义View的流程,就必须对View的绘制流程有深刻理解,下面我简单说几点:
#### DecorView被加载到Window中
- 从Activity的startActivity开始,最终调用到ActivityThread的handleLaunchActivity方法来创建Activity,首先,会调用performLaunchActivity方法,内部会执行Activity的onCreate方法,从而完成DecorView和Activity的创建。然后,会调用handleResumeActivity,里面首先会调用performResumeActivity去执行Activity的onResume()方法,执行完后会得到一个ActivityClientRecord对象,然后通过r.window.getDecorView()的方式得到DecorView,然后会通过a.getWindowManager()得到WindowManager,最终调用其addView()方法将DecorView加进去。- WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobal的addView()方法中不仅会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView通过root.setView()把DecorView加载到Window中。这里的ViewRootImpl是ViewRoot的实现类,是连接WindowManager和DecorView的纽带。View的三大流程均是通过ViewRoot来完成的。#### 了解绘制的整体流程绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。#### 理解MeasureSpecMeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View。它由三种测量模式,如下:- EXACTLY:精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值。- AT_MOST:最大值测量模式,当视图的宽高指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。- UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为makeMeasureSpec,解包方法为getMode和getSize。普通View的MeasureSpec的创建规则如下:对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。#### View绘制流程之Measure- 首先,在ViewGroup中的measureChildren()方法中会遍历测量ViewGroup中所有的View,当View的可见性处于GONE状态时,不对其进行测量。- 然后,测量某个指定的View时,根据父容器的MeasureSpec和子View的LayoutParams等信息计算子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。所以在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息。解决方式如下:- Activity/View#onWindowFocusChanged:此时View已经初始化完毕,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。- view.post(runnable): 通过post可以将一个runnable投递到消息队列的尾部,始化好了然后等待Looper调用次runnable的时候,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,DrawChild区别与联系?requestLayout()方法 :会导致调用 measure()过程 和 layout()过程,将会根据标志位判断是否需要ondraw。onLayout()方法:如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。onDraw()方法:绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)。drawChild():去重新回调每个子视图的draw()方法。#### invalidate() 和 postInvalidate()的区别 ?invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。[更详细的内容请查看这里](https://jsonchao.github.io/2018/10/28/Android%20View%E7%9A%84%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B/)赞,感觉我要支持下markdown了~
我感觉没有必要,如果可以的话让回答者给链接就行了,毕竟把真正需要回答的内容放在非正式环境中还是不合适的。 这个确实可以单独起博客
期待中~
大多数自定义View要么是在onDraw方法中画点东西,和在onTouchEvent中处理触摸事件。
自定义View步骤:
自定义View注意事项:
如果现有ViewGroup的排版或者行为不满足当前需求,就可以自定义ViewGroup
自定义ViewGroup步骤:
自定义ViewGroup注意事项:
JsonChao回答的很详细了,简单聊几个需要注意的点:
1. UNSPECIFIED 这个模式一定要充分理解,了解使用场景,早期的博客很多都不了解,所以都一带而过这个模式,造成很多人通过看博客学习的,其实自己也不清楚,实际上面试我会很注意这个细节,很少有人答出来。
2. 关于LayoutParams,会涉及到好几个方法的重载,很多人不会注意到。3. View的状态存储于恢复不说了。
4. 关于自定义View + 动画,要有一定的认识。
好了,先这些。大佬请教下,第2点提到的LayoutParams,具体指什么呢?想去看源码的,没找到方向.....
一、确定继承的父类
二、重写相关方法
1. onMeasure()
1.5 onSizeChanged()
2. onLayout()
2. onDraw()
三、完善自定义属性四、自测一下有没有问题
1.步骤:onMeasure() -> onLayout() -> onDraw()
2.注意事项:padding 的设置,滑动冲突,重复绘制, 3.ViewGroup 步骤:步骤:onMeasure() -> onLayout() -> onDraw();注意事项:padding 的设置,滑动冲突,重复绘制,子 View 的各个递归调用 4.不要在 onDraw() 中创建对象,不绘制被遮挡的部分,所有可变动的属性都在 xml 可配置