Android随笔随想-GUI-触摸事件分发

Android随笔随想-GUI-触摸事件分发

基于Android2.3的随笔分析

1. 随笔

1.1 触摸事件的产生

触摸事件的产生等背景资料以及android底层的处理,可以参照本随笔的最后部分中的资料

1.2 触摸事件与ViewRoot的关联

在上篇中,提到了ViewRoot在setView时的几个操作,现在我们稍微回顾一下:

ViewRoot.setView()

/**
 * We have one child
 */
public void setView(View view, WindowManager.LayoutParams attrs,
        View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            mWindowAttributes.copyFrom(attrs);
            attrs = mWindowAttributes;
            if (view instanceof RootViewSurfaceTaker) {
                mSurfaceHolderCallback =
                        ((RootViewSurfaceTaker)view).willYouTakeTheSurface();
                if (mSurfaceHolderCallback != null) {
                    mSurfaceHolder = new TakenSurfaceHolder();
                    mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
                }
            }
            Resources resources = mView.getContext().getResources();
            CompatibilityInfo compatibilityInfo = resources.getCompatibilityInfo();
            mTranslator = compatibilityInfo.getTranslator();

            if (mTranslator != null || !compatibilityInfo.supportsScreen()) {
                mSurface.setCompatibleDisplayMetrics(resources.getDisplayMetrics(),
                        mTranslator);
            }

            boolean restore = false;
            if (mTranslator != null) {
                restore = true;
                attrs.backup();
                mTranslator.translateWindowLayout(attrs);
            }
           ......

            if (!compatibilityInfo.supportsScreen()) {
                attrs.flags |= WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
            }

            mSoftInputMode = attrs.softInputMode;
            mWindowAttributesChanged = true;
            mAttachInfo.mRootView = view;
            mAttachInfo.mScalingRequired = mTranslator != null;
            mAttachInfo.mApplicationScale =
                    mTranslator == null ? 1.0f : mTranslator.applicationScale;
            if (panelParentView != null) {
                mAttachInfo.mPanelParentWindowToken
                        = panelParentView.getApplicationWindowToken();
            }
            mAdded = true;
            int res; /* = WindowManagerImpl.ADD_OKAY; */

            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            requestLayout();
            mInputChannel = new InputChannel();
            try {
                res = sWindowSession.add(mWindow, mWindowAttributes,
                        getHostVisibility(), mAttachInfo.mContentInsets,
                        mInputChannel);
            } catch (RemoteException e) {
                mAdded = false;
                mView = null;
                mAttachInfo.mRootView = null;
                mInputChannel = null;
                unscheduleTraversals();
                throw new RuntimeException("Adding window failed", e);
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }

            if (mTranslator != null) {
                mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets);
            }
            mPendingContentInsets.set(mAttachInfo.mContentInsets);
            mPendingVisibleInsets.set(0, 0, 0, 0);
            if (Config.LOGV) Log.v(TAG, "Added window " + mWindow);
            if (res < WindowManagerImpl.ADD_OKAY) {//handle exception
               ......
            }

            if (view instanceof RootViewSurfaceTaker) {
                mInputQueueCallback =
                    ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
            }
            if (mInputQueueCallback != null) {
                mInputQueue = new InputQueue(mInputChannel);
                mInputQueueCallback.onInputQueueCreated(mInputQueue);
            } else {
                InputQueue.registerInputChannel(mInputChannel, mInputHandler,
                        Looper.myQueue());
            }

            view.assignParent(this);
            mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0;
            mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0;
        }
    }
}

在上面核心的操作有两个

  1. 将mWindow通过Wms的client的proxy端,设置给了Wms
  2. 注册InputQueue的callback-InputHandler和InputQueue的InputChannel(InputChannel的功能,个人未知)

InputHandler用于接收系统输入系统的通知.在系统收到触摸和按键消息时,会通过这个callback交给App来处理

private final InputHandler mInputHandler = new InputHandler() {
    public void handleKey(KeyEvent event, Runnable finishedCallback) {
        startInputEvent(finishedCallback);
        dispatchKey(event, true);
    }

    public void handleMotion(MotionEvent event, Runnable finishedCallback) {
        startInputEvent(finishedCallback);
        dispatchMotion(event, true);
    }
};

