View的事件传递源码详细解析

简介

View是Android中最后的UI的控件单元;

一个触摸事件传递到View的时候就是最后一个要处理事件的;

它不同于ViewGroup,它没有其他的子类;

因此,View的事件传递的源代码是最好掌握的。

这里认真分析源代码,当做一个学习笔记;源代码使用android2.2的版本,很简单,直接易懂;

废话不多说开始主题;

View事件传递总体流程

首先任何一个Ui控件接收事件都是从dispatchTouchEvent方法开始,这是系统里面指定的,如果要细究就要从Window开始了,这里就不展开了;View的总体事件流程如下图所示:

dispatchTouchEvent()方法

这个方法返回 true :表示自己处理了这个事件;返回false,表示不处理这个事件了;

这个方法的源代码如下:

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

源码注释主要是说:把触摸事件传递到这个目标View。

  • if条件一,mOnTouchlistener就是我们开发中会设置的setOnTouchListener事件;判断它是不是null
  • if条件二,判断这个View是不是可使用的,默认都是;
  • if条件三,判断 重写的onTouchListener中的onTouch方法是返回 true or false

假如以上三个条件全部成立的话,那么dispatchEvent()方法就返回true;不会再往下面执行onTouchEvent方法了,这就表示自己要处理该事件了。

再详细看看if的三个条件

 mOnTouchListener != null

mOnTouchListener是触摸监听接口OnTouchListener的某个实现,是由开发者注入的,这可以从源代码里找到;

    /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        mOnTouchListener = l;
    }

从这里,我们可以看到mOnTouchListener被赋值了,如果我们没有设置setOnTouchListener那么mOnTouchListener就是null了,上面if条件就不成立了,

我们设置setOnTouchListener之后,就会就行第二个条件的判断 

(mViewFlags & ENABLED_MASK) == ENABLED

这个条件的意思是当前这个View是不是能够使用;

单独看每一个位运算符,也是心里不是很踏实,到底是不是,其实从源码里也是可以确认的:

    /**
     * Returns the enabled status for this view. The interpretation of the
     * enabled state varies by subclass.
     *
     * @return True if this view is enabled, false otherwise.
     */
    @ViewDebug.ExportedProperty
    public boolean isEnabled() {
        return (mViewFlags & ENABLED_MASK) == ENABLED;
    }

这个isEnabled()方法内容和if条件二也就是(mViewFlags & ENABLED_MASK) == ENABLED一模一样可用,其返回true,表示可用,返回false表示不可用;平时开发中我们设置setEnable就会影响此处的返回结果;

mOnTouchListener.onTouch(this, event) 

这个条件相信看看OnTouchListener的接口就一切都懂了,

OnTouchListener里面只有一个onTouch方法

  /**
     * Interface definition for a callback to be invoked when a touch event is
     * dispatched to this view. The callback will be invoked before the touch
     * event is given to the view.
     */
    public interface OnTouchListener {
        /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onTouch(View v, MotionEvent event);
    }

现在想想,如果子类实现了这个接口,不就得实现接口里面的方法吗?,而恰好这个方法返回的是一个boolean

上面的if条件没有执行就执行onTouchEvent方法了;

onTouchEvent()方法

下面在逐步分析onTouchEvent方法的内部执行:

这个方法比较长,这样也好,就不用我们找来找去了,这里贴一下全部完整的源代码:

                
        public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
        //这个View是不能够使用的 
        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));
        }
        //这个View设置了触摸代理事件
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //这个View是可以点击或者可以长按情况,结果一定返回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.首先if条件判断View是不可用的

这个条件的源码片段:

        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中的条件 (viewFlags & ENABLED_MASK) == DISABLED 判断这个View是不是可用的。

如果不可用的话,if条件成立 会返回 (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));

是否可以点击或者是否可以长按;对应isClickable()和isLongClickable()方法,可以从源代码里面查看到,这里也贴一下这两个方法isClickable()和isLongClickable()的源代码:


    @ViewDebug.ExportedProperty
    public boolean isClickable() {
        return (mViewFlags & CLICKABLE) == CLICKABLE;
    }

    public boolean isLongClickable() {
        return (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
    }

这里小结一下:如果一个View是不可用,只要它的可以点击 或者 是可长按点击的,它都返回true,也就是消耗了这次事件;代码也不会继续往下执行了;否则返回false,不消费事件

2.是否设置了触摸代理事件mTouchDelegate

这一段源代码片段:

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

源代码的意思是,这个View有没有设置触摸代理事件 ,就是把当前View的触摸事件叫另一个View来处理,如果mTouchDelegate不为null成立的话,就会进入这个条件内部,否则继续往下执行;

3.是可以点击或者长按的话

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

-------省略中间       
 return true
    }
