View工作原理之触摸消息派发过程

1、触摸消息总体派发过程

和按键消息派发过程类似,当底层读取到触摸消息,会回调ViewRoot内部的mInputHandler对象的dispatchMotion()方法来发送一个异步消息,该消息被函数deliverPointerEvent()处理。执行完该函数后,调用finishInputEvent()向消息获取模块发送一个回执,以便进行下一次消息派发。

下面来分析deliverPointerEvent()函数的具体过程,代码如下:

    private void deliverPointerEvent(MotionEvent event) {
        if (mTranslator != null) {
            mTranslator.translateEventInScreenToAppWindow(event);
        }
        
        boolean handled;
        if (mView != null && mAdded) {

            // enter touch mode on the down
            boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
            if (isDown) {
                ensureTouchMode(true);
            }
            if(Config.LOGV) {
                captureMotionLog("captureDispatchPointer", event);
            }
            if (mCurScrollY != 0) {
                event.offsetLocation(0, mCurScrollY);
            }
            if (MEASURE_LATENCY) {
                lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
            }
            handled = mView.dispatchTouchEvent(event);
            if (MEASURE_LATENCY) {
                lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
            }
            if (!handled && isDown) {
                int edgeSlop = mViewConfiguration.getScaledEdgeSlop();

                final int edgeFlags = event.getEdgeFlags();
                int direction = View.FOCUS_UP;
                int x = (int)event.getX();
                int y = (int)event.getY();
                final int[] deltas = new int[2];

                if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
                    direction = View.FOCUS_DOWN;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
                    direction = View.FOCUS_UP;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                    direction = View.FOCUS_RIGHT;
                } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                    direction = View.FOCUS_LEFT;
                }

                if (edgeFlags != 0 && mView instanceof ViewGroup) {
                    View nearest = FocusFinder.getInstance().findNearestTouchable(
                            ((ViewGroup) mView), x, y, direction, deltas);
                    if (nearest != null) {
                        event.offsetLocation(deltas[0], deltas[1]);
                        event.setEdgeFlags(0);
                        mView.dispatchTouchEvent(event);
                    }
                }
            }
        }
    }

1、进行物理像素到逻辑像素的转换。

2、如果是Down消息,调用ensureTouchMode(true)进入触摸模式

3、event.offsetLocation(0, mCurScrollY)方法中,mCurScrollY记录了视图在屏幕坐标中的Y轴滚动,该方法将屏幕坐标转换为视图坐标

4、调用mView.dispatchTouchEvent(event)将消息派发给根视图,该函数内部会把消息派发到整个View树。对于Activity包含的窗口,根视图就是PhoneWindow中的DecorView,而对于非应用窗口,根视图就是一个普通的ViewGroup。

5、如果根视图及子视图没有处理该消息,最后进行屏幕偏移,如果偏移后,匹配到了新视图,则将消息派发到该视图。

2、根视图内部消息派发过程

对于mView.dispatchTouchEvent()的派发过程。该函数是在ViewRoot中的deliverPointerEvent()方法中调用的。mView的类型是PhoneWindow中的DecorView或普通的ViewGroup。

下面贴出DecorView中dispatchTouchEvent 中的代码:

        public boolean dispatchTouchEvent(MotionEvent ev) {
            final Callback cb = getCallback();
            return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
                    .dispatchTouchEvent(ev);
        }

首先判断是否存在CallBack对象,这个对象就是Activity对象,如果有,则调用Activity的dispatchTouchEvent(),否则调用DecorView基类ViewGroup中的dispatchTouchEvent()。

在Activity中的dispatchTouchEvent()代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

(1)如果是DOWN消息,调用onUserInteraction(),便于在消息处理前做点什么。

(2)getWindow()返回的是PhoneWindow对象,调用superDispatchTouchEvent(ev),也就是将消息派发给根视图,继而派发到整个View树。

(3)如果Window类没有处理该消息,则调用onTouchEvent(),onTouchEvent()默认什么也没做。

3、ViewGroup内部消息派发过程

ViewGroup的内部处理逻辑也采用递归方式,但与按键处理的方式稍有不同。

触摸消息首先会发给View树的最后一个子视图,如果子视图没有处理,再派发给它的父视图处理。和按键消息的派发过程正好相反。

下面是ViewGroup的dispatchTouchEvent()方法,代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }

        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;

        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;

                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);
        }

        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        return target.dispatchTouchEvent(ev);
    }

(1)将布局坐标转换为视图坐标

(2)处理DOWN消息,判断视图坐标落在哪个子视图。

disallowIntercept代表不允许拦截触摸事件

如果disallowIntercept为true,则直接将消息传递给子视图,否则先调用onInterceptTouchEvent(),当该方法返回false时,也就是事件没有被用户拦截,则继续将事件传递给子视图。

然后遍历寻找子视图,调用child.getHitRect(frame),给frame赋值视图坐标范围,调用frame.contains(scrolledXInt, scrolledYInt)判断用户触摸点是不是落在了该视图上,如果是,通过ev.setLocation(xc, yc)给MotionEvent设置调整坐标,然后把事件传递给该子视图。

需要注意的是,这里遍历子视图,是从最顶层子视图开始的。对于找到的这个子视图调用child.dispatchTouchEvent(ev),如果事件被消费了,则会返回true,紧接着mMotionTarget = child,这样触摸事件的目标就找到了,最后调用target.dispatchTouchEvent(ev),把事件派发给这个View。如果返回的是false,说明这个child不是常规的view,而是一个ViewGroup,则继续进行递归调用,递归到最后,child的数量为0,则把这个ViewGroup也当成一个常规的View,调用super.dispatchTouchEvent(ev),把消息派发给了这个ViewGroup,这样你的ViewGroup也能执行onTouchEvent()等事件了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值