系统在决定将事件交给我们的app来处理时

  1. 触摸事件,会回调handleMotion()的方法
  2. 按压事件,会回调handleKey()的方法

那么我们接下来顺着这条线,往下找即可

1.3 触摸事件的分发处理

我们以ACTION_DOWN为例,分析触摸事件的分发处理
先来整体的看下触摸事件处理分发的序列图

1.3.1 整体序列图

系统触摸处理时序图

1.3.2 ViewRoot中的控制分发

ViewRoot.mInputHandler.handleMotion

 public void handleMotion(MotionEvent event, Runnable finishedCallback) {
        startInputEvent(finishedCallback);
        dispatchMotion(event, true);
    }
  1. 标记了事件的callback
  2. 交给ViewRoot进行处理,注意,这里的第二个参数是true,也就是这种事件,都需要发送确认的消息

ViewRoot.dispatchMotion()

private void dispatchMotion(MotionEvent event, boolean sendDone) {
    int source = event.getSource();
    if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
        dispatchPointer(event, sendDone);
    } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
        dispatchTrackball(event, sendDone);
    } else {
        // TODO
        Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
        if (sendDone) {
            finishInputEvent();
        }
    }
}
  1. 判断了事件的来源,是手指按压,还是轨迹球
  2. 如果是手指按压,dispatchPointer()
  3. 如果是轨迹球,dispatchTraceball
  4. 其他情况,直接不支持,给运行事件的callback即可

    我们的是按压ACTIO_DOWN,因而走的是dispatchPointer()

ViewRoot.disatchPointer()

private void dispatchPointer(MotionEvent event, boolean sendDone) {
    Message msg = obtainMessage(DISPATCH_POINTER);
    msg.obj = event;
    msg.arg1 = sendDone ? 1 : 0;
    sendMessageAtTime(msg, event.getEventTime());
}

系统交给我们要处理的事件,也是通过handler的方式来处理

ViewRoot.handleMessage()

 ...... 
 case DISPATCH_POINTER: {
        MotionEvent event = (MotionEvent) msg.obj;
        try {
            deliverPointerEvent(event);
        } finally {
            event.recycle();
            if (msg.arg1 != 0) {
                finishInputEvent();
            }
            if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
        }
    } break;
    ......
  1. 直接deliverPointerEvent()
  2. 将事件回收
  3. 如果是需要sendDone,那么通知finishInputEvent

在这里的sendDone以及event的回收我们就不再看了,感兴趣的自己可以看看

ViewRoot.deliverPointerEvent()

