View 及 ViewGroup 绘制过程概述
- measure
对单个View控件来说即 measure 完成获取 View 本身的宽高;对 ViewGroup 来说除了完成自身整体的 measure,还要遍历完成所有子View的 measure。onMeasure方法中应完成:①得到各个子控件的宽/高测量值给②使用;②得到自定义根布局的整体宽高,并根据当前布局的测量模式将对应宽/高测量值通过 setMeasuredDimension(widthSize, heightSize);
- (子)View 本身的具体宽高
- (子)View 本身的
及其父容器的
这些都封装在 LayoutParams
针对第二条举个例子:假设父容器 LinearLayout 的 layout_width 是 match_parent,子 View(Button) 的 layout_width 也是 match_parent,那么该 Button 的测量宽值就是 LinearLayout 的宽值;但如果子 Button 的 layout_width 是 wrap_content 或者具体 dp值时,那么该 Button 的测量宽值就应该是其本身的具体宽值
上述总结的两个因素,系统帮我们将其一起封装在了 MeasureSpec
(一个32位 int 值)中,MeasureSpec
MeasureSpec 的前两位标识 | 种类 | 含义 | 对应的 LayoutParams |
00 | | 父容器对 View 没有大小限制 | 一般用于系统内部 |
01 | | 父容器指定 View 所需大小,View 忽略自身大小 | match_parent 或具体数值 |
10 | | 父容器指定可用大小,View 的大小不能超过该值,具体值看 View 本身大小 | wrap_content |
- layout
layout 是 ViewGroup 用来确定子元素的位置,当 ViewGroup 位置确定,onLayout方法中会遍历所有子元素并调用其 layout方法,而在 layout方法中 onLayout方法又会被调用。layout方法确定 View本身的位置,onLayout方法确定所有子元素的位置
- 示例:简单自定义的ViewGroup中确定所有子元素位置
- getMeasureWidth()&getWidth() – getMeasureHeight()&getHeight()
- getMeasureWidth() 和 getMeasureHeight() 是View的 onMeasure() 过后得到的测量宽高
- getWidth() 和 getHeight() 是View的 layout() 后得到的最终宽高
- draw
使用 @Override protected void onDraw(Canvas canvas) 中的 canvas 和 Paint对象等绘制图形即可
View 的事件体系一
- MotionEvent
- 手指接触屏幕后会产生一系列事件,主要的事件类型如下
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指离开屏幕的瞬间
- 通过 MotionEvent 对象可得到点击事件发生时的 x/y 坐标
- getX/getY:相对当前 View 左上角的 x/y 坐标
- getRawX/getRawY:相对手机屏幕左上角的 x/y 坐标
- TouchSlop
即系统所能识别的最小滑动距离,若滑动距离小于该常量,系统不认为此时进行的是滑动操作 - VelocityTracker
在 onTouchEvent方法 中追踪并获取滑动的速度,包括水平和竖直方向的速度
当不再需要使用时,应重置并回收内存
- GestureDetector
辅助检测用户的单击、滑动、长按、双击等行为
- 完整示例:左右滑动切换 Activity
- GestureDetector 的监听接口及方法的部分总结如下
- OnGestureListener
- onDown:手指触屏瞬间
- onShowPress:手指触屏尚未松开或滑动
- onSingleTapUp:单击
- onScroll:滑动
- onLongPress:长按
- onFling:手指触屏,快速滑动后松开
- OnDoubleTapListener
- onDoubleTap:双击,与 onSingleTapConfirmed 不可共存
- onSingleTapConfirmed:严格单击,不是双击中的一次单击
- onDoubleTapEvent:双击行为
- 建议监听滑动在 onTouchEvent方法中实现,监听双击则使用 GestureDetector
View 的事件体系二 - - 滑动
- 通过View本身的scrollTo/scrollBy方法实现
- scrollBy(基于当前位置的滑动)也是调用scrollTo(基于所传递参数的绝对滑动)方法
- 滑动过程中View内部的属性mScrollX和mScrollY可通过getScrollX和getScrollY方法获取
- 本方式只能移动View的内容,不能移动View本身
- 通过动画实现
- 主要操作View的translationX和translationY两个属性来对View进行平移
- 明白补间动画(不能真正改变View的位置参数,仅对View的影像做操作)和属性动画(√)的区别
- 通过改变View的LayoutParams使View重新布局实现
上述三个方法的使用场景
方法 | 使用场景 |
1.scrollTo/scrollBy | 对View内容的滑动 |
2.动画 | 无交互的View和实现复杂的动画效果 |
3.LayoutParams | 有交互的View |
- 弹性滑动
- Scroller的工作机制
- Scroller本身不能实现View的滑动,需要配合View的 computeScroll方法(自行实现)才能完成弹性滑动的效果,不断的让View重绘,而每次重绘的距离起始时间会有一个时间间隔,通过这个时间间隔Scrollerr就可以得到View的滑动位置,然后通过scrollTo方法来完成View的滑动。即View的每次重绘都会导致View进行小幅滑动,多次小幅滑动就组成了弹性滑动
- 当View重绘后会在draw方法中调用computeScroll,而computeScroll又会向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法进行第二次重绘,同样会调用computeScroll方法;然后继续向Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法滑动到新的位置,如此反复至滑动过程结束
- 动画onAnimationUpdate方法
在该方法中调用getAnimationFraction()方法获取动画帧片段,在每一小段时间中调用View的scrollTo方法一小段一小段的移动View - 延时策略
切割若干个时间片段,在Handler中调用View的scrollTo方法一小段一小段的移动View
事件分发 - 分析对象为 MotionEvent
- @Override public boolean dispatchTouchEvent(MotionEvent ev)
进行事件的分发。若事件能够传递给当前 View,则此方法一定会被掉用,返回结果受当前 View 的 onTouchEvent
方法和下级 View 的 dispatchTouchEvent
- @Override public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部掉用,用来判断是否拦截某个事件,若当前 View 拦截了某个事件,则在同一事件序列(即从手指触屏瞬间至手指离开屏幕瞬间,期间产生的一系列事件,即down-move-up过程)中,此方法不会被再次调用,返回结果表示是否拦截当前事件
默认返回 false 即不拦截事件,View 没有该方法,一旦事件传递给它,则它的 onTouchEvent方法就会被调用 - @Override public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent
方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如不消耗,则在同一事件序列中,当前 View 无法再次接收到事件
可点击的 View 的 onTouchEvent方法默认返回 true 即消耗事件,且 View 的 enable/disable 属性不影响该方法的默认返回值 - 上述三个方法的关系如下伪代码表示
根据伪代码简要说明事件的传递规则:对于一个根 ViewGroup,事件产生后,首先会传递给它,此时它的 dispatchTouchEvent方法就会被调用,若它的 onInterceptTouchEvent方法返回 true 拦截当前事件,该事件就会在它的 onTouchEvent方法中处理;若它的 onInterceptTouchEvent方法返回 false不拦截当前事件,该事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理
补:当一个 View 需要处理事件时,若其设置了 OnTouchListener,则 OnTouchListener 中的 onnTouch方法会被调用,若返回 false,则当前 View 的 onTouchEvent方法才会被调用。在 onTouchEvent方法中,若设置 OnClickListener,则其 onClick方法会被调用,即 OnClickListener 优先级处于事件传递的最末端
- requestDisallowInterceptTouchEvent
事件传递过程时由外向内的,即由父元素分发给子元素。通过 requestDisallowInterceptTouchEvent方法可在子元素中干预父元素的事件分发过程,但 ACTION_DOWN事件除外
事件传递机制的总结
- 同一事件序列是指从手指触屏瞬间至离开屏幕瞬间,过程中产生的一系列事件,即从down事件开始,up事件结束,中间包含若干move事件
- 正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某个事件,那么同一事件序列内的所有事件都会直接交给其处理,因此同一事件序列中的事件不能分别由两个View同时处理。通过特殊手段,如一个View可将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
- 某个View一旦拦截,那么同一事件序列都只能由它来处理,且其onInterceptTouchEvent方法不会再被调用
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再交给它来处理,且事件将重新交个它的父元素处理,即父元素的onTouchEvent方法会被调用
- 若View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,且当前View可持续接收到后续的事件,最终这些消失的点击事件会传递给Activity处理
- ViewGroup默认不拦截任何事件,即ViewGroup的onInterceptTouchEvent方法默认返回false
- View没有onInterceptTouchEvent方法,若有事件传递给它,则其onTouchEvent方法会被调用
- View的onTouchEvent默认会消耗事件,除非它是不可点击的(clickable,longClickable == false)。View的longClickable默认都为false
- View的enable属性不影响onTouchEvent的默认返回值,即使View是disable状态,只要其clickable或longClickable有一个为true,则其onTouchEvent就返回true
- onClick会发生的前提是当前View是可点击的,且其收到了down和up的事件
- 事件传递过程是由外向内的,即事件总是先传递给父元素,再由父元素分发给子元素。但通过requestDisallowInterceptTouchEvent方法可在子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外