View 是用户交互的基本组件。一个 View 占据了屏幕上的一个方形区间,能够绘制图像并处理事件。View 是 UI 的基础,我们前面看的 TextView , Button , LinearLayout , RelativeLayout 其实都是 View 的子类。 子类 ViewGroup 是各种 layout 的基类。 ViewGroup 可装载 View 和其它 ViewGroup 。
使用 View
屏幕上的所有 view 都同属于1棵树(tree)。可以用代码来添加 view 或者在 layout 的 xml 文件中指定。 View的子类有很多,可显示文字( TextView )、图片( ImageView )、网页( WebView)等。
view 的树形结构例子
文章图片
有一些通用的操作:
- 设置属性:比如给TextView设置文字内容。每种子类都有不同的方法。在xml中也可以指定view的内容。
- 设置关注:framework会处理用户输入时的移动关注。要强行关注某个view,请调用
requestFocus()
。 - 设置监听器:View允许客户端设置一些监听器。例如所有的view都能设置监听器去监听focus事件。 开发者可以用
setOnFocusChangeListener(android.view.View.OnFocusChangeListener)
来设置focus事件监听。 也可监听点击事件。 - 设置是否可见:用
setVisibility(int)
方法显示或隐藏view
不要主动调 measure layout draw 的相关方法View 的回调函数 我们关注过 Activity , Service 等等组件的回调函数(生命周期)。这里介绍 View 的回调函数。
- ndroid framework 会处理 View 的测量(measure),布局(layout)和绘制(draw)工作。 开发者不需要主动调用相关的方法。除非是自定义了ViewGroup。
分类 | 方法 | 说明 |
---|---|---|
创建 | 构造器 | 用代码创建View用的构造器和加载layout中的View用的构造器不同。从layout加载View时,会解析xml中定义的一些参数。 |
onFinishInflate() |
从xml中加载view,当它以及它的所有子view都加载完成时走这个函数。 | |
Layout | onMeasure(int, int) |
view和它的子view要决定尺寸的时候调用。 |
onLayout(boolean, int, int, int, int) |
view给它所有的子view指定尺寸和位置。 | |
onSizeChanged(int, int, int, int) |
当view的尺寸发生变化时走这个方法。 | |
Drawing | onDraw(android.graphics.Canvas) |
view绘制它的内容时走这个方法。 |
Event processing | onKeyDown(int, android.view.KeyEvent) |
当key事件发生时走这个方法。 |
onKeyUp(int, android.view.KeyEvent) |
key up 事件发生 | |
onTrackballEvent(android.view.MotionEvent) |
光标球移动事件 | |
onTouchEvent(android.view.MotionEvent) |
接收到了触摸事件 | |
Focus | onFocusChanged(boolean, int, android.graphics.Rect) |
view获得或失去关注(focus)时调用 |
onWindowFocusChanged(boolean) |
view所在的window获得或失去关注(focus)时调用 | |
Attaching | onAttachedToWindow() |
当view关联到window时调用 |
onDetachedFromWindow() |
当view与window解除关联时调用 | |
onWindowVisibilityChanged() |
当view关联的window可见性变化时调用 |
在
onMeasure
方法中 View 会对其所有的子元素执行 measure 过程,此时 measure 过程就从父容器“传递”到了子元素中,接着子元素会递归的对其子元素进行 measure 过程,如此反复完成对整个 View 树的遍历。onLayout 与 onDraw 过程的执行流程与此类似。measure 过程决定了 View 的测量宽高,这个过程结束后,就可以通过
getMeasuredHeight
和getMeasuredWidth
获得 View 的测量宽高了。layout 过程决定了 View 在父容器中的位置和 View 的最终显示宽高,
getTop
等方法可获取 View 的 top 等四个位置参数(View的左上角顶点的坐标为(left, top), 右下角顶点坐标为(right, bottom))。 getWidth
和getHeight
可获得 View 的最终显示宽高(width = right - left;height = bottom - top)
。draw 过程决定了 View 最终显示出来的样子,此过程完成后,View 才会在屏幕上显示出来。
自定义一个类继承View,放到activity的layout中,打印log观察各个函数调用情况
ActivityonCreate
ViewLifeView(Context context, @Nullable AttributeSet attrs)
ViewonFinishInflate
ActivityonStart
ActivityonResume
ViewonAttachedToWindow
ViewonMeasure
ViewonSizeChanged
ViewonLayout
ViewonDraw
ViewonWindowFocusChangedtrue
ViewonMeasure
ViewonLayout
ViewonDraw
ActivityonPause
ViewonWindowFocusChangedfalse
ActivityonStop
ActivityonDestroy
可以看出,在 activity 可见(
onResume
)后,view 与 window 关联起来。 先测量,决定自身大小,决定在父容器中的位置和大小,然后绘制到屏幕上。 根据上面的 log,我们也可以看出 View 的主要绘制流程是 measure、layout、draw。onDraw 和 invalidate() 连续多次调用
invalidate()
,onDraw
的执行次数是多少次。[act] 多次 invalidate
View生命周期 onDraw, 1535337048483
View生命周期 onDraw, 1535337048499
View生命周期 onDraw, 1535337048516
View生命周期 onDraw, 1535337048532
View生命周期 onDraw, 1535337048549
从例子中可看出,
onDraw
执行的间隔大约是 16ms。和 Android 刷新界面的时间间隔接近。再看
invalidate()
方法注释,大意是当 view 可见时,onDraw
方法将在未来某个时间点被调用。/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* ....
*/
public void invalidate() {
invalidate(true);
}
也就是说,调用了
invalidate()
方法后,并不保证会立即执行 onDraw
。ID View 可能会有一个与之关联的数字 id。通常来说这些是在 layout xml 文件中分配的 id。 常见用法是:
定义一个 Button 在 layout 中并且分配一个 id
在 Activity 的
onCreated
方法里通过 id 找到这个 ButtonButton myButton = findViewById(R.id.my_button);
view 的 ID 并不需要全局独一无二的,而是要在它所属的树里是唯一的。
位置 Position 一个 view 占据一个方形的位置。view 的位置用左上角的点来表示。位置和尺寸的单位是像素(pixel)。 调用
getLeft()
和 getTop()
可以获得 view 的坐标。getLeft()
返回 view 的 left 值,或者说是x值。getTop()
返回top值,或者说是y值。这些方法返回的坐标是 view 在它的父 view 中的位置。例如,假设getLeft()
返回20,表示这个view在它父view左边缘往右20个像素的位置。另外,
getRight()
方法能返回 view 的 right 值。getBottom()
方法返回 bottom 值。 getRight() == getLeft() + getWidth()
Size, padding 和 margins view 的尺寸(size)有宽(width)和高(height)。一个 view 实际上有两对宽高值。
第一对宽高是测量宽高(measured width/height)。这个尺寸表示一个 view 想要在父 view 里要多大。通过
getMeasuredWidth()
和getMeasuredHeight()
方法可以得到测量宽高。第二对宽高可以理解为实际宽高。这个宽高有可能与测量宽高不同。通过
getWidth()
和 getHeight()
可拿到宽高值。为了测量尺寸,view 需要考虑 padding 值。padding 值的单位是像素(px),分为左上右下(left,top,right,bottom)。 padding可用于将view的内容偏移特定数量的像素。 例如,左padding为2像素的时候,会把view的内容从左向右推2个像素。 可以用
setPadding(int, int, int, int)
或者setPaddingRelative(int, int, int, int)
方法设置padding值。 用getPaddingLeft()
, getPaddingTop()
, getPaddingRight()
, getPaddingBottom()
, getPaddingStart()
, getPaddingEnd()
获取对应的padding值。view可以设定padding值,但没有margin值。ViewGroup能支持margin值。
setX 与 setTranslationX的区别 首先来看
getX()
与 getTranslationX()
的区别。getX()
获得view在屏幕中的x坐标。getTranslationX()
获得相对于起始位置x的差值。
getX, Y = [536.0000, 0.0000];
TranslationX,Y = [0.0000, 0.0000];
。setX
指定了view在父视图中的位置。setTranslationX
指定了相对于初始位置的位置。如果我们想让view回到初始位置,可以直接调用
setTranslationX(0)
。setTranslationX
可以应用在drawerLayout中,弹出抽屉视图时让主视图跟着移动。Layout layout(布局)有2个过程:测量(measure)过程和布局(layout)过程。测量过程在
measure(int, int)
方法里实现,view树从顶往下遍历测量一遍。在遍历过程中,每个view把尺寸信息往下传。在测量过程中,每个view都存下了它的测量值。 第二个过程在布局方法中layout(int, int, int, int)
,也是自顶向下进行的。 测量过程中,每个父view都用测量值来负责定位它的子view。当 view 的
measure()
方法执行完毕,getMeasuredWidth()
和getMeasuredHeight()
的返回值已经确定了;它的子view同理。 一个view的测量宽高必须遵守它的父view的限制。这保证了测量过程的最后,所有的父view都能接受它们子view的测量结果。一个父view可能会多次调用它的子view的measure()
方法。 例如,父view可能会传未指定尺寸给子view,来搞清楚子view想要多大的尺寸。如果子view的未固定尺寸太小或者太大,就给子view的measure
方法传确定的参数。 测量过程用2个类来表示尺寸。MeasureSpec类用来告诉父view想要的尺寸和位置。 基础的LayoutParams类描述宽高想要多大。对于宽或高,可用以下的设定:- 一个确切数字
MATCH_PARENT
,表示和父view一样大(要扣除padding值)WRAP_CONTENT
,能包含自己的内容即可(要考虑自己的padding值)
weight
这个属性。 父view传递给子view,用MeasureSpecs来传递要求。MeasureSpecs有3种模式:UNSPECIFIED
父控件对子控件不加任何束缚,子元素可以得到任意想要的大小,这种MeasureSpec一般是由父控件自身的特性决定的。比如ScrollView,它的子View可以随意设置大小,无论多高,都能滚动显示,这个时候,size一般就没什么意义。EXACTLY
父view给一个确定的尺寸数值给子view。希望子View完全按照自己给定尺寸来处理。AT_MOST
父view给一个最大值,希望子view不能超出限制。
- 来了一个事件,分发到对应的 view。这个 view 处理事件,并通知相关的监听器。
- 如果在处理事件过程中,view的边界需要变化,会调用
requestLayout()
。 - 类似的,如果view的内容发生变化,会调用
invalidate()
。 - 调用
requestLayout()
或者invalidate()
,framework会在恰当时间处理好测量,布局和绘制的过程。
UI 线程:触摸事件描述 触摸事件分发
处理 view 的线程叫做 UI 线程。必须在 UI 线程操作 view。 如果在其他线程里想要操作 view,可以考虑使用 Handler
由根视图向子 view 分发。
onInterceptTouchEvent
方法(ViewGroup才有)的返回值决定是否拦截触摸事件(true:拦截,false:不拦截)。 如果 ViewGroup 拦截了触摸事件,那么其 onTouchEvent
就会被调用用来处理触摸事件。 分发的过程中,ViewGroup可以拦截事件,不再继续分发。触摸事件消费
onTouchEvent
方法的返回值决定是否处理完成触摸事件- true:已经处理完成,不需要给父 ViewGroup 处理
- false:还没处理完成 ,需要传递给父 ViewGroup 处理
onTouch
先于onTouchEvent
,mOnTouchListener
可以拦住onTouchEvent
; 有mOnTouchListener
,则执行mOnTouchListener.onTouch
方法。 onTouchEvent
获取手机屏幕的触摸事件。View 相关面试题 1. View 绘制流程
触发 addView 流程:
文章图片
2. MeasureSpec 是什么?
MeasureSpec表示的是一个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。
根据父容器的 MeasureSpec 和子 View 的 LayoutParams 等信息计算子 View 的MeasureSpec
文章图片
4. 自定义 Viewwrap_content 不起作用的原因
因为 onMeasure()->getDefaultSize(),当 View 的测量模式是 AT_MOST 或EXACTLY 时,View 的大小都会被设置成子 View MeasureSpec 的 specSize。
public static int getDefaultSize(int size, int measureSpec) {
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
5. 为什么 onCreate 获取不到 View 的宽高
Activity 在执行完 oncreate,onResume 之后才创建 ViewRootImpl,ViewRootImpl 进行 View 的绘制工作调用链
startActivity -> ActivityThread.handleLaunchActivity -> onCreate -> 完成 DecorView 和 Activity 的创建 -> handleResumeActivity -> onResume() -> DecorView 添加到WindowManager -> ViewRootImpl.performTraversals()方法,测量(measure),布局(layout),绘制(draw), 从 DecorView 自上而下遍历整个 View 树。
Android零基础入门教程视频参考