private void deliverPointerEvent(MotionEvent event) {
    //1. 屏幕尺寸的转换
    if (mTranslator != null) {
        mTranslator.translateEventInScreenToAppWindow(event);
    }

    boolean handled;
    if (mView != null && mAdded) {
          //2. 如果是down事件,保证是touchMode
        // enter touch mode on the down
        boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
        if (isDown) {
            ensureTouchMode(true);
        }
        ......
        if (mCurScrollY != 0) {
            event.offsetLocation(0, mCurScrollY);
        }
        if (MEASURE_LATENCY) {
            lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
        }
        //3. 交给DecorView做处理
        handled = mView.dispatchTouchEvent(event);
        if (MEASURE_LATENCY) {
            lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
        }
        //4. 如果DecorView没有处理,并且是down的事件,那么将事件进行一些边界的处理,如果有合适的view,再交给DecorView做处理
        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. 屏幕尺寸的转换(大小屏幕的处理,假如说当前app并不是显示全屏的,而是以某种比例来显示的,可以采取这样的方式,将我们实际屏幕的坐标转为app中的坐标,之前小米的大屏幕手机显示小屏幕模式,从这里来看,最主要的更改也就是这里的屏幕尺寸转换)
  2. 如果是down事件,保证是touchMode(此时会通知wms,touchMode发生了更改,并且会寻找当前窗口的焦点,然后requestFocus,处理逻辑忽略,因为对事件分发没有影响)
  3. 交给DecorView做处理
  4. 如果DecorView没有处理,并且是down的事件,那么将事件进行一些边界的处理,如果有合适的view,再交给DecorView做处理

从上述的分析来看,目前对我们的流程跟踪有效的是DecorView.dispatchTouchEvent(),即第三步

小总结一下

  1. ViewRoot与输入系统的结合是ViewRoot的InputChannel和InputHandler,InputHandler用于接收输入系统的回调通知
  2. ViewRoot以Handler的方式来接收和处理InputHandler的事件
  3. ViewRoot在具体要处理这个Event时,如果需要,会做屏幕尺寸的转换;down事件,会通知Wms,本地也会做更改;交给DecorView来处理;如果是down事件,并且没有处理,那么会边界的处理,如果调整的范围内,能够找到合适的view,还是会再次交给DecorView来处理

那么下面,我们继续跟踪DecorView的dispatchTouchEvent()即可

1.3.3 DecorView体系的处理

DecorView.dispatchTouchEvent()

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

DecorView是继承自FrameLayout的自定义view,这里直接根据是否有callback和mFeatureId的值,决定是给callback,还是直接处理

对于Activity,是符合callback != null 并且 mFeatureId<0的,因而是给callback.dispatchTouchEvent()来处理的.在前面的随笔中,也提到过,构建Activity时,PhoneWindow的callback是Activity,那么直接查看Activity的dispatchTouchEvent即可

Activity.dispatchTouchEvent()

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 * 
 * @param ev The touch screen event.
 * 
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
  1. ACTION_DOWN事件,通知onUserInteraction()(可以看看onUserInteraction()的说明,用户到底有没有操作这个Activity,可以在这里做处理)
  2. 获取window,然后调用superDispatchTouchEvent()来消费,如果消费了,返回true,没有消费,继续
  3. 调用Activity的onTouchEvent()

    从分析上看,我们查看第二步和第三步即可

PhoneWindow.superDispatchTouchEvent()

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

直接是交给了DecorView处理,继续跟踪

DecorView.superDispatchTouchEvent()

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

DecorView也没有做什么特殊处理,直接是调用了ViewGroup的dispatchTouchEvent()方法,变为了普通的viewgroup处理,暂时我们先不继续的看ViewGroup的体系,下个chapter分析这个,前面Activity的部分还没有结束.

Activity.onTouchEvent()

 /**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 * 
 * @param event The touch screen event being processed.
 * 
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    return false;
}

默认Actiivty是不做事件的处理的,因而是直接返回false的

小总结一下

从ViewRoot交给DecorView,DecorView开始了自己的事件分发的道路,在这里

  1. DecorView是直接交给Activity来处理
  2. Activity通知Activity端,onUserInteraction()
  3. Activity交给PhoneWindow来处理(PhoneWindow也间接的交给了真正的View体系来处理这个事件)
  4. 如果PhoneWindow没有处理,那么Activity自己便消费了

从上面便可以看出,事件是DecorView自己主动将事件交给Activity来处理的,因而事件的体系可以这么理解.
根布局DecorView,自己主动派发事件给Activity,Activity可以认作是拦截器,默认情况下,拦截器只是间接的将事件转发给DecorView做正常的View体系的事件分发即可

1.3.4 View体系的处理

view体系的处理分为两部分,一部分是ViewGroup的控制分发,另外一部分是View的控制分发处理

1.3.4.1 ViewGroup的控制分发

下图为ViewGroup控制分发的图示

ViewGroup事件分发流程图

ViewGroup.dispatchTouchEvent()

/**
 * {@inheritDoc}
 */
@Override
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. 如果filterTouchEventForSecurity不是安全的,那么直接返回false(可以查看下源码,窗口在模糊时,返回的是false,具体发生情况需要查询资料)
  2. 获取GroupFlags,是否不允许拦截,赋值给disallowIntercept
  3. 如果是ACTION_DOWN,清空MotionTarget,如果disallowIntercept为true或者自身的viewGroup不做拦截即onINterceptTouchEvent为false,开始寻找viewGroup的直接孩子节点作为目标对象.倒序遍历,判断子view中是否包含点击的位置,如果包含,则将事件的坐标转换为子view相对于父view,即当前view的坐标,并且给子view设置CANCEL_NEXT_UP_EVENT,此时调用子view的派发事件方法,dispatchTouchEvent(),如果子view消费了,那么设置mMotionTarget为子view,返回true.如果没有,继续遍历.
  4. 判断当前事件是否是upOrCancel,保存到isUpOrCancel,如果是,GroupFlag中设置为ALLOW_INTERCEPT,影响下次的事件分发
  5. 如果targetView为null,那么将viewGroup作为普通的view,直接调用super.dispatchTouchEvent,然后返回调用的值,在此之前,会判断CANCEL_NEXT_UP_EVENT的值,自己并不会记录motion_target,但是viewGroup的父view会记录这个值
  6. 如果是允许拦截,并且viewgroup的拦截返回了true,表示确实要拦截这个事件,那么给targetView手动传递一个cancel的事件,并且主动调用targetView的dispatchTouchEvent(),清空MotionTarget,返回true
  7. 再次判断isUpOrCancel,清空motionTarget
  8. 到这里时,target != null , 此时,将坐标转为targetView的坐标,如果有CANCEL_NEXT_UP_EVENT的标记,将事件转为ACTION_CANCEL,删除target的mRipvateFlags,清空mMotionTarget的值
  9. 调用target的分发事件方法

逻辑简单图示

View的事件控制分发

备注: 此图缺少disallowIntercept的逻辑

小总结一下

  1. viewGroup在分发时,在寻找目标时,是ACTION_DOWN的事件,如果子view没有消费ACTION_DOWN,并且自身也没有消费ACTION_DOWN,那么接下来的事件,就不再传递给此viewGroup
  2. 子view可以请求父view,不做拦截的处理,这样子view的处理优先级是高于父view的,从ACTION_DOWN的处理便知.disallowIntercept的判断在前
  3. dispatchTouchEvent 用于控制分发事件的流程,onInterceptTouchEvent()用于表示是否拦截事件,onTouchEvent()表示具体的事件消费
  4. view体系在控制分发时,均为父view,保存子view作为motionTargetView,如果自己作为targetView,是由父view保存状态的
1.3.4.2 View的控制分发

View.dispatchTouchEvent()

/**
 * 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 (!onFilterTouchEventForSecurity(event)) {
        return false;
    }

    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
  1. 校验touchEvent是否是安全的,如果不是,返回false
  2. 在view是enable的,并且mOnTouchListener不是null时,回调mOnTouchListener的onTouch方法,如果mOnTouchListener返回true,那么直接返回true,否则,继续
  3. 返回View的onTouchEvent方法

小总结一下

  1. 我们在设置了普通view的onTouchListener,并且返回true,那么view的onTouchEvent()是不会调用的
  2. 在view的onTouchEvent()消费了down事件,并且父view,将此事件都传递过来时,onTouchEvent()是一直在调用的.

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. 判断是否是enabled,如果不是,返回clickable与long_clickable的结果,并不是直接返回的false
  2. 如果mTouchDelegate不为null,并且onTouch消费了这个事件,直接返回true
  3. 在view是clickable或者是long_clickable的情况下,按照事件的流程来分析这部分

    • ACTION_DOWN:

      1. 构建了一个CheckForTap的类,用于检测点击事件
      2. view的privateTag中,添加Prepressed的标记,并且设置mHasPerformedLongPress为false
      3. 通过view内置的handler,post一个tabTimeout的一个检测点击事件的runnable: CheckForTab

从这里也可以看到,并不是我们在触摸到屏幕时,假如当前view会消费这个事件,便会直接显示按压的状态,而是有一个PREPRESSED的状态,接下来在到了tapTimeout的状态,才会处于按压的状态

  • ACTION_MOVE:

如果在我们的移动过程中,超过了v和slop的边界,那么便会:

  1. 移除tabCallback
  2. 如果当前处于PRESSED的状态,移除LONG_PRESSED的callback,回复privateflag的pressed状态为normal
  3. 更新drawable

    • ACTION_UP:

如果当前处于按压和PRESSED的状态下,才会做以下的处理,其他情况,不做处理

  1. 如果当前是focusable,,并且是focusableInTouchMode但是没有获取到焦点,那么会做焦点的获取,并且结果赋值给focusTaken
  2. 如果当前没有执行LongPress,移除LongPressCallback,即不再做longClickable的callback检测
  3. 如果当前的focusTaken是false,即当前我们是焦点,或者说requestFocus失败时,会执行mPerformClick或者直接执行performClick(在post失败后)
  4. 如果当前在prepressed的情况下,会更新privateFlags为PRESSED,更新为按压的图片,然后postDelay一个恢复的state
  5. 而不是4的情况,并且是post mUnsetPressedState失败时,会直接执行mUnsetPressedState
  6. 移除tap的callback

从上面也可以看出,对于在PREPRESSED和PRESSED的状态下,均会执行click的操作的:performClick()

  • ACTION_CANCEL:

    1. 更新privateTag的值为normal的,unpressed的
    2. 更新drawable的状态
    3. 移除tapCallback

在上面我们只是看到了设置按压状态的分析,并没有具体调用onClickListenener和onLongClickListener的具体代码
根据上面我们的线索,继续分析

CheckForTabClick

 private final class CheckForTap implements Runnable {
    public void run() {
        mPrivateFlags &= ~PREPRESSED;
        mPrivateFlags |= PRESSED;
        refreshDrawableState();
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            postCheckForLongClick(ViewConfiguration.getTapTimeout());
        }
    }
}
  1. 更新privateFlags的状态为PRESSED,移除PREPRESSED的状态
  2. 更新drawable的状态为按压
  3. 如果是long_clickable的状态,那么继续post一个longClick的检查,

在按压和move的状态下,我们看到并没有按压的事件处理,不是时间到了就处理,而是在up时performClick的类中进行的处理

PerformClick

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

View.performClick()

/**
 * Call this view's OnClickListener, if it is defined.
 *
 * @return True there was an assigned OnClickListener that was called, false
 *         otherwise is returned.
 */
public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }

    return false;
}
  1. 在mOnClickListener不为null时,播放声音,并且调用mOnClickListener.onClick()的方法,返回true
  2. 在mOnClickListener为null时,直接返回false

