自定义View以及事件分发总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012082017/article/details/88087590
  1. 坐标系原点默认是屏幕左上角,向右为X轴正方向,向下为Y轴正方向。

  2. View的getTop()、getLeft()、getBottom()、getRight()是相对父View来说的。

  3. 注意区分View的坐标系Canvas的坐标系。View坐标系的原点是View的左上角;Canvas的坐标系默认是与View的重合,但是通过平移、旋转、缩放可以进行操作。

  4. 触摸事件MotionEvent的getX()、getY()是相对于View坐标系的;getRawX()、getRawY()是相对于屏幕坐标系的。

  5. 0°角与X轴正方向重合,角度沿着顺时针增大。

  6. A R G B 的取值范围均为0255(即16进制的0x000xff),A 从0x00到0xff表示从透明到不透明;RGB 从0x00到0xff表示颜色从浅到深。

  7. bitmap大小计算公式,单位为Byte:
    bitmap.getWidth()*bitmap.getHeight()*(1/inSampleSize)2*(目标设备分辨率/dpi文件夹分辨率)2*色彩空间

  8. merge标签必须是xml文件的根标签,merge标签与include标签一起用;通过LayoutInflater填充merge标签时必须指定父ViewGroup。

  9. getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

  10. onMeasure()用于使用父View传过来的widthMeasureSpec,heightMeasureSpec来确定自身能达到的最大尺寸(可以超,但是超过这个尺寸的部分将显示不出来)。至于View真正的尺寸还需要onLayout()的过程去确定,onLayout()中父View指定的矩形参数有可能使getWidth()比getMeasureWidth()大。最佳实践:遵守规范,通过数学计算控制参数把内容控制在屏幕之内。

  11. View的绘制流程从ViewRootImpl的performTraversals()开始,performTraversals()里面会依次执行measure()、layout()、draw()的流程。
    measure():先会获取根布局的MeasureSpec,如果是match_parent则为EXACTLY,如果是wrap_content则为AT_MOST,大小皆为窗口(window)的大小。也就意味着根视图总是会充满全屏的。View的onMeasure()接受父View传来的宽高参数,onMeasure的默认实现是调用getDefaultSize()来获取View的大小,如果MeasureSpec的mode为EXACTLY和AT_MOST则获取MeasureSpec中的尺寸信息。覆写onMesure()函数进行测量的时候记得调用setMeasuredDimension()。ViewGroup需要调用measureChildren()(最后会调用measureChild())去触发子View进行测量。

    layout():接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。View中的onLayout()是空方法。

    draw():measure和layout的过程都结束后,接下来就进入到draw的过程了。

  12. 调用顺序:onMeasure()–>onSizeChanged()–>onLayout()–>onDraw()。

  13. 视图重绘:invalidate()中先调用skipInvalidate()方法来判断当前View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。接着循环请求自己的父View去重绘,直到循环到最外层的根View后,调用ViewRoot的invalidateChildInParent()去重绘。最后会通过Handler发送一条DO_TRAVERSAL的消息给ViewRoot自身,再次调用performTraversals()进行绘制。

  14. invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用**requestLayout()**了。

  15. 自定义View有三种:自绘控件、组合控件、继承控件。

  16. onDraw()自绘制图形的时候,通常会把Canvas坐标系移动到中央(或者是旋转等会使Canvas坐标系变化的操作),因为这样进行参数计算比较简单,但是这样会存在一个问题:如果想实现View内区域点击事件监控的话,存在坐标不一致的问题。因为MotionEvent的getX()、getY()是相对View的,如果取触摸点的getX()、getY()去跟圆的Region去判断的话,将会被错误的判断为触摸了圆,而事实上在Canvas坐标系中该触摸点为(-x,-y)。解决方法:对Canvas坐标系进行了变化的时候记录下它的逆矩阵,用逆矩阵对MotionEvent的**getRawX()、getRawY()**进行转化即可得到触摸点相对于该Canvas的坐标。原理:在绘制的时候Canvas会把自己的坐标转化为屏幕坐标进行绘制,所以想要还原回Canvas绘制状态时的坐标可以用它的逆矩阵进行逆向操作。

    View坐标系与Canvas坐标系不重合时

    注意:如果Canvas坐标系与View坐标系重合则直接用MotionEvent的getX()、getY()即可。

  17. 事件分发流程:Activity->ViewGroup->View;对每个接收对象来说:dispatchTouchEvent()->onInterceptTouchEvent()->onTouchEvent()。

类型 函数 Activity ViewGroup View 返回值
事件分发 dispatchTouchEvent() true:不继续向下分发
事件拦截 onInterceptTouchEvent() x x true:拦截事件
事件消费 onTouchEvent() true:消费掉事件
  1. ViewGroup事件分发伪代码:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result = false;             // 默认状态为没有消费过
    
        if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
            result = child.dispatchTouchEvent(ev);
        }
    
        if (!result) {                      // 如果事件没有被消费,询问自身onTouchEvent
            result = onTouchEvent(ev);
        }
    
        return result;
    }
    

    可以看到只有onInterceptTouchEvent()的返回值没有赋给result,所以onInterceptTouchEvent()返回true只会中断事件dispatch,还是会继续从当前的View进行onTouch()反向回调。而dispatchTouchEvent()返回true中断掉所有流程;onTouchEvent()返回true则会中断掉回调过程,也即是表示事件被消费了。

  2. View相关事件调用顺序:onTouchListener>onTouchEvent>onLongClickListener>onClickListener

    伪代码:

    public boolean dispatchTouchEvent(MotionEvent event) {
      if (mOnTouchListener.onTouch(this, event)) {
          return true;
      } else if (onTouchEvent(event)) {
          return true;
      }
      return false;
    }
    

    onTouchEvent()中处理onClickListener和onLongClickListener。

    精简版源码:

    public boolean onTouchEvent(MotionEvent event) {
        ...
        final int action = event.getAction();
      	// 检查各种 clickable
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    removeLongPressCallback();  // 移除长按
                    ...
                    performClick();             // 检查单击
                    ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    checkForLongClick(0);       // 检测长按
                    ...
                    break;
                ...
            }
            return true;                        // ◀︎表示事件被消费
        }
        return false;
    }
    

    上面可以看出:只要 View 可点击onTouchEvent()就返回 true,就表示事件被消费了。

    举例,

    <RelativeLayout
        android:background="#CCC"
        android:id="@+id/layout"
        android:onClick="myClick"
        android:layout_width="200dp"
        android:layout_height="200dp">
        <View
            android:clickable="true"
            android:layout_width="200dp"
            android:layout_height="200dp" />
    </RelativeLayout>
    

    现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件myClick,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 android:clickable="true" 正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
    1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
    2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

  3. 事件分发核心要点:

    • 事件分发原理: 责任链模式,事件层层传递,直到被消费。
    • View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
    • View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
    • 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
    • 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
    • ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
    • ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
    • 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
    • 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
    • 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来

猜你喜欢

转载自blog.csdn.net/u012082017/article/details/88087590
今日推荐