转载:9102年末,我对Android view的13条认识;
(顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找)
https://github.com/xiangjiana/Android-MS
一丶 View 的绘制流程?
- 参考答案
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽/ / 高,layout 确定 View 的最终宽/ / 高和 四个顶点的位置,而 draw 则将 View绘制到屏幕上
View 的绘制过程遵循如下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己(onDraw)
- 绘制 children(dispatchDraw)
-
绘制装饰(onDrawScollBars)
二丶View的事件分发制
点击事件产生后,首先传递给 Activity 的 dispatchTouchEvent
方法,通过PhoneWindow
传递给 DecorView
,然后再传递给根 ViewGroup
,进入 ViewGroup
的dispatchTouchEvent
方法,执行 onInterceptTouchEvent
方法判断是否拦截,再不拦截的情况下,此时会遍历 ViewGroup
的子元素,进入子 View 的dispatchToucnEvent
方法,如果子 view 设置了 onTouchListener
,就执行 onTouch
方法,并根据 onTouch
的返回值为 true 还是 false 来决定是否执行 onTouchEvent
方法,如果是 false 则继续执行 onTouchEvent
。在onTouchEvent
的 Action Up 事件中判断,如果设置了 onClickListener
,就执行 onClick
方法。
View 事件传递分发机制?
参考回答:
- View 事件分发本质就是对
MotionEvent
事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上 - 点击事件的传递顺序: Activity( Window)→ViewGroup→ View
- 事件分发过程由三个方法共同完成
-
dispatchTouchEvent
:用来进行事件的分发。如果事件能够传递给当前 View,那么此方法一定会被调用,返回结果受当前 View 的onTouchEvent
和下级View 的dispatchTouchEvent
方法的影响,表示是否消耗当前事件 -
onInterceptTouchEvent
:在上述方法内部调用,对事件进行拦截。该方法只在ViewGroup
中有,View(不包含ViewGroup
)是没有的。一旦拦截,则执行ViewGroup
的onTouchEvent
,在ViewGroup
中处理事件,而不接着分发给 View。且只调用一次,返回结果表示是否拦截当前事件 -
onTouchEvent
: 在dispatchTouchEvent
方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件
-
三丶 View的加载流程
View 随着 Activity 的创建而加载,startActivity 启动一个 Activity 时,在ActivityThread
的 handleLaunchActivity
方法中会执行 Activity 的 onCreate
方法,这个时候会调用 setContentView
加载布局创建出 DecorView
并将我们的 layout加载到 DecorView
中,当执行到 handleResumeActivity
时,Activity 的 onResume
方法被调用,然后 WindowManager
会将 DecorView
设置给 ViewRootImpl
,这样,DecorView
就被加载到Window中了,此时界面还没有显示出来,还需要经过 View的 measure,layout 和 draw 方法,才能完成 View 的工作流程。我们需要知道 View的绘制是由ViewRoot
来负责的,每一个DecorView
都有一个与之关联的ViewRoot
,这种关联关系是由WindowManager
维护的,将DecorView
和 ViewRoot
关联之后,ViewRootImpl
的requestLayout
会被调用以完成初步布局,通过scheduleTraversals
方法向主线程发送消息请求遍历,最终调用ViewRootImpl
的 performTraversals
方法,这个方法会执行 View 的 measure layout 和 draw 流程
四丶 自定义view 需要注意的几点
1.让 view 支持 wrap_content 属性,在 onMeasure
方法中针对 AT_MOST 模式做专门处理,否则 wrap_content
会和 match_parent
效果一样(继承 ViewGroup
也同样要在 onMeasure
中做这个判断处理)
if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension( 200 , 200 );
// wrap_content
情况下要设置一个默认值,200 只是举个例子,最终的值需要计算得到刚好包裹内容的宽高值
} else if (widthMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension( 200 ,heightMeasureSpec );
} else if (heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(heightMeasureSpec , 200 );
}
2.让 view 支持 padding(onDraw 的时候,宽高减去 padding 值,margin 由父布局控制,不需要 view 考虑),自定义 ViewGroup
需要考虑自身的 padding 和子 view的 margin 造成的影响
3.在 view 中尽量不要使用 handler,使用 view 本身的 post 方法
4.在 onDetachedFromWindow 中及时停止线程或动画
5.view 带有滑动嵌套情形时,处理好滑动冲突
五丶View 的 measure layout 和 draw
在上边的分析中我们知道,View 绘制流程的入口在 ViewRootImpl
的performTraversals
方法,在方法中首先调用 performMeasure
方法,传入一个childWidthMeasureSpec
和 childHeightMeasureSpec
参数,这两个参数代表的是DecorView
的 MeasureSpec
值,这个 MeasureSpec
值由窗口的尺寸和 DecorView
的 LayoutParams
决定,最终调用 View 的 measure 方法进入测量流程
measure :
View 的 measure 过程由 ViewGroup
传递而来,在调用 View.measure 方法之前,会首先根据 View 自身的 LayoutParams
和父布局的MeasureSpec
确定子 view 的MeasureSpec
,然后将 view 宽高对应的 measureSpec
传递到 measure 方法中,那么子 view 的 MeasureSpec
获取规则是怎样的?分几种情况进行说明
1.父布局是 EXACTLY 模式:
a. 子 view 宽或高是个确定值,那么子 view 的 size 就是这个确定值,mode是 EXACTLY(是不是说子 view 宽高可以超过父 view?见下一个)
b. 子 view 宽或高设置为 match_parent,那么子 view 的 size 就是占满父容器剩余空间,模式就是 EXACTLY
c. 子 view 宽或高设置为 wrap_content,那么子 view 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
2.父布局是 AT_MOST 模式:
a. 子 view 宽或高是个确定值,那么子 view 的 size 就是这个确定值,mode 是EXACTLY
b. 子 view 宽或高设置为 match_parent,那么子 view 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
c. 子 view 宽或高设置为 wrap_content,那么子 view 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
3.父布局是 UNSPECIFIED 模式:
a. 子 view 宽或高是个确定值,那么子 view 的 size 就是这个确定值,mode 是EXACTLY
b. 子 view 宽或高设置为 match_parent,那么子 view 的 size 就是 0,模式就是UNSPECIFIED
c. 子 view 宽或高设置为 wrap_content,那么子 view 的 size 就是 0,模式就是UNSPECIFIED
获取到宽高的 MeasureSpec
后,传入 view 的 measure 方法中来确定 view 的宽高,这个时候还要分情况
1.当
MeasureSpec
的 mode 是 UNSPECIFIED,此时 view 的宽或者高要看 view 有没有设置背景,如果没有设置背景,就返回设置的minWidth
或minHeight
,这两个值如果没有设置默认就是 0,如果 view 设置了背景,就取minWidth
或minHeight
和背景这个drawable
固有宽或者高中的最大值返回
2.当MeasureSpec
的 mode 是 AT_MOST 和 EXACTLY,此时 view 的宽高都返回从MeasureSpec
中获取到的 size 值,这个值的确定见上边的分析。因此如果要通过继承 view 实现自定义 view,一定要重写onMeasure
方法对wrap_conten
属性做处理,否则,他的match_parent
和wrap_content
属性效果就是一样的
layout:
layout 方法的作用是用来确定 view 本身的位置,onLayout
方法用来确定所有子元素的位置,当 ViewGroup
的位置确定之后,它在 onLayout
中会遍历所有的子元素并调用其 layout 方法,在子元素的 layout 方法中 onLayout
方法又会被调用。layout 方法的流程是,首先通过 setFrame
方法确定 view 四个顶点的位置,然后view 在父容器中的位置也就确定了,接着会调用onLayout
方法,确定子元素的位置,onLayout
是个空方法,需要继承者去实现。
getMeasuredHeight
和getHeight
方法有什么区别?getMeasuredHeight
(测量高度)形成于 view 的 measure 过程,getHeight
(最终高度)形成于 layout 过程,在有些情况下,view 需要 measure 多次才能确定测量宽高,在前几次的测量过程中,得出的测量宽高有可能和最终宽高不一致,但是最终来说,还是会相同,有一种情况会导致两者值不一样,如下,此代码会导致 view 的最终宽高比测量宽高大100PX
public void layout(int l,int t,int r, int b) {
super.layout(l,t,r+100,b+100); {
}
View 的绘制过程遵循如下几步:
a.绘制背景 background.draw(canvas)
b.绘制自己(onDraw)
c.绘制 children(dispatchDraw)
d.绘制装饰(onDrawScrollBars)
View 绘制过程的传递是通过 dispatchDraw
来实现的,它会遍历所有的子元素的draw 方法,如此 draw 事件就一层一层的传递下去了
ps: view 有一个特殊的方法 setWillNotDraw
,如果一个 view 不需要绘制内容,即不需要重写 onDraw
方法绘制,可以开启这个标记,系统会进行相应的优化。默认情况下,View 没有开启这个标记,默认认为需要实现 onDraw
方法绘制,当我们继承 ViewGroup
实现自定义控件,并且明确知道不需要具备绘制功能时,可以开启这个标记,如果我们重写了 onDraw
,那么要显示的关闭这个标记
子 view 宽高可以超过父 view?能
1. android:clipChildren = "false" 这个属性要设置在父 view 上。代表其中的子View 可以超出屏幕。
2. 子 view 要有具体的大小,一定要比父 view 大 才能超出。比如 父 view 高度100px 子 view 设置高度 150px。子 view 比父 view 大,这样超出的属性才有意义。(高度可以在代码中动态赋值,但不能用 wrap_content / match_partent)。
3. 对父布局还有要求,要求使用 linearLayout(反正我用 RelativeLayout 是不行)。你如果必须用其他布局可以在需要超出的 view 上面套一个linearLayout 外面再套其他的布局。
4. 最外面的布局如果设置的 padding 不能超出
六丶MotionEvent 是什么?包含几种事件?什么条件下会产生?
参考回答:
MotionEvent 是手指接触屏幕后所产生的一系列事件。典型的事件类型有如下:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指从屏幕上松开的一瞬间
ACTION_CANCELL:手指保持按下操作,并从当前控件转移到外层控件时触发
正常情况下,一次手指触摸屏幕的行为会触发一系列点击
事件,考虑如下几种情况:
点击屏幕后松开,事件序列:DOWN→UP
点击屏幕滑动一会再松开,事件序列为DOWN→MOVE→.....→MOVE→UP
七丶如何解决View 的事件冲突
参考回答:
常见开发中事件冲突的有 ScrollView
与 RecyclerView
的滑动冲突、RecyclerView
内嵌同时滑动同一方向
滑动冲突的处理规则:
- 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
- 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View 拦截事件,何时由内部 View 拦截事件。
- 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的实现方法:
-
外部拦截法: 指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。
具体方法:需要重写父容器的onInterceptTouchEvent
方法,在内部做出相应的拦截。 -
内部拦截法: 指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。
具体方法:需要配合requestDisallowInterceptTouchEvent
方法
八丶Scroller 是怎么实现View的弹性滑动
参考回答:
- 在
MotionEvent.ACTION_UP
事件触发时调用startScroll()
方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量(滑动距离、滑动时间) - 接着调用
invalidate/postInvalidate()
方法,请求 View重绘,导致 View.draw 方法被执行 - 当 View 重绘后会在 draw 方法中调用
computeScroll
方法,而computeScroll
又会去向Scroller
获取当前的scrollX
和scrollY
;然后通过scrollTo
方法实现滑动;接着又调用postInvalidate
方法来进行第二次重绘,和之前流程一样,如此反复导致 View 不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过成结束
九丶 invalidate()和 postInvalidate() 的区别 ?
invalidate()
与 postInvalidate()
都用于刷新 View,主要区别是 invalidate()
在主线程中调用,若在子线程中使用需要配合 handler;而 postInvalidate()
可在子线程中直接调用。
十丶 SurfaceView 和 View 的区别?
- View 需要在 UI 线程对画面进行刷新,而
SurfaceView
可在子线程进行页面的刷新 - View 适用于主动更新的情况,而
SurfaceView
适用于被动更新,如频繁刷新,这是因为如果使用 View 频繁刷新会阻塞主线程,导致界面卡顿 -
SurfaceView
在底层已实现双缓冲机制,而 View 没有,因此SurfaceView
更适用于需要频繁刷新、刷新时数据处理量很大的页面(如视频播放界面)
十一丶 scrollTo()和 scollBy() 的区别?
-
scollBy
内部调用了scrollTo
,它是基于当前位置的相对滑动;而scrollTo
是绝对滑动,因此如果使用相同输入参数多次调用scrollTo
方法由于 View 的初始位置是不变的,所以只会出现一次 View 滚动的效果 - 两者都只能对 View 内容的滑动,而非使 View 本身滑动。可以使用
Scroller
有过度滑动的效果
十二丶自定义View 如何考虑机型适配?
- 合理使用 warp_content,match_parent
- 尽可能的是使用 RelativeLayout
- 针对不同的机型,使用不同的布局文件放在对应的目录下,android 会自动匹配。
- 尽量使用点 9 图片。
- 使用与密度无关的像素单位 dp,sp
- 引入 android 的百分比布局。
- 切图的时候切大分辨率的图,应用到布局当中。在小分辨率的手机上也会有很好的显示效果。
十三丶View 的滑动方式
a. layout(left,top,right,bottom)
:通过修改 View 四个方向的属性值来修改 View 的坐标,从而滑动 View
b. offsetLeftAndRight() offsetTopAndBottom()
:指定偏移量滑动 view
c. LayoutParams
,改变布局参数:layoutParams
中保存了 view 的布局参数,可以通过修改布局参数的方式滑动 view
d. 通过动画来移动 view:注意安卓的平移动画不能改变 view 的位置参数,属性动画可以
e. scrollTo/scrollBy
:注意移动的是 view 的内容,scrollBy(50,50)
你会看到屏幕上的内容向屏幕的左上角移动了,这是参考对象不同导致的,你可以看作是它移动的是手机屏幕,手机屏幕向右下角移动,那么屏幕上的内容就像左上角移动了
f. scroller:scroller
需要配置 computeScroll
方法实现 view 的滑动,scroller
本身并不会滑动 view,它的作用可以看作一个插值器,它会计算当前时间点 view 应该滑动到的距离,然后 view 不断的重绘,不断的调用 computeScroll
方法,这个方法是个空方法,所以我们重写这个方法,在这个方法中不断的从 scroller
中获取当前 view 的位置,调用 scrollTo
方法实现滑动的效果