接着上述的CheckForTap,我们查看longClick的处理

View.postCheckForLongClick()

private void postCheckForLongClick(int delayOffset) {
    mHasPerformedLongPress = false;

    if (mPendingCheckForLongPress == null) {
        mPendingCheckForLongPress = new CheckForLongPress();
    }
    mPendingCheckForLongPress.rememberWindowAttachCount();
    postDelayed(mPendingCheckForLongPress,
            ViewConfiguration.getLongPressTimeout() - delayOffset);
}
  1. 标记mHasPerformedLongPress为false
  2. 构建CheckForLongPress的runnable对象,用于post后检测longPress
  3. mPendingCheckForLongPress.rememberWindowAttachCount记录window attach的次数
  4. 通过handler post longPress的处理

在时间到了之后,查看CheckForLongPress的runnable的实现

View.CheckForLongPress

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;
    }
}

判断当前是否是在按压,并且parent不为null,然后windowAttachCount也是相同的,如果performLongClick()为true,便设置为mHasPerformedLongPress为true

View.performLongClick()

 /**
 * Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the
 * OnLongClickListener did not consume the event.
 *
 * @return True if one of the above receivers consumed the event, false otherwise.
 */
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;
}
  1. 如果设置了longClickListener,调用mOnLongClickListener.onLongClick()方法,并且赋值给handled
  2. 如果没有handled,调用showContextMenu()
  3. 返回handled的结果

