第18天 Android Touch事件学习 5 点击与长按原理


这是事件学习的第四篇文章:

《Andorid Touch事件学习 1 点击事件》

《Android Touch事件学习 2 触发点击事件的地方》

《Android Touch事件学习 3 区分各种手势基础知识》

《Android Touch事件学习 4 获取手指触摸位置》


    在第一篇文章中又点击事件的一个例如引入事件的学习,之后第二篇文章查找一下点击事件最终是在什么地方触发的,发现是在onTouchEvent方法中,第三篇和第四篇总结了一下onTouchEvent的参数MotionEvent对象的常用属性getAction() 与 getX(), getY()。

    前几篇是打下基础,现在可以基于这些知识分析下View.onTouchEvent也就是之前第二篇文章中查找到的发现点击事件触发的地方,View类是所有视图的基类,也就是如果子类不覆写此方法的话,触屏事件都是交由View.onTouchEvent处理。


    分析的源码是Andorid 4.0 (Android 14)原因之前也解释过,这种通过的功能因为各种版本变化不会太大,View.onTouchEvent有130行肯定不会直接粘贴到blog上然后一行行的解释是什么意思,打算按照先总后分的形式整理。这篇文章并不是我想写这么长,实在是方法本身源码就很多,。


一、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) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
                // 如果抬起手指清除掉按下状态
                mPrivateFlags &= ~PRESSED;
                // 当前显示还是按下状态,所以重刷一下
                refreshDrawableState();
            }
			
            // 应该是当前视图处理当前触摸事件的,但是因为指定为禁用状态,
            // 所以还是消耗当前事件,只是不做任何处理。
            // 这样处理符合逻辑,因为用户当前触摸的是这个视图,虽然被设置为不触发任何处理。
            // 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) {
            // 事件虽然传递给了当前视图,
            // 但是如果其他视图通过设置mTouchDelegate增大触摸区域
            // 并且当前触摸点在其他视图的扩大区域内。
            if (mTouchDelegate.onTouchEvent(event)) {
                // 交由扩大触摸区域的那个视图处理
                // 并消耗此次触摸事件
                return true;
            }
        }

        // 如果当前视图是点击或者长按状态
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                // 点击与长按重要处理的地方,之后着重分析这块
                ......
            }
			
            // 是点击或者长按,当前方法反馈与处理用户操作
            return true;
        }
	
        // 当前视图不处理,由事件传递机制在找其他匹配的视图
        // 具体如何查找之后在分析
        return false;
    }

在看针对各种动作的判断与处理之前在整理一下以上源码的结构:

1. 当前视图是否处于禁用状态(如果是抬起手指,清理掉按下状态)
2. 是否在其他视图的扩大范围内(通过TouchDelegate实现)
3. 如果以上两者都不成立,并且当前视图处于点击或者长按状态
4. 如果以上3者都不符合条件,返回false表明当前视图不消耗此次触摸事件


以上源码会涉及到以下知识,可以查看这些变量的注释,就不一一贴出源码了:

1. mViewFlags是全局变量,用于存放视图状态信息。

2. TouchDelegate 设置视图的点击区域(增大或者缩小可点击区域)

3.  DISABLED 当前视图禁用状态。通过使用View.setEnabled(false)设置视图为禁用状态

4. CLICKABLE 可点击。通过使用View.setClickable(true)设置

5. LONG_CLICKABLE 可长按。通过使用View.setLongClickable(true)设置



二、点击与长按手势处理

    在第三篇文章《Android Touch事件学习 3 区分各种手势基础知识》中分析过,Android通过各种ACTION(动作)来区分用户行为,然后现在需要通过系统提供的这些ACTION来判断是点击还是长按,根据之前的经验ACTION的执行是有先后顺序的依次是:ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL,且当用户手指按下时触发ACTION_DOWN,手指抬起时触发ACTION_UP,移动时被拦截触发ACTION_CANCEL,这三个都是仅会触发依次,手指移动是触发ACTION_MOVE会根据手指移动事件执行0到多次。  View.onTouchEvent中并不是一这个次序来摆放源码的,但是下面会依次按照上面的顺序分块进行解释。


三、 ACTION_DOWN

