View 的绘制 & 事件分发

View 及 ViewGroup 绘制过程概述

  1. 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
    00UNSPECIFIED父容器对 View 没有大小限制一般用于系统内部
    01EXACTLY父容器指定 View 所需大小,View 忽略自身大小match_parent 或具体数值
    10AT_MOST父容器指定可用大小,View 的大小不能超过该值,具体值看 View 本身大小wrap_content
  2. 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() 后得到的最终宽高

  3. draw

    使用 @Override protected void onDraw(Canvas canvas) 中的 canvas 和 Paint对象等绘制图形即可

View 的事件体系一

  1. MotionEvent

    • 手指接触屏幕后会产生一系列事件,主要的事件类型如下

      • ACTION_DOWN:手指刚接触屏幕

      • ACTION_MOVE:手指在屏幕上移动

      • ACTION_UP:手指离开屏幕的瞬间

    • 通过 MotionEvent 对象可得到点击事件发生时的 x/y 坐标

      • getX/getY:相对当前 View 左上角的 x/y 坐标

      • getRawX/getRawY:相对手机屏幕左上角的 x/y 坐标

  2. TouchSlop

    即系统所能识别的最小滑动距离,若滑动距离小于该常量,系统不认为此时进行的是滑动操作

  3. 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();
    
  4. 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 的监听接口及方法的部分总结如下

      1. OnGestureListener

        • onDown:手指触屏瞬间

        • onShowPress:手指触屏尚未松开或滑动

        • onSingleTapUp:单击

        • onScroll:滑动

        • onLongPress:长按

        • onFling:手指触屏,快速滑动后松开

      2. OnDoubleTapListener

        • onDoubleTap:双击,与 onSingleTapConfirmed 不可共存

        • onSingleTapConfirmed:严格单击,不是双击中的一次单击

        • onDoubleTapEvent:双击行为

    • 建议监听滑动在 onTouchEvent方法中实现,监听双击则使用 GestureDetector

View 的事件体系二 - - 滑动

  1. 通过View本身的scrollTo/scrollBy方法实现

    • scrollBy(基于当前位置的滑动)也是调用scrollTo(基于所传递参数的绝对滑动)方法

    • 滑动过程中View内部的属性mScrollX和mScrollY可通过getScrollX和getScrollY方法获取

    • 本方式只能移动View的内容,不能移动View本身

  2. 通过动画实现

    • 主要操作View的translationX和translationY两个属性来对View进行平移

    • 明白补间动画(不能真正改变View的位置参数,仅对View的影像做操作)和属性动画(√)的区别

  3. 通过改变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
  4. 弹性滑动

    1. 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方法滑动到新的位置,如此反复至滑动过程结束

    2. 动画onAnimationUpdate方法

      在该方法中调用getAnimationFraction()方法获取动画帧片段,在每一小段时间中调用View的scrollTo方法一小段一小段的移动View

    3. 延时策略

      切割若干个时间片段,在Handler中调用View的scrollTo方法一小段一小段的移动View

事件分发 - 分析对象为 MotionEvent

  1. @Override public boolean dispatchTouchEvent(MotionEvent ev)

    进行事件的分发。若事件能够传递给当前 View,则此方法一定会被掉用,返回结果受当前 View 的 onTouchEvent 方法和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件

  2. @Override public boolean onInterceptTouchEvent(MotionEvent event)

    在上述方法内部掉用,用来判断是否拦截某个事件,若当前 View 拦截了某个事件,则在同一事件序列(即从手指触屏瞬间至手指离开屏幕瞬间,期间产生的一系列事件,即down-move-up过程)中,此方法不会被再次调用,返回结果表示是否拦截当前事件
    默认返回 false 即不拦截事件,View 没有该方法,一旦事件传递给它,则它的 onTouchEvent方法就会被调用

  3. @Override public boolean onTouchEvent(MotionEvent event)

    dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如不消耗,则在同一事件序列中,当前 View 无法再次接收到事件
    可点击的 View 的 onTouchEvent方法默认返回 true 即消耗事件,且 View 的 enable/disable 属性不影响该方法的默认返回值

  4. 上述三个方法的关系如下伪代码表示

    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 优先级处于事件传递的最末端

  5. requestDisallowInterceptTouchEvent

    事件传递过程时由外向内的,即由父元素分发给子元素。通过 requestDisallowInterceptTouchEvent方法可在子元素中干预父元素的事件分发过程,但 ACTION_DOWN事件除外

事件传递机制的总结

  1. 同一事件序列是指从手指触屏瞬间至离开屏幕瞬间,过程中产生的一系列事件,即从down事件开始,up事件结束,中间包含若干move事件

  2. 正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某个事件,那么同一事件序列内的所有事件都会直接交给其处理,因此同一事件序列中的事件不能分别由两个View同时处理。通过特殊手段,如一个View可将本该自己处理的事件通过onTouchEvent强行传递给其他View处理

  3. 某个View一旦拦截,那么同一事件序列都只能由它来处理,且其onInterceptTouchEvent方法不会再被调用

  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再交给它来处理,且事件将重新交个它的父元素处理,即父元素的onTouchEvent方法会被调用

  5. 若View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,且当前View可持续接收到后续的事件,最终这些消失的点击事件会传递给Activity处理

  6. ViewGroup默认不拦截任何事件,即ViewGroup的onInterceptTouchEvent方法默认返回false

  7. View没有onInterceptTouchEvent方法,若有事件传递给它,则其onTouchEvent方法会被调用

  8. View的onTouchEvent默认会消耗事件,除非它是不可点击的(clickable,longClickable == false)。View的longClickable默认都为false

  9. View的enable属性不影响onTouchEvent的默认返回值,即使View是disable状态,只要其clickable或longClickable有一个为true,则其onTouchEvent就返回true

  10. onClick会发生的前提是当前View是可点击的,且其收到了down和up的事件

  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,再由父元素分发给子元素。但通过requestDisallowInterceptTouchEvent方法可在子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值