小总结一下

  1. view在触摸状态下,有三个状态

1) PREPRESSED
2) PRESSED
3) LONG_PRESSED

  • 在PRESSED和PRESSED的区别在于,是否显示的是按压
  • view的事件在enabled为true,并且有clickable或者long_cliable,便可以消费事件,onTouchEvent便可以返回true,即不一定是要有OnClickListener或者OnLongClickListener的
  • 在没有long_click或者long_click的处理返回false时,单击事件还是可以触发的

从这里我们也看到了,ListView的onItemClickListener不是只有listView自身可以触发,在内部view收到long_click时,但是view自身没有消费时,也是可以触发的(具体可以查看showContextMenu()的实现).

2. 随想

2.1 事件分发处理,是至顶而下分发,至顶而下拦截,默认情况下自下而上消费

2.2. TouchDelegate为什么要设置给ParentView,而不是自己

从ViewGroup的contains方法来看,在做点击事件的包含时,并没有delegate的相关调用,因而,这儿区域的扩大,可能是有问题的,因而做法是给view的parent设置touchDelegate,并且父View是enabled,这样才能接收到这个事件.

3. 相关资源

Android之Input子系统事件分发流程

图解android系列-Android的用户输入处理

柯元旦-android内核剖析-Ch13 View工作原理

posted @ 2017-04-05 09:01  Panda Pan  阅读(8)  评论(0编辑  收藏  举报