case MotionEvent.ACTION_DOWN:
	mHasPerformedLongPress = false;

	// 1. 必须先满足是鼠标右键 或是 手写笔第一个按钮,才会返回true
	if (performButtonActionOnTouchDown(event)) {
		break;
	}

	// 2. 当前视图是否可滚动(例如:当前是ScrollView视图,返回true)
	// Walk up the hierarchy to determine if we‘re inside a scrolling container.
	boolean isInScrollingContainer = isInScrollingContainer();

	// For views inside a scrolling container, delay the pressed feedback for
	// a short period in case this is a scroll.
	if (isInScrollingContainer) {
		// 滚动视图内,先不设置为按下状态,因为用户之后可能是滚动操作
		// 不是此次分析的重点,感兴趣可以自己了解下
		mPrivateFlags |= PREPRESSED;
		if (mPendingCheckForTap == null) {
		    mPendingCheckForTap = new CheckForTap();
		}
		postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
	} else {
		// Not inside a scrolling container, so show the feedback right away
		// 不在滚动视图内,立即反馈为按下状态
		mPrivateFlags |= PRESSED;
		// 刷新为按下状态
		refreshDrawableState();
		// 3. 与 4.
		checkForLongClick(0);
	}
	break;


以上标注了1,2,3,4,如果感觉注释已经看懂了可以直接忽略以下关于这几点的源码注释,直接跳到下一个动作ACTION_MOVE


1. performButtonActionOnTouchDown方法源码

    /**
     * Performs button-related actions during a touch down event.
     *
     * @param event The event.
     * @return True if the down was consumed.
     *
     * @hide
     */
    protected boolean performButtonActionOnTouchDown(MotionEvent event) {
        // 如果是鼠标右键,手写笔第一个按钮(详见BUTTON_SECONDARY常量注释)
        if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
            if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
                return true;
            }
        }
        return false;
    }


2. isInScrollingContainer方法源码
    /**
     * @hide
     */
    public boolean isInScrollingContainer() {
        ViewParent p = getParent();
        while (p != null && p instanceof ViewGroup) {
            if (((ViewGroup) p).shouldDelayChildPressedState()) {
                return true;
            }
            p = p.getParent();
        }
        return false;
    }

是否在滚动容器中,详见ViewGroup.shouldDelayChildPressedState(),注释说处于兼容问题这个方法默认返回true,但是在所有不可滚动的子类。
例如LinearLayout等所有不会滚动的视图都会覆写此方法并返回false。


3. checkForLongClick方法源码

    private void checkForLongClick(int delayOffset) {
        // 当前视图可以执行长按操作
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                // 4 与 5
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            // 4. 
            mPendingCheckForLongPress.rememberWindowAttachCount();
            // 延迟一段时间把runnable添加到消息队列
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }


ViewConfiguration.getLongPressTimeout()  Android 4.0 源码值是500



4.CheckForLongPress类源码

    class CheckForLongPress implements Runnable {

        private int mOriginalWindowAttachCount;

        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                // 5. 触发执行长按事件
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }
    }	


这里很容易搞不清楚,这里竟然还判断
mOriginalWindowAttachCount == mWindowAttachCount
明明在CheckForLongPress.rememberWindowAttachCount()中进行赋值的,解释一下mOriginalWindowAttachCount是CheckForLongPress的内部变量,而mWindowAttachCount是全局变量,CheckForLongPress的对象是延时发送到消息队列的,也就是说如果在延迟期间mWindowAttachCount改变这个判断条件还是过不了,那这个变量都在哪里会发生改变呢?不贴源码了,会在View.dispatchAttachedToWindow方法中进行累加,而此方法会在ViewGroup.addView时调用,也就是再次期间添加视图的话,不会满足条件。


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


四、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
	// 1. 触摸点是否在当前视图内
	if (!pointInView(x, y, mTouchSlop)) {
		// Outside button
		// 如果手指移出视图区域
		
		// 2. 去除视图轻触状态
		removeTapCallback();
		
		// 如果当前是按下状态
		if ((mPrivateFlags & PRESSED) != 0) {
			// Remove any future long press/tap checks
			// 3. 移除还没有执行的长按与轻触检测
			removeLongPressCallback();

			// Need to switch from pressed to not pressed
			// 移除按下状态
			mPrivateFlags &= ~PRESSED;
			// 移除后刷新视图
			refreshDrawableState();
		}
	}
	break;


    与之前一样,以下是上面ACTION_MOVE处理调用的方法,如果已经明白可以直接忽略以下方法与注释,直接看ACTION_UP的分析



