View 和 ViewGroup
View 是 Android 中最基本的 UI 组件,在屏幕上绘制一块矩形区域。
ViewGroup 是一种特殊的 View,它可以包含多个子 View 和子 ViewGroup,用于放置、组织、管理视图结构。
常用控件和布局的继承结构:
LinearLayout 和 RelativeLayout 性能对比
Android Project 默认生成的 avtivity_main.xml 布局文件中,根结点使用 RelativeLayout,然而作为顶级 View 的 DecorView 则是垂直方向的 LinearLayout,从上至下分为标题栏、内容栏。常用的 setContentView()
方法就是为内容栏设置布局。
- RelativeLayout 的子 View 需要两次
onMeasure()
过程,而 LinearLayout 只需一次,但是当 LinearLayout 设置 weight 属性后,同样需要两次onMeasure()
过程。 - 在不影响层级深度的情况下,推荐使用 Linearlayout 而非 RelativeLayout。
DecorView 层级深度已知且固定,标题栏与内容栏,采用 RelativeLayout 并不会降低层级深度,因此使用 LinearLayout 效率更高。
Project 默认使用 RelativeLayout 作为根结点,是希望开发者能够尽量减少 View 层级结构,避免使用 LinearLayout 多层嵌套完成布局。
LayoutInflater
LayoutInflater inflate()
方法用于动态加载布局,将 XML 布局文件实例化为其对应的 View 对象。
public View inflate(int resource, ViewGroup root) // 方法一
public View inflate(int resource, ViewGroup root, boolean attachToRoot) // 方法二
inflate() 方法参数详解
- resource(int): 需要加载的 XML 布局资源的 ID
- root(ViewGroup): 设置加载的布局的父级层次结构
- attachToRoot(boolean): 是否将加载的布局附加到父级层次结构
情况一: root 为 null;
如果 root 为 null,attachToRoot 参数将失去意义。
无需将 resource 指定的布局添加到 root 中,同时没有任何 ViewGroup 容器来协助 resource 指定的布局的根元素生成布局参数 LayoutParams。
情况二: root 不为 null,attachToRoot 为 true;
将 resource 指定的布局添加到 root 中,inflate()
方法返回结合后的 View,其根元素是 root。View 将会根据它的父 ViewGroup 容器的 LayoutParams 进行测量和放置。
使用方法一即未设置 attachToRoot 参数时,如果 root 不为 null,attachToRoot 参数默认为true。
情况三: root 不为 null,attachToRoot 为 false;
无需将 resource 指定的布局添加到 root 中,inflate()
方法返回 resource 指定的布局 View,根元素是自身的最外层,View 不存在父 ViewGroup,但是可以根据 root 的 LayoutParams 进行测量和放置。
情况三不解之处在于,既然 attachToRoot 为 false,无需将 resource 指定的布局添加到 root 中,那么为什么 root 仍然不为 null?创建的 View 必然包含 layout 属性,但是这些属性需要在 ViewGroup 容器中才能生效,根据 ViewGroup 容器的 LayoutParams 进行测量和放置 View。
情况三的意思是,无需将 View 添加到某个 ViewGroup 容器中,却又能根据这个 ViewGroup 容器的 LayoutParams 进行测量和放置 View。
情况一和情况三依赖手动添加 View。
多个参数版本的 inflate()
方法最终汇合调用:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
解析 XML 格式数据
Pull 解析方式基于事件驱动方式,Android 中使用 XmlPullParser 对象,调用 setInput()
方法传递数据给解析器。开始循环解析,通过 getEventType()
方法获取当前解析事件,如果当前解析事件并非 END_DOCUMENT
,调用 next()
方法获取下一个解析事件。针对不同事件处理,getName()
方法可以获得当前节点名字(tag name),nextText()
方法可以获得当前节点内的具体内容(TEXT
)。
Events(事件):
-
START_DOCUMENT
开始文档 -
START_TAG
开始标签 -
TEXT
文本内容 -
END_TAG
结束标签 -
END_DOCUMENT
结束文档
XML(可扩展标记语言)组成部分:
- Declaration(声明):XML 文件首行声明一些文档信息
。
- Tag(标签):以
<
开头,以>
结尾。包括:start-tag()、end-tag(
)、empty-element tag(
)。 - Text(文本):开始与结束 tag 之间的文本内容。
- Attribute(属性):位于开始 tag 之中,以键值对形式补充说明 tag 。
视图绘制流程
-
onMeasure()
决定 View 的大小 -
onLayout()
决定 View 在 ViewGroup 中的位置 -
onDraw()
绘制 View
onMeasure()
视图大小的测量过程,是由父视图、布局文件、以及视图本身共同完成的。
-
父视图提供参考大小(MeasureSpec: specSize, specMode)给子视图
-
UNSPECIFIED
子视图按照自身条件设置成任意的大小 -
EXACTLY
父视图希望子视图的大小应该由 specSize 来决定 -
AT MOST
子视图最大只能是 specSize 中指定的大小
-
-
布局文件中指定视图的大小
MATCH_PARENT
WRAP_CONTENT
- 视图本身最终决定大小
onLayout()
根据测量出来的(onMeasure()
)宽度和高度确定视图的位置。关键方法:public void layout (int l, int t, int r, int b)
方法接收左、上、右、下的坐标。
onDraw()
完成测量(onMeasure()
)和布局操作(onLayout()
)之后,创建 Canvas 对象绘制视图。
事件分发机制
重要方法:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
事件分发顺序:由 Activity 开始先传递给 ViewGroup 再传递给 View。
Activity 层面
事件分发始于 Activity.dispatchTouchEvent()
方法,传递事件至 Window 的根视图。
若最终没有视图消费事件则调用 Activity.onTouchEvent(event)
方法。
ViewGroup 层面
ViewGroup 中可以通过 ViewGroup.onInterceptTouchEvent()
方法拦截事件传递,返回 true 代表同一事件列不再向下传递给子 View,返回 false 代表事件继续传递,默认返回 false。
事件递归传递至子 View 的 View.dispatchTouchEvent()
方法,如果事件被子 View 消费,则返回 true,ViewGroup 将无法再处理事件。
如果没有子 View 消费事件则判断 ViewGroup 中是否存在已注册的事件监听器(mOnTouchListener),存在则调用它的 ViewGroup.OnTouchListener.onTouch()
方法,如果 onTouch()
方法返回 false 即未消费事件,则进一步去执行 ViewGroup.onTouchEvent(event)
方法。
View 层面
View.dispatchTouchEvent()
方法:首先判断 View 中是否存在已注册的事件监听器(mOnTouchListener),存在则调用它的 View.OnTouchListener.onTouch()
方法,如若 onTouch()
方法返回 false 即未消费事件,则进一步去执行 View.onTouchEvent(event)
方法。
View 可以注册事件监听器(Listener)实现 onClick(View v)、onTouch(View v, MotionEvent event)
方法。相比 onClick()
方法,onTouch()
方法能够做的事情更多,判断手指按下、抬起、移动等事件。同时注册两者事件传递顺序,onTouch()
方法将会先于 onClick()
方法执行,并且 onTouch()
方法可能执行多次(MotionEvent 事件:ACTION_DOWN
、ACTION_UP
、ACTION_MOVE
)。如若设置 onTouch()
方法返回值为 true,事件视为被 onTouch()
方法消费,不再继续向下传递给 onClick()
方法。
布局性能优化
优化布局层级
每个控件和布局都需要经过初始化、布局、绘制过程才能呈现出来。当使用多层嵌套的 LinearLayout 以致产生较深的视图层级结构,更甚者在 LinearLayout 中使用 layout_weight
参数,导致子 View 需要两次 onMeasure()
过程。如此反复执行初始化、布局、绘制过程容易造成性能问题。
需要开发者检查布局、修正布局,可以借助 Lint 工具发现布局文件中的视图层级结构里值得优化的地方,同时扁平化处理原本多层嵌套的布局,例如使用 RelativeLayout 作为根节点。
使用
通过使用
和
标签,在当前布局中嵌入另一个较大的布局作为组件,从而复用完整的布局的视图层级结构。
和
标签的区别:
标签旨在重用布局文件
layout1.xml:
layout2.xml:
最终布局视图层级结构:
标签旨在减少视图层级
layout2.xml:
最终布局视图层级结构:
懒加载 View
有时布局中包含很少使用的复杂视图,可以在需要时加载视图,减少内存使用,加快渲染速度。ViewStub 是一个轻量级视图,在构建视图层级结构中消耗资源较小。但是实际项目使用中,开发者习惯切换视图的 Visibility 而不是使用 ViewStub。
屏幕大小适配手段
使用 wrap_content
和 match_parent
,布局尽可能自适应屏幕大小。
-
wrap_content
让当前控件的大小能够刚好包含住里面的内容。 -
match_parent
让当前控件的大小和父布局的大小一样。
使用配置限定符,程序在运行时根据当前设备的配置自动加载合适的资源。
Android 常见的限定符:
屏幕特征 | 限定符 |
---|---|
大小 | small, normal, large, xlarge |
分辨率 | ldpi, mdpi, hdpi, xhdpi, xxhdpi |
方向 | land, port |
使用 Nine-Patch 图片 ,从图片资源角度,支持不同屏幕大小,Nine-Patch 图片允许指定哪些区域可以拉伸而哪些区域不可以。