return false;

如果要是可点击或者是可长按点击的,可以看到只要进入这个if,统统返回true;代表事件被这个View消费了。

现在已经是onTouchEvent方法的最后了,对于中间省略部分是switch情形中的各种case ,我们一个一个啃,逐步分析,就按照

  • 按下(MotionEvent.ACTION_DOWN) ,
  • 移动(MotionEvent.ACTION_MOVE),
  • 取消了(MotionEvent.ACTION_CANCEL),
  • 抬起(MotionEvent.ACTION_UP)  

这个事件的逻辑进行

3.1 MotionEvent.ACTION_DOWN

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

代码第2行 mPengdingCheckForTap是一个Runnable类型的对象,延迟执行长按任务。。。如果是null

代码第3行就会创建一个对象;

代码第5行 是一个私有的flag标记变量mPrivateFlags ,mPrivateFlags |= PREPRESSED;这句的意思是标记为点击按下了;

代码第6行 是 mHasPerformedLongPress是一个布尔类型的变量,值为false,表示 还没有执行长按;

代码第7行是将任务mPengingCheckForTap加入到消息队列里面延迟执行 ,至于延迟多久 ViewConfiguration.getTapTimeout()获取的时间,源码中是115毫秒,这里是单击的时间,超过了115毫秒单击时间限定之后就执行mPengdingCheckForTap(Runnable长按任务);

对于第4行代码,可能有疑惑到底是干嘛用的,这里我解释一下 ,View中有很多状态 ,比如 可不可以单击 、可不可以双击 、可以可以长按、 可不可以由焦点;其实每种状态都对应2种情况 要么可以true,要么不可以false; 对应到代码中控制的是2进制的某一个位是1和0; 1表示true。 0表示false,说白了是通过二进制位运算来控制true or false,因为2进制每一位是0或1;

  • 接下来,我们在看看mPendingCheckForTap中实现的run方法:
    private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PREPRESSED;
            mPrivateFlags |= PRESSED;
            refreshDrawableState();
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                postCheckForLongClick(ViewConfiguration.getTapTimeout());//执行长按
            }
        }
    }

第3行取消了点击按下行为标记 ,因为按下事件超过了115毫秒,之后就不能再当单击按下行为来对待了;

第4行标记成了长按的行为; (PRESSED也是一个final型的变量 它的存在的意义就是为了判断是不是长按下,基本原理也是通过二进制的某一位是0还是1来的)

第5行更新背景

第6行,(mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE判断是不是可长按,这个行为如果我们在代码里面设置长按监听 或者是可以长按的话 就是可以的,没有的

话,这个条件就不成立;

  • postCheckForLongClick的源代码如下:
    private void postCheckForLongClick(int delayOffset) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }

这里其实最主要的就是 mPendingCheckForLongPress 也是一个Runnable对象,而ViewConfiguration.getLongPressTimeout()的值源码中是500毫秒,超过了500毫秒就会执行长按了

  • CheckForLongPress的run方法
    class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }
        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }
    }

从 4行开始,isPressed()源码如下:

    public boolean isPressed() {
        return (mPrivateFlags & PRESSED) == PRESSED;
    }

看到了吗?这个条件此时一定是true 

mParent!=null也一定是成立的 在任何View通过setContentView方法加入到Activity中的时候,一定有父View,

最后一个条件 mOriginalWindowAttachCount == mWindowAttachCount  看看上一个源码片段的 rememberWindowAttachCount()方法就知道也一定是true