1. pointInView方法源码

    /**
     * Utility method to determine whether the given point, in local coordinates,
     * is inside the view, where the area of the view is expanded by the slop factor.
     * This method is called while processing touch-move events to determine if the event
     * is still within the view.
     */
    private boolean pointInView(float localX, float localY, float slop) {
        // 把触摸点的x,y 与当前视图的上下左右进行比较,看是否在视图区域内
        // 视图区域的上下左右都增加slop长度,在视图外添加的slop区域内也算点击到视图内了
        // 目的是为了增加当前视图的可点击区域,避免在视图边界处,即使移动一丁点就会
        // 系统可能就会认为是在两个视图间切换
        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                localY < ((mBottom - mTop) + slop);
    }	
	
    // 在View视图构造器中对mTouchSlop进行初始化
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();


2.removeTapCallback源码

    /**
     * Remove the tap detection timer.
     */
    private void removeTapCallback() {
        // 移除轻触探测定时器
	
        // 此Runnable是在滚动视图是才会创建
        if (mPendingCheckForTap != null) {
            // 去除预按下状态
            mPrivateFlags &= ~PREPRESSED;
            // 从消息队列中删除mPendingCheckForTap
            removeCallbacks(mPendingCheckForTap);
        }
    }

3.removeLongPressCallback方法源码

    /**
     * Remove the longpress detection timer.
     */
    private void removeLongPressCallback() {
        // 移除长按探测定时器
	
        if (mPendingCheckForLongPress != null) {
          removeCallbacks(mPendingCheckForLongPress);
        }
    }


五、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 (prepressed) {
			// The button is being released before we actually
			// showed it as pressed.  Make it show the pressed
			// state now (before scheduling the click) to ensure
			// the user sees it.
			// 如果是与按下状态,抬起手指前设置为按下状态
			mPrivateFlags |= PRESSED;
			refreshDrawableState();
	   }

		// 如果没有执行长按
		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();
				}
				// 使用post到runnable发送到消息队列的目的是:
				// 消息队列是依次执行,把之前post到队列的runnable执行完
				// 才会执行当前runnable,以保证在之前所有状态都处理完后执行
				if (!post(mPerformClick)) {
					// 如果执行不成功,必须保证触发点击事件
					// 所以直接调用PerformClick类内部调用的触发事件方法
					performClick();
				}
			}
		}

		if (mUnsetPressedState == null) {
			// 1. 清除按下状态
			mUnsetPressedState = new UnsetPressedState();
		}

		if (prepressed) {
			// 如果是预按下状态,过段事件后在发送到消息队列
			postDelayed(mUnsetPressedState,
					ViewConfiguration.getPressedStateDuration());
		} else if (!post(mUnsetPressedState)) {
			// If the post failed, unpress right now
			// 执行失败的话,保证视图不会永远处于按下状态
			// 直接执行一次
			mUnsetPressedState.run();
		}
		// 清除轻触
		removeTapCallback();
	}
	break;


1.UnsetPressedState 类源码

    private final class UnsetPressedState implements Runnable {
        public void run() {
            setPressed(false);
        }
    }	


六、ACTION_CANCEL

case MotionEvent.ACTION_CANCEL:
	// 清理按下状态
	mPrivateFlags &= ~PRESSED;
	// 刷新一下
	refreshDrawableState();
	// 清除轻触状态
	removeTapCallback();
	break;



ViewConfiguration是系统配置,各手机厂商可能会根据自身手机特点修改这些参数。


七、简单总结

1. 满足一些先决条件。例如:当前视图非禁用状态、当前视图允许点击或者长按(详见 一、onTouchEvent整体结构)

之后通过系统反馈的动作来进行判断

2. ACTION_DOWN:当前是否为滚动视图,如果不是,当前视图先显示为按下状态,且在500毫秒后执行长按操作。(详见 三、ACTION_DOWN)

3. ACTION_MOVE:如果手指移动出当前视图范围内,清理以上设置的所有状态,并且如果长按还没有执行不会触发。(详见 四、ACTION_MOVE)

4. ACTION_UP:如果MOVE时没有进行清理,且还没有执行长按操作,执行点击操作(详见 五、ACTION_UP)

5. ACTION_CANCEL:清理所有状态









第18天 Android Touch事件学习 5 点击与长按原理

上一篇:苹果Mac窗口布局整理工具:Tiles


下一篇:js制作论坛发帖