基础篇-View事件传递与绘制机制,自定义View实现理解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gjr9596/article/details/52213952

一.View事件分发机制

1、基础知识

(1) 所有 Touch 事件都被封装成了 MotionEvent 对象,包括 Touch 的位置、时间、历史记录以及第几个手指(多指触摸)等。
(2) 事件类型分为 ACTION_DOWN, ACTION_UP, ACTION_MOVE,ACTION_CANCEL,ACTION_POINTER_DOWN, ACTION_POINTER_UP, ,每个事件都是以 ACTION_DOWN 开始 ACTION_UP 结束。
(3) 对事件的处理包括三类,分别为
a.传递——dispatchTouchEvent()函数
b.拦截——onInterceptTouchEvent()函数
c.消费——onTouchEvent()函数和 OnTouchListener(返回true,事件消费)

2、传递流程

(1) 事件从 Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的 View(ViewGroup)开始一直往下(子 View)传递。子 View 可以通过 onTouchEvent()对事件进行处理。

(2) 事件由父 View(ViewGroup)传递给子 View,ViewGroup 可以通过 onInterceptTouchEvent()对事件做拦截,停止其往下传递。

(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子 View 没有消费事件,事件会反向往上传递,这时父 View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到 Activity 的 onTouchEvent()函数,从下往上的消费次序。

(4) 如果 View 没有对 ACTION_DOWN 进行消费,之后的其他事件不会传递过来。

(5) OnTouchListener 优先于 onTouchEvent()对事件进行消费。
看View的源码实现:
设置的mOnTouchListener不为空,并且是可响应事件的,
就执行mOnTouchListener.onTouch(),返回true时,表示事件被消费拦截,
如果返回false或者其他条件不符,事件继续往下传递处理执行onTouchEvent()的流程
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
onTouchEvent中判断处理包含点击与长按事件的判断
PDF英文原文介绍:Mastering the Android Touch System
附上两张原文中流程图:
(1) View 处理事件流程图 : Action_Down在view层中没有被拦截或消费情况下流程,之后其他MOVE/UP事件就不会传递过来了

(2) View 处理事件流程图 : Action_Down在view层中被拦截或消费情况下流程,Action_move和Action_up继续传递给View层


3.关于VIew的onTouchEvent()中事件分发判断逻辑解析:

/**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
    }



1.接收到Action_down,

(1)mPrivateFlags设置一个PREPRESSED的标识
(2)设置mHasPerformedLongPress=false;表示长按事件还未触发;
(3)发送一个延迟为ViewConfiguration.getTapTimeout()的延迟消息,到达延时时间后会执行CheckForTap()
(4)延时消息到期后,取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识,刷新背景,如果View支持长按事件不为空,则再发一个延时消息,检测长按事件
  1. case MotionEvent.ACTION_DOWN:  
  2.                     if (mPendingCheckForTap == null) {  
  3.                         mPendingCheckForTap = new CheckForTap();  
  4.                     }  
  5.                     mPrivateFlags |= PREPRESSED;  
  6.                     mHasPerformedLongPress = false;  
  7.                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
  8.                     break;  
延时任务处理点击事件:
  1. private final class CheckForTap implements Runnable {  
  2.       public void run() {  
  3.           mPrivateFlags &= ~PREPRESSED;  
  4.           mPrivateFlags |= PRESSED;  
  5.           refreshDrawableState();  
  6.           if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
  7.               postCheckForLongClick(ViewConfiguration.getTapTimeout());  
  8.           }  
  9.       }  
  10.   }  
可以看到如果设置115ms后,没有抬起,会将View的标识设置为PRESSED且去掉PREPRESSED标识,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms),这个115ms刚好时检测预备按压事件PREPRESSED时间;也就是用户从DOWN触发开始算起,如果115ms内抬起了,不触发点击事件,在115到500间抬起,直接出发点击事件,500后没有抬起则先触发了长按事件,在判断是否出发点击事件:
(1)、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;则拦截事件,不触发点击,等
(2)、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;事件继续传递下去
  1. class CheckForLongPress implements Runnable {  
  2.   
  3.         private int mOriginalWindowAttachCount;  
  4.   
  5.         public void run() {  
  6.             if (isPressed() && (mParent != null)  
  7.                     && mOriginalWindowAttachCount == mWindowAttachCount) {  
  8.                 if (performLongClick()) {  
  9.                     mHasPerformedLongPress = true;  
  10.                 }  
  11.             }  
  12.         } 

2.接收到Action_move:

拿到当前触摸的x,y坐标,判断当然触摸点有没有移出我们的View,如果移出了:
(1)、执行removeTapCallback(); 
  1. private void removeTapCallback() {  
  2.        if (mPendingCheckForTap != null) {  
  3.            mPrivateFlags &= ~PREPRESSED;  
  4.            removeCallbacks(mPendingCheckForTap);  
  5.        }  
  6.    } 
(2)、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();
 private void removeLongPressCallback() {
        if (mPendingCheckForLongPress != null) {
          removeCallbacks(mPendingCheckForLongPress);
        }
    }
(3)、最后把mPrivateFlags中PRESSED标识去除,刷新背景
要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等

3.接收到Action_up:

a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;

b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;

c、如果是500ms以后,那么有两种情况:

i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;

ii. 没有设置onLongClickListener或者onLongClickListener.onClick返回false,点击事件OnClick事件依然可以触发;
d、最后执行mUnsetPressedState.run(),将setPressed传递下去,然后将PRESSED标识去除;


二.View绘制流程


1. View 树的绘图流程


当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:

View 绘制流程函数调用链

需要说明的是,用户主动调用 request,只会出发 measure 和 layout 过程,而不会执行 draw 过程

2. 概念measure 和 layout


从整体上来看 Measure 和 Layout 两个步骤的执行:

树的遍历是有序的,由父视图到子视图,每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。
具体分析
measure 过程由measure(int, int)方法发起,从上到下有序的测量 View,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。

measure 过程传递尺寸的两个类

  • ViewGroup.LayoutParams (View 自身的布局参数)
  • MeasureSpecs 类(父视图对子视图的测量要求)
  • ViewGroup.LayoutParams
    这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:

    • 具体值
    • MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)
    • WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)
  • ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的 ViewGroup.LayoutParams 的子类 RelativeLayoutParams。
    有时我们需要使用 view.getLayoutParams() 方法获取一个视图 LayoutParams,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的 LayoutParams,比如 View 的父控件为 RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。

    MeasureSpecs
    测量规格,包含测量要求和尺寸的信息,有三种模式:

    • UNSPECIFIED
      父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到,

    • EXACTLY
      父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。

    • AT_MOST
      父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。

3. measure 核心方法

  • measure(int widthMeasureSpec, int heightMeasureSpec)
    该方法定义在View.java类中,为 final 类型,不可被复写,但 measure 调用链最终会回调 View/ViewGroup 对象的 onMeasure()方法,因此自定义视图时,只需要复写 onMeasure() 方法即可。

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的 width 和 height 的测量要求。在我们自身的自定义视图中,要做的就是根据该 widthMeasureSpec 和 heightMeasureSpec 计算视图的 width 和 height,不同的模式处理方式不同。

  • setMeasuredDimension()
    测量阶段终极方法,在 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的 Measure 过程的,只需调用setMeasuredDimension()设置根据 MeasureSpec 计算得到的尺寸即可,你可以参考 ViewPagerIndicator 的 onMeasure 方法。

4. layout 相关概念及核心方法

首先要明确的是,子视图的具体位置都是相对于父视图而言的。View 的 onLayout 方法为空实现,而 ViewGroup 的 onLayout 为 abstract 的,因此,如果自定义的 View 要继承 ViewGroup 时,必须实现 onLayout 函数。


在 layout 过程中,子视图会调用getMeasuredWidth()getMeasuredHeight()方法获取到 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight,作为自己的 width 和 height。然后调用每一个子视图的layout(l, t, r, b)函数,来确定每个子视图在父视图中的位置。

5. 绘制流程相关概念及核心方法

先来看下与 draw 过程相关的函数:

  • View.draw(Canvas canvas): 由于 ViewGroup 并没有复写此方法,因此,所有的视图最终都是调用 View 的 draw 方法进行绘制的。在自定义的视图中,也不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制,如果自定义的视图确实要复写该方法,那么请先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。

  • View.onDraw():
    View 的onDraw(Canvas)默认是空实现,自定义绘制过程需要复写的方法,绘制自身的内容。

  • dispatchDraw() 发起对子视图的绘制。View 中默认是空实现,ViewGroup 复写了dispatchDraw()来对其子视图进行绘制。该方法我们不用去管,自定义的 ViewGroup 不应该对dispatchDraw()进行复写。


绘制流程图

  • drawChild(canvas, this, drawingTime)
    直接调用了 View 的child.draw(canvas, this,drawingTime)方法,文档中也说明了,除了被ViewGroup.drawChild()方法外,你不应该在其它任何地方去复写或调用该方法,它属于 ViewGroup。而View.draw(Canvas)方法是我们自定义控件中可以复写的方法,具体可以参考上述对view.draw(Canvas)的说明。从参数中可以看到,child.draw(canvas, this, drawingTime) 肯定是处理了和父视图相关的逻辑,但 View 的最终绘制,还是 View.draw(Canvas)方法。

  • invalidate()
    请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View。

  • requestLayout()
    当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发measure()layout()过程,但不会进行 draw。


6. 自定义View实现方法

如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。那么下面我们就来依次学习一下,每种方式分别是如何自定义View的。

(1)自绘控件

自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的。
比如,这是一个自定义计数空间,重写onDraw();
这里首先是将Paint画笔设置为蓝色,然后调用Canvas的drawRect()方法绘制了一个矩形,这个矩形也就可以当作是CounterView的背景图吧。接着将画笔设置为黄色,准备在背景上面绘制当前的计数,注意这里先是调用了getTextBounds()方法来获取到文字的宽度和高度,然后调用了drawText()方法去进行绘制就可以了。
每次点击,调用invalidate()方法会导致视图进行重绘,因此onDraw()方法在稍后就将会得到调用重绘。
基于此,我们可以加入更加巧妙的逻辑判断,来实现自己的自绘控件。

  1. public class CounterView extends View implements OnClickListener {  
  2.   
  3.     private Paint mPaint;  
  4.       
  5.     private Rect mBounds;  
  6.   
  7.     private int mCount;  
  8.       
  9.     public CounterView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  12.         mBounds = new Rect();  
  13.         setOnClickListener(this);  
  14.     }  
  15.   
  16.     @Override  
  17.     protected void onDraw(Canvas canvas) {  
  18.         super.onDraw(canvas);  
  19.         mPaint.setColor(Color.BLUE);  
  20.         canvas.drawRect(00, getWidth(), getHeight(), mPaint);  
  21.         mPaint.setColor(Color.YELLOW);  
  22.         mPaint.setTextSize(30);  
  23.         String text = String.valueOf(mCount);  
  24.         mPaint.getTextBounds(text, 0, text.length(), mBounds);  
  25.         float textWidth = mBounds.width();  
  26.         float textHeight = mBounds.height();  
  27.         canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2  
  28.                 + textHeight / 2, mPaint);  
  29.     }  
  30.   
  31.     @Override  
  32.     public void onClick(View v) {  
  33.         mCount++;  
  34.         invalidate();  
  35.     }  
  36.   
  37. }  


(2)、组合控件

组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。

举个例子来说,标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。
左右两边按钮(文字,背景,事件,显示否),中间提示文字变更等。来视图复用。
比如下拉刷新的PullToRefresh,android-Ultra-Pull-To-Refresh等等。


(3)、继承控件

继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。
比如 SwipeMenuListView,继承Listview,实现getView()的视图,包括SwipeMenuLayout=ItemView+MenuView 2部分,之后,重写Listview的onTouchEvent(),将事件传递到SwipeMenuLayout中来消费,进行view滑动,判断显示。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页