博主从牛客等地方收集的与Android View系统相关的面试题,答案由自己整理,非标准答案,仅供参考;
View 基础
View的渲染流程(UI绘制)
View 的渲染大致分为三部分:测量、布局和绘制;
通过 ViewRootImpl#requestLayout 方法,向主线程的 Handler 发送一个异步Message并且开启同步屏障,让渲染过程能尽快执行;
异步Messahe中的Runnable会调用到 performTraversals 方法开始整颗 View 的渲染流程;
先进行测量
performMeasure,调用DecorView#measure并传递屏幕的宽高,由 DecorView 开始自上而下的测量整颗 View 树的尺寸每个View 经过测量后都存有
measureHeight和measureWidth测量结果,但这个并不是最终的宽高,最终的宽高在布局结束后确定;自定义View需要实现
onMeasure来测量自身尺寸,如果是 ViewGroup 还需要先测量子View的尺寸来确定自身的尺寸;
进行布局
performLayout,调用DecorView#layout,由 DecorView 开始自上而下的对 View 树进行布局布局是通过 left、top、right、bottom 这四个相较于父容器的相对坐标确定 View 的位置,确定了这四个值也就确定了最终的宽高;
View的onLayout是空实现,但是ViewGroup的onLayout是抽象方法,因此其子类需要实现该方法来对其子View布局;
进行绘制
performDraw,调用DecorView#draw,传递一个 Canvas 绘制整颗 View 树的内容;draw方法实现了绘制的通用逻辑:先绘制background、用onDraw绘制自身内容、用dispatchDraw绘制子View内容、绘制 foreground等;自定义 View 需要实现
onDraw方法来绘制内容,对于 ViewGroup 还需要调用子View的draw方法绘制其内容;
如何获取view的视图的宽高
通过
View#post能够获取 view 的真实宽高在非初次进入
onResume生命周期直接获取其宽高;通过
viewTreeObserver.OnGlobalLayoutListener回调中获取到视图的宽高重写
view#onSizeChange方法获取重写
view#onLayout方法获取如果在 xml 文件中具体声明了精确值,那么可以通过
LayoutParams来获取到宽高;
View的宽高只有在布局阶段结束后才能得到:
view.post()的原理
ViewRootImpl 中有一个持有主线程 Looper 的 ViewRootHandler,view#post 一般是给这个 Handler 发送消息;
这个 ViewRootHandler 的执行时机是在 ViewRootImpl#performTraversals 执行后,此时已经完成了 View 树的测量布局和绘制过程,然后才执行 view#post 发送的 Runnable,因此我们能通过该方法获取到 View 的真实宽高;
触发invalidate()和requestLayout()会发生什么?
invalidate
invalidate 会将重绘操作不断向其父容器传递,最终会调用 ViewRootImpl#performTraversals 方法,因为没有设置 LayoutRequested 标志位,因此不会触发测量和布局操作,而是直接进行绘制过程:
如果开启了硬件加速,则遍历整颗子树,对设置了
PFLAG_INVALIDATED标志的View进行重绘操作,即调用其draw方法;如果是 ViewGroup 则会重绘当前 ViewGroup 为根节点的 View树;
如果没有开启硬件加速,则会调用
drawSoft方法,对整颗 View 树进行重绘操作;
如果只是绘制的内容有变化,则可以调用该方法进行重绘操作;
requestLayout
requestLayout 会将该操作不断向其父容器传递,最终会调用到 ViewRootImpl#performTraversals 方法,依次执行 performMeasure 、performLayout 和 performDraw ;
并不是整颗 View 树都会重新测量、布局和绘制,在 reuqestLayout 的调用过程中,会给调用者及其所有父容器设置标志位,在执行 measure 和 layout 方法中都会对标志位进行判断,只有存在该标志位才会执行;
绘制操作也不一定会执行,只有在布局过程中有View发生尺寸发生变化才会重新绘制;
一般在 requestLayout 之后调用 invalidate 保证能重新绘制;
View事件分发
View的事件分发机制
Linux内核接收到相关硬件中断后,得到对应的原始输入数据,由系统服务 IntputManagerService 负责读取并封装为输入事件,分发给对应的窗口;
Activity 初次到达 Resume 生命周期时,涉及 ViewRootImpl 的实例化,会在 IMS 注册并获取 InputChannel 来与 IMS 通信:IMS 将输入事件发送给 ViewRootImpl,ViewRootImpl 负责将事件分发给 DecorView,并将处理结果返回给IMS;
DecorView 的 dispatchTouchEvent 分发事件会经过 Activity、Window 再次回到 DecorView 的 superDispatchTouchEvent 方法,通过调用父类 FrameLayout 的 dispatchTouchEvent 方法将事件正式分发给 View 树;
在 View 树中的事件分发过程是一个 DFS 遍历树的过程,由外到内自上而下的分发事件,找到能够消费事件的目标 View;
对于 View 来说,事件分发会调用到 dispatchTouchEvent 方法,负责滚动条滑动事件、 setOnTouchListener 以及 onTouchEvent 方法,如果其中一个消费了事件就不会继续消费;
其中
onTouchEvent方法实现了默认的点击、长按以及 tooltip 的交互逻辑;如果设置的
setOnTouchListener消费了事件,会导致onTouchEvent无法被调用,造成点击、长按事件的失效;
对于 ViewGroup 来说,事件分发会调用到 dispatchTouchEvent 方法:
首先会判断是否拦截事件,会调用
onInterceptTouchEvent来判断,该方法默认返回 false;对于ACTION_DOWN事件只会调用一次该方法进行拦截;如果ViewGroup 没有拦截本次事件,而且事件是
ACTION_DOWN时,就会将事件分发给其子View,会根据子View绘制的顺序从后向前找,找到能够消费事件的View,并将其保存在mFirstTouchTarget里面,后续的事件序列就会直接分发给mFirstTouchTarget单链表中的所有 View;通过
mFirstTouchTarget可以直接将后续的事件序列分发,而不需要重新DFS 查找对应的 View,提高了事件分发的效率;
如果ViewGroup拦截了事件,或者没有找到子View消费事件,那么就会自己尝试消费事件,调用父类 View 的
dispatchTouchEvent方法
如何解决滑动冲突
滑动冲突的本质时,内部的滑动 View 无法接收到输入事件,而是被外部的滑动View所拦截并消费了;
要解决滑动冲突,首先就是要让外部的滑动 View 对 ACTION_DOWN 事件不再拦截,让该事件能够分发到内部的滑动 View,让内部 View 来判断是否进行滑动;
内部的滑动 View 接收到
ACTION_DOWN事件后请求外部滑动 View 不拦截requestDisallowInterceptTouchEvent后续的事件内部的滑动 View 根据实际情况来判断是否处理滑动事件,如果需要就自己消费,如果不需要了则请求外部滑动View拦截事件,比如内部滑动View已经不能滑动等等;
长按如何实现
在 ACTION_DOWN 事件发生时,向 Handler postDelayed 一个执行长按逻辑的 Runnable,这个 delay 的时间是长按的触发时间;
当 ACTION_UP 事件发生时,如果存在长按对应的 Runnable 的话,就需要移除;
如果Runnable到了执行时间没有被移除的话,就会被执行对应的长按逻辑;
事件ACTION_CANCEL是什么
事件 ACTION_CANCEL 并不是由用户直接产生的,而是 ViewGroup 在某个View消费已经分发的事件序列中途,拦截了后续的事件序列,就会导致正在消费事件的 View 无法接收到 ACTION_UP 事件,可能会造成异常的UI显示和逻辑问题,比如按钮的按压状态不消失、异常触发长按事件等;
因此 ViewGroup 拦截了事件序列后,需要在消费的 View 分发一个 ACTION_CANCEL 事件,表示事件序列已经取消,让消费的 View 能够取消对应的UI状态以及清除一些交互逻辑;
自定义View
自定义View的绘制过程
自定义过View吗?如何实现的?
自定义view 过程中要注意的点
(触摸事件,组合,onMeasure处理, padding处理, 自绘控件,canvas,paint,path)