View 及 ViewGroup 绘制过程概述
-
measure
对单个View控件来说即 measure 完成获取 View 本身的宽高;对 ViewGroup 来说除了完成自身整体的 measure,还要遍历完成所有子View的 measure。onMeasure方法中应完成:①得到各个子控件的宽/高测量值给②使用;②得到自定义根布局的整体宽高,并根据当前布局的测量模式将对应宽/高测量值通过
setMeasuredDimension(widthSize, heightSize);
交给系统。注意:对这个测量值有影响的因素包括-
(子)View 本身的具体宽高
-
(子)View 本身的
android:layout_width="wrap_content | match_parent | 具体dp值" android:layout_height="wrap_content | match_parent | 具体dp值"
及其父容器的
android:layout_width="wrap_content | match_parent | 具体dp值" android:layout_height="wrap_content | match_parent | 具体dp值"
这些都封装在
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
顾名思义即测量规格,系统将 View 本身的具体宽高 Size 封装在 MeasureSpec 的后30位中,将 LayoutParams 作为测量模式封装在 MeasureSpec 的前两位中,下面总结 MeasureSpec 中的测量模式种类:MeasureSpec 的前两位标识 种类 含义 对应的 LayoutParams 00 UNSPECIFIED
父容器对 View 没有大小限制 一般用于系统内部 01 EXACTLY
父容器指定 View 所需大小,View 忽略自身大小 match_parent 或具体数值 10 AT_MOST
父容器指定可用大小,View 的大小不能超过该值,具体值看 View 本身大小 wrap_content -
-
layout
layout 是 ViewGroup 用来确定子元素的位置,当 ViewGroup 位置确定,onLayout方法中会遍历所有子元素并调用其 layout方法,而在 layout方法中 onLayout方法又会被调用。layout方法确定 View本身的位置,onLayout方法确定所有子元素的位置
-
示例:简单自定义的ViewGroup中确定所有子元素位置
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = 0; ...... for (int i = 0; i < getChildCount(); i++) { ...... // 第一个子元素位置(0,0)(子元素宽,第一个子元素高) // 第二个子元素位置(第一个子元素宽,0)(第一、二子元素宽的和,第二个子元素高) // 后面依次类推可确定每个子元素的位置 childView.layout(left, 0, left + childWidth, childHeight); left += childWidth; ...... } }
-
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方法 中追踪并获取滑动的速度,包括水平和竖直方向的速度
// 获取VelocityTracker实例 VelocityTracker mVelocityTracker = VelocityTracker.obtain(); @Override public boolean onTouchEvent(MotionEvent event) { // 追踪事件 mVelocityTracker.addMovement(event); // 计算并获取当前滑动的水平/竖直速度 mVelocityTracker.computeCurrentVelocity(1000); // 每1000ms滑动的像素值 float xVelocity = mVelocityTracker.getXVelocity(); float yVelocity = mVelocityTracker.getYVelocity(); ...... }
当不再需要使用时,应重置并回收内存
mVelocityTracker.clear(); mVelocityTracker.recycle();
-
GestureDetector
辅助检测用户的单击、滑动、长按、双击等行为
-
完整示例:左右滑动切换 Activity
public abstract class BaseSetupActivity extends Activity { private GestureDetector mGestureDetector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 实例化手势识别器,并添加滑动监听 mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { /** 快速滑动。e1: 起点坐标 e2: 终点坐标 velocityX: 水平滑动速度 velocityY:竖直滑动速度 */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(e2.getRawY() - e1.getRawY()) > 100) { Log.d("catfaceooo", "竖直方向滑动范围太大"); return true; } if (Math.abs(velocityX) < 100) { Log.d("catfaceooo", "水平滑动速度太慢"); return true; } // 判断向左划还是向右划 if (e2.getRawX() - e1.getRawX() > 200) { // 向右划,上一页 showPrevious(); return true; } if (e1.getRawX() - e2.getRawX() > 200) { // 向左划,下一页 showNext(); return true; } return super.onFling(e1, e2, velocityX, velocityY); } }); } /** 按钮点击上一页 */ public void previous(View view) { showPrevious(); } /** 按钮点击下一页 */ public void next(View view) { showNext(); } // 暴露给需要滑动切换的Activity public abstract void showPrevious(); public abstract void showNext(); /** 当前界面被触摸时,走此方法 */ @Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event);// 将事件委托给手势识别器处理 return super.onTouchEvent(event); } }
-
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重新布局实现
MarginLayoutParams params = (MarginLayoutParams) bt_test.getLayoutParams(); // 下面两行的结果就是:向右平移100像素 params.width += 100; params.leftMargin += 50; bt_test.setLayoutParams(params);
上述三个方法的使用场景
方法 使用场景 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 属性不影响该方法的默认返回值 -
上述三个方法的关系如下伪代码表示
public boolean dispatchTouchEvent(MotionEvent event) { boolean consume = false; if(onInterceptTouchEvent(event)) { consume = onTouchEvent(event); } else { consume = child.dispathchTouchEvent(event); } return consume; }
根据伪代码简要说明事件的传递规则:对于一个根 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事件除外