1. 什么是View
在Android的官方文档中是这样描述的:表示了用户界面的基本构建模块。一个View占用了屏幕上的一个矩形区域并且负责界面绘制和事件处理。
手机屏幕上所有看得见摸得着的都是View。这一点对所有图形系统来说都一样,例如ios的UIView。
2. View和Activity的区别
我们之前学习过android的四大组件,Activity是四大组件中唯一一个用来和用户进行交互的组件。可以说Activity就是android的视图层。
如果再细化,Activity相当于视图层中的控制层,是用来控制和管理View的,真正用来显示和处理事件的实际上是View。
每个Activity内部都有一个Window对象, Window对象包含了一个DecorView(实际上就是FrameLayout),我们通过setContentView给Activity设置显示的View实际上都是加到了DecorView中。
3. View种类
android提供了种类丰富的View来应对各种需求,例如提供文字显示的TextView,提供点击事件的Button,提供图片显示的ImageView,还有各种布局文件,例如Relativilayout,Linearlayout等等。他们都是继承自View。
4. ViewGroup
ViewGroup继承自View,并实现了两个接口ViewParent和ViewManager。
ViewManager提供了三个抽象方法addView,removeView,updateViewLayout。用来添加、删除、更新布局。
ViewParent主要提供了一系列操作子View的方法例如焦点的切换,显示区域的控制等等。
5. 为什么要有ViewGroup?
实际上所有的事情View都能做,包括显示复杂的界面,我们只需要设计一个复杂的View即可。例如短信通知的icon,一个可以显示图片又可以显示文字的View,我们后期学习了View的draw方法后,可以轻松的设计一个View来达到这个效果,但是这样不仅复杂,而且重用性较差,还会因为一点小改动而重复的创造轮子,这显然不符合程序员偷懒的原则,所以我们可以完全把ImageView和TextView组合到一起就可以了,这个时候我们就需要一个容器,ViewGroup,来装这两个View。
ViewGroup和View最大的不同是可以组合多个View,那么多个View在一起,该如何摆放,这就是ViewGroup需要解决的问题。
6. View树
我们看到的界面,都是以一个ViewGroup作为根View,通过往ViewGroup中添加子View(可以是View,也可以是ViewGroup),来组合出各具特色的界面。
这种从根到叶的组合方式,我们可以看做成一个View树。(类似于XML),而View的显示和事件处理,都是依赖于这个View树。
绘制和事件处理的起始点,都是从根View开始一级一级的往下传递。我们从任意一层发起绘制,都将反馈到根View,然后再从上往下传递。
之前我们说过根View就是Window中的DecorView,也就是一个FrameLayout。
6.1 View树示意图
对SystemUI,也就是我们常说的StatusBar显示在哪儿呢,其实SystemUI是一个单独的App,随着系统启动而启动,将会启动一个系统级服务,接收我们提交的通知,该应用也会有一个window,并且级别比我们普通应用的window要高,所以会显示在我们的应用的外面,只不过该window的高度比较小。
7. View的测量、布局、绘制过程
整个android系统 CS架构,view被展示到界面上需要经过3个步骤
- 需要花多大:measure –> onMeasure –> setMeasuredDimension
- 画在什么地方:layout –> setFrame –> onLayout
- 怎么画:draw –> > onDraw –> dispatchDraw
7.1 显示一个View需要经过哪些步骤
- Measure测量一个View的大小
- Layout摆放一个View的位置
- Draw画出View的显示内容
其中measure和layout方法都是final的,无法重写,虽然draw不是final的,但是也不建议重写该方法。这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。
7.2 如何发起一个View树的测量/布局/绘制流程
通过调用requestLayout/requestFocus都将发起一个View树的测量。测量完毕后会进行布局,布局完毕后就会绘制。
如果View的大小没有发生改变,布局也没有变化,只是显示的内容发生了变化,则可以通过invalidate来请求绘制,此时将不会测量和布局,直接从绘制开始。
7.3 View内部的mPrivateFlags变量
View中有一个私有int变量mPrivateFlags,用于保存View的状态,int型32位,通过0/1可以保存32个状态的true或者false,采用这种方式可以有效的减少内存占用,提高运算效率。
当某一个View发起了测量请求时,将会把mPrivateFlags中的某一位从0变为1,同时请求父View,父View也会把自身的该值从0变为1,同时也将会把其他子View的值从0变为1。这样一层一层传递,最终传到到DecorView,DecorView的父View是ViewRoot,所以最终都将由ViewRoot来进行处理。
ViewRoot收到请求后,将会从上至下开始遍历,检查标记,只要有相对应的标记就执行测量/布局/绘制
当Activity被创建时,会相应的创建一个Window对象,Window对象创建时会获取应用的WindowManager(注意,这是应用的窗口管理者,不是系统的)。
Activity被创建后,会调用Activity的onCreate方法。我们通过设置setContentView就会调用到Window中的setContextView,从而初始化DecorView。
所以我们需要隐藏标题栏什么的,都需要在DecorView初始化之前进行设置。
DecorView初始化之后将会被添加到WindowManager中,同时WindowManager中会为新添加的DecorView创建一个对应的ViewRoot,并把DecorView设置给ViewRoot。
所以根View就是DecorView,因为DecorView的父亲是ViewRoot,实现自ViewParent接口,但是没有继承自View,所以根本不是一个View。
从系统的命名来看,WindowManger继承自ViewManager,而添加到WindowManager中的是DecorView,不是Window,都说明了其实真正意义上的window就是View。
在ViewRoot的构造方法中会通过getWindowSession来获取WindowManagerService系统服务的远程对象(这才是系统级的)。
当ViewRoot的setView方法中将会调用requestLayout进行第一次视图测量请求。同时sWindowSession.add自身内部的一个W对象,以此达到和WindowManagerService的关联。
W是一个Binder对象。可以实现跨进程的通信了,并且是一个双方都掌握着主动调用的跨进程通信方式。
7.4 常用的标记位
- FORCE_LAYOUT 请求绘制,将从measure开始,,并增加LAYOUT_REQUIRED标记
- 持有LAYOUT_REQUIRED标记的View将会被执行layout,完毕后会去掉LAYOUT_REQUIRED和FORCE_LAYOUT
- DRAWN带有该标签的将不会被draw,注意,这和上面两个不一致,当draw完毕后会加上该标签,当没有该标签才会被draw。
还有一些其他的标记位,大家可以自行阅读源码。
7.5 测量/布局/绘制流程
测量事件最终传递到decorView的父亲ViewRoot那里,由它的函数performTraversals来执行,听名字就知道是执行遍历了。
首先它会检测之前设置的标记为来确定是否需要测量大小,是,就会直接执行decorView的measture方法,该方法内部会测量完自身后,将会继续遍历所有子View,直到每一个设置有标记的子View都测量完。
然后它会检测是否需要布局,是,将会执行decorView的layout方法进行,该方法内部也会遍历所有设置有标记位子View。
8. measure 测量
8.1 测量流程
测量View是在measure()方法中,而measure()方法是final修饰的,不允许重写,但是在measure()方法中回调了onMeasure()方法,所以我们自定义View的时候需要重写onMeasure()方法,在该方法中实现测量的逻辑
- 如果是普通View,则直接通过setMeasureDimension()方法设置大小即可
- 如果是ViewGroup,则需要循环遍历所有子View,调用子View的measure()方法,测量每个子View的大小,等所有的子View都测量完毕,最后通过setMeasureDimension()设置ViewGroup自身的大小
8.2 LayoutParams
每个View都包含一个ViewGroup.LayoutParams类或者其派生类,LayoutParams中包含了View和它的父View之间的关系,而View大小正是View和它的父View共同决定的。
我们设置View的大小,有match_parent、wrap_content和具体的dip值。
match_parent对应值为-1、wrap_conten对应值为-2,具体dip对应其设定的值。在测量时,View的父类从Layout中读出宽高值,根据不同的值设置不同的计算模式。
布局文件中所有layout_开头的在代码中都是需要通过LayoutParams来设置。
当我们通过addView添加一个子View时,如果它没有LayoutParams或者是LayoutParams的类型不匹配,那么将会创建一个默认的LayoutParams。
通过布局文件进行layout_width,layout_height进行设定。通过代码设置,需要一个LayoutParams来描述。
View view = new View(this);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
view.setLayoutParams(lp);
8.3 measure
/**
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
measure是final修饰的方法,不可被重写。在外部调用时,直接调用view.measure(int wSpec, int hSpec)。measure中调用了onMeasure。自定义view时,重写onMeasure即可
8.4 onMeasure
measure是一个final方法,用来测量View自身的大小,View类该方法体逻辑比较简单,只是根据判断条件决定是否需要调用onMeasure。方法接受两个参数,分别就是通过MeasureSpec类合成测量模式和大小的宽与高。
实际上View的大小是无限大