如果都成立那就执行performLongClick方法了

   public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        if (mOnLongClickListener != null) {
            handled = mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

看看第6行代码 我们自己实现的长按点击事件中onLongClick方法如果返回true 最后这个方法就返回true,如果返回false,这个方法最后就返回false

对于返回true,通过看上面的源代码很好理解,如果返回的是false ,这里我们也要分析一下为什么最终这个方法也返回的是false

 handled = showContextMenu();

主要是这行代码

    public boolean showContextMenu() {
        return getParent().showContextMenuForChild(this);
    }

getParent()一定是一个ViewGruop吧?那么去到ViewGruop中去看看这个方法

    public boolean showContextMenuForChild(View originalView) {
        return mParent != null && mParent.showContextMenuForChild(originalView);
    }

翻遍源码ViewGroup的所有子类都没有Overdide这个方法,那么 mParent!=null最后一定会是false,因为顶级的DecoerView没有父View;是不是!

这里长安监听的方法返回treu还是false对后面有什么影响?这里先保留,继续往后看源代码吧

3.2 MotionEvent.ACTION_MOVE

移动的时候的源代码

                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;

首先第6行mTouchSlop是系统所能识别的最小滑动距离,他是从哪里来的 在初始化的时候View获取的

    public View(Context context) {
        mContext = context;
        mResources = context != null ? context.getResources() : null;
        mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED;
        // Used for debug only
        //++sInstanceCount;
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

在我们认定用户是在滑动像素之后,一个触摸要移动的距离,就是被当做滑动的最小移动距离

再看那么if条件 意思是不是 触摸超过了View宽高的+系统所能识别的最小滑动距离,也就是被认为滑动到了View的外面去了,这个时候就会执行里面的取消单击,取消执行长按行为:

    /**
     * Remove the tap detection timer.
     */
    private void removeTapCallback() {
        if (mPendingCheckForTap != null) {
            mPrivateFlags &= ~PREPRESSED;
            removeCallbacks(mPendingCheckForTap);
        }
    }

上面是取消单击行为和任务,代码之后都有分析,不难看懂

    private void removeLongPressCallback() {
        if (mPendingCheckForLongPress != null) {
          removeCallbacks(mPendingCheckForLongPress);
        }
    }

取消长按任务,这个后面跟着取消了长按的标记

 3.3 MotionEvent.ACTION_CANCEL

取消的源代码

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

首先,我们要知道什么什么会收到这个事件MotionEvent.ACTION_CANCEL,上一个动作被父容器拦截的时候,子View会收到这样一个事件,比如用户一直在Moving,当某一个移动事件被父View拦截的时候,这个子View就会收到这个取消的事件;

分析代码 

第2行代码是取消长按按下的标记

第3行是刷新背景

第4行是取消单击任务和标记

3.4 MotionEvent.ACTION_UP

最后我们再来看一个抬起事件

                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;

首先,如果用户滑动的时候事件没有被取消或者没有移动到View之外的区域,那么第3行的这个条件一定会成立;

7~9行代码 if条件的意思是 是否可获取焦点 、是否触摸可以获取焦点、 是否已经获取了焦点 

对于View本身来说,这三个条件都是不成立的,这点可以在View初始化的时候通过获取的attr来看到,View的子类中

对于EditText来说这三个条件会成立,手指点击EditText的离开的时候,取得了焦点第三个条件就会成立;

成立了的话,通过requstFouse返回值就是true,否则的话,这个地方focusTaken 会一直是false;

11行代码mHasPerformedLongPress的这个值只有长按监听里面的 onLongClick方法返回的是true,这个值才是true;否则的话这个值就是false;

true的话这个条件不执行,用户的onLongClick方法执行了且返回了false,那么后续还是会执行以下单击监听事件,~~~

16行 会进入这个if条件 除非是EditText,

20~24行代码是 要执行点击监听事件

    private final class PerformClick implements Runnable {
        public void run() {
            performClick();
        }
    }

看23行如果这个任务投放到消息队列里面没有被执行,会返回值false,通过本地再执行一次,如果返回了true,那么24行代码就不会执行了;

29行以后的代码 主要就是刷新状态 和背景的内容,前面都有分析和解释,也是很容易看懂的;

好了,Veiw的事件传递的代码,就分析了一遍,希望对每一个看了这个源代码的人都有帮助;



 


 



 



 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android View 是 Android 中最基本的 UI 构建块之一,负责在屏幕上绘制视图并响应用户的操作。下面是一个简单的 View 码分析过程: 1. 首先,我们需要了解 View 的继承关系。View 是 Android 中所有 UI 组件的基类,它的直接子类包括 ViewGroup、TextView、ImageView 等。其中,ViewGroup 又是各种布局容器的基类,例如 LinearLayout、RelativeLayout 等。 2. 接着,我们可以查看 View 的基本属性。这些属性包括 layout_width、layout_height、padding、background 等。其中,layout_width 和 layout_height 决定了 View 在布局中的大小,padding 指定了 View 的内边距,background 则是 View 的背景。 3. View 的绘制过程可以分为两个阶段:测量和绘制。在测量阶段,View 会根据其 layout_width 和 layout_height 等属性计算出自身的尺寸。在绘制阶段,View 会将自身绘制到屏幕上。 4. View事件响应机制是 Android 中 UI 开发的重要部分。当用户触摸屏幕时,系统会将事件传递ViewView 会根据自身的点击区域判断是否响应该事件,并将事件传递给其父容器或下一个 View 进行处理。 5. 最后,我们可以查看 View码实现,深入了解 View 的内部实现逻辑。例如,View 的测量和绘制过程是通过 onMeasure 和 onDraw 方法实现的,事件响应机制是通过 onTouchEvent 和 dispatchTouchEvent 方法实现的。 总的来说,理解 Android View码实现可以帮助我们更好地理解 Android UI 开发的工作原理,从而编写出更高效、更灵活、更具交互性的应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值