深入Android开发之--理解View#onTouchEvent

摘要:
=0){setPressed;}//Adisabledviewthatisclickablestillconsumesthetouch//events,itjustdoesn'trespondtothem.return;}如果此View有触碰事件处理代理,那么将此事件交给代理处理:?1234567891011121314151617181920212223mHasPerformedLongPress=false;if{break;}//Walkupthehierarchytodetermineifwe'reinsideascrollingcontainer.booleanisInScrollingContainer=isInScrollingContainer();//Forviewsinsideascrollingcontainer,delaythepressedfeedbackfor//ashortperiodincasethisisascroll.if{mPrivateFlags|=PFLAG_PREPRESSED;if{mPendingCheckForTap=newCheckForTap();}postDelayed;}else{//Notinsideascrollingcontainer,soshowthefeedbackrightawaysetPressed;checkForLongClick;}break;上面分支代码的第一个调用,performButtonActionOnTouchDown一般的设备都是返回false.因为目前的实现中,它是处理如鼠标的右键的.可以看下这个方法的源代码:?

一:前言

View是Android中最基本的UI单元.

当一个View接收到了触碰事件时,会调用其onTouchEvent方法.方法声明如下:

1
2
3
4
5
6
7
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
publicbooleanonTouchEvent(MotionEvent event) {

了解下View怎么处理onTouchEvent方法是很有必要的.

在具体的看View是怎么处理触碰事件之前,从用户交互上,我们先需要对View处理的事件有一些期望:

(1)能够区分将用户的触碰事件是点击还是滑动区别开来.

(2)能够将点击与长按区别开来.

二: 处理流程分析

View#onTouchEvent方法主要做了如下处理:

(1) 如果此view被禁用了. (如果是触碰完成事件则设置按下状态),然后返回是否可点击.

(中间的注释的意思为:一个可点击的View虽然禁用了,但是还是要把事件消耗掉,只是不响应它们而已.

1
2
3
4
5
6
7
8
9
if((viewFlags & ENABLED_MASK) == DISABLED) {
if(event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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));
}

(2) 如果此View有触碰事件处理代理,那么将此事件交给代理处理:

1
2
3
4
5
if(mTouchDelegate != null) {
if(mTouchDelegate.onTouchEvent(event)) {
returntrue;
}
}

(3)如果不可点击(既不能单击,也不能长按)则直接返回.false

(4)可点击时,处理触控事件.根据,按下,移动,取消,抬起,这些基本触摸事件来分别处理.

它们其中又有很强的关联性.

三:触摸事件分析:

(3.1)当触控开始时:即处理case MotionEvent.ACTION_DOWN:分支.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mHasPerformedLongPress = false;
if(performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
booleanisInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if(isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if(mPendingCheckForTap == null) {
mPendingCheckForTap = newCheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else{
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;

上面分支代码的第一个调用,performButtonActionOnTouchDown(event)一般的设备都是返回false.

因为目前的实现中,它是处理如鼠标的右键的.(如果此View响应或者其父View响应右键菜单,那么就此事件就被消耗掉了.)

可以看下这个方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Performs button-related actions during a touch down event.
*
* @param event The event.
* @return True if the down was consumed.
*
* @hide
*/
protectedbooleanperformButtonActionOnTouchDown(MotionEvent event) {
if((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if(showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
returntrue;
}
}
returnfalse;
}

对于MotionEvent的BUTTON_SECONDARY常量,对于鼠标中的按键来说,是指右键.

第二个方法调用:isInScrollingContainer(),

它的注释已经写得很明白了,就是遍历整个View树来判断当前的View是不是在一个滚动的容器中.

因为对于触碰事件的处理,我符合我们讲的,不能把滑动当前点击.所以先判断是不是在一个可滑动的容器中.

下面是此方法的实现代码:

1
2
3
4
5
6
7
8
9
10
publicbooleanisInScrollingContainer() {
ViewParent p = getParent();
while(p != null&& p instanceofViewGroup) {
if(((ViewGroup) p).shouldDelayChildPressedState()) {
returntrue;
}
p = p.getParent();
}
returnfalse;
}

检查结果有两种情况:

(1)如果不是在一个可滚动的容器中:

调用setPressed(true) 设置按下状态.,setPressed 主要是设置PFLAG_PRESSED标志位.后面会具体分析此方法.

检查长按.

(2)如果是在一个可滚动的容器中:

先设置用户准备点击这么一个标志位:PFLAG_PREPRESSED.

然后则发送一个延迟消息来确定用户到底是要滚动还是点击.

1
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

在给定的tapTimeout时间之内,用户的触摸没有移动,就当作用户是想点击,而不是滑动.

具体的做法是,将 CheckForTap的实例mPendingCheckForTap添加时消息队例中,延迟执行.

如果在这tagTimeout之间用户触摸移动了,则删除此消息.否则:执行按下状态.然后检查长按.

CheckForTap消息方法如下:

1
2
3
4
5
6
7
privatefinalclassCheckForTap implementsRunnable {
publicvoidrun() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true);
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}

检查长按也是大概类似的思路:等等再决定.

checkForLongClick方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
privatevoidcheckForLongClick(intdelayOffset) {
if((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if(mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = newCheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}

CheckForLongPress消息类要单独说一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classCheckForLongPress implementsRunnable {
privateintmOriginalWindowAttachCount;
publicvoidrun() {
if(isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if(performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
publicvoidrememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}

因为等待形成长按的过程中,界面可能发生变化如Activity的pause及restart,这个时候,长按应当失效.

View中提供了mWindowAttachCount来记录View的attach次数.当检查长按时的attach次数与长按到形成时.

的attach一样则处理,否则就不应该再当前长按. 所以在将检查长按的消息添加时队伍的时候,要记录下当前的windowAttachCount.

(3.2)当手指在屏幕移动时:case MotionEvent.ACTION_MOVE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<strong> caseMotionEvent.ACTION_MOVE:
finalintx = (int) event.getX();
finalinty = (int) event.getY();
// Be lenient about moving outside of buttons
if(!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;</strong>

第一句注释讲的是,对于触碰是否超出边界宽容一些.所以在判断触摸中间点是否在此View中时,先将上下左右增大mTouchSlop个像素,再判断.

如果在View的外面,将处理点击消息移除.如果是已经准备长按了,则将长按的消息移除.并将View的按下状态设置为false.

看看上面调用的pointInView的实现,如下:

1
2
3
4
5
6
7
8
9
10
/**
* 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.
*/
privatebooleanpointInView(floatlocalX, floatlocalY, floatslop) {
returnlocalX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}

(3.3) 精简代码再分析,加深理解:

到这里我们已经分析了一个View中的触控事件的.按下,和移动了.如果初次接触可能有点晕.但是让我以一个最简单的情况把这些上面出现过的代码重新组织一下:

我们的情况就是,一个Activity中只有一个正常的Button.

所以我们View处理触控事件的代码应该如下:

当手指按下时:

1
2
setPressed(true);
checkForLongClick(0);

设置按下的效果.派发一个消息侦查用户是否准备长按.

这个时候有两种情况:

一:用户手指按下过了一段时间.也没有到处移动,所以我们认为用户是想长按.触发执行长按消息:

1
2
3
4
5
6
if(isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if(performLongClick()) {
mHasPerformedLongPress = true;
}
}

这些代码,在上面分析的时候,提及到了一点.

下面再多说几句:

最外面的if判断.

主要是判断,长按是在按下的基础之上出现的.所以要isPressed(),

执行长按时,父View还在(指View层级还没有销毁),WindowAttachCount不变,指此窗口还是当初View按下时的窗口而不是重建的窗口.

最里面的判断,是判断View界面是否执行了长按,然后设置对应标志字段.

二:用户按下之后到处移动:

这个时候就执行了ACTION_MOVE分支的代码了.

if ((mPrivateFlags & PFLAG_PRESSED) != 0) 按照之前的执行流程.

因为,上面调用了setPressed(true).在些方法中,mPrivateFlags字段中的PFLAG_PRESSED标志为被启用了.

setPressed的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Sets the pressed state for this view.
*
* @see #isClickable()
* @see #setClickable(boolean)
*
* @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
* the View's internal state from a previously set "pressed" state.
*/
publicvoidsetPressed(booleanpressed) {
finalbooleanneedsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if(pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else{
mPrivateFlags &= ~PFLAG_PRESSED;
}
if(needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}

(3.4)触控完成时(即当手指都抬起来时):

这分支的代码加上注释看起来稍微有点长.我们分开来分析:

首先是检查PFLAG_PREPRESSEDPFLAG_PRESSED这两个标志.如果其中一个为真则处理.

根据上面的分析我们知道这两个标志位首先是在开始触控时(即手指按下ACTION_DOWN)时设置时,

PFLAG_PREPRESSED表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动.

PFLAG_PRESSED表示不是在一个可滚动的容器中,已经可以确定按下这一操作.

1
2
3
4
5
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED ) != 0;
booleanpressed = (mPrivateFlags & PFLAG_PRESSED) != 0;
if(pressed || prepressed){
// 处理些事件
}

然后是看是否需要获得焦点及用变量focusTaken设置是否获得了焦点.

如果我们还没有获得焦点,但是我们在触控屏下又可以获得焦点,那么则请求获得焦点.

1
2
3
4
5
// take focus if we don't have it already and we should in touch mode.
booleanfocusTaken = false;
if(isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

然后,如果之前是prepressed那么现在就设置按下状态:

虽然用户在我们还没有显示按下状态的效果时就不按了.我们还是得在进行实际的点击操作时,

让用户看到按下的效果.

1
2
3
4
5
6
7
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.
setPressed(true);
}

然后是判断是否进行了长按:

如果没有,那好,移除长按的延迟消息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 = newPerformClick();
}
if(!post(mPerformClick)) {
performClick();
}
}
}

下面是判断有没有重新请求获得焦点,如果还没有新获得焦点,说明之前已经是按下的状态了.

派发执行点击操作的消息.这是为了在实际的执行点击操作时,让用户有时间再看看按下的效果.

之后就是派发消息来取消点击状态:

1
2
3
4
5
6
7
8
9
10
11
12
if(mUnsetPressedState == null) {
mUnsetPressedState = newUnsetPressedState();
}
if(prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} elseif(!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();

ViewConfiguration.getPressedStateDuration() 获得的是按下效果显示的时间,由PRESSED_STATE_DURATION

常量指定,为64毫秒.

小结:其他的事件处理,基本是设置状态,派发消息.到这里就需要对,当前的状态,做出判断及处理.

(3.5) 接下来就是最简单的,但是也很重要的,当触控事件被系统取消:ACTION_CANCEL:

在这个事件中,只需要setPressed(false),并移除按下,及长按的延迟消息就可以了.

具体代码如下:

1
2
3
4
5
caseMotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;

四: 总结

View#onTouchEvent方法虽然只有几十行代码,但是对于我们理解触控事件的处理方法.

MotionEvent各个事件的处理方法都是有很大的帮助.

值得我们这这个方法的代码打印出来,多多学习.

免责声明:文章转载自《深入Android开发之--理解View#onTouchEvent》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇无法设置主体sa的凭据SQL语句insert into 不存在则插入,存在则修改下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

android学习和广告平台赚钱

[原创] android学习和广告平台赚钱 - Java,Android,Android学习,Android赚钱,广告平台 - mobile - ITeye论坛 前言: 2011年11月份我开始学习android软件开发(本人有良好的java基础、web开发基础、c++基础),2011年12月份开始开发第一款手机软件(软件名字就不透露了),2012年1...

点击页面div弹窗以外隐藏的两种思路

在本文为大家介绍两种思路实现点击页面其它地方隐藏该div,第一种是对document的click事件绑定事件处理程序.. 第一种思路分两步 第一步:对document的click事件绑定事件处理程序,使其隐藏该div 第二步:对div的click事件绑定事件处理程序,阻止事件冒泡,防止其冒泡到document,而调用document的onclick方法隐藏...

Android前端—显示GIF动画

Android前端—显示GIF动画 一、技术概述   在Android原有组件的情况下,是不能打开GIF的动画的,GIF动画在ImageView中以静态的形式显示。  我们希望在开发的过程中能够采用类似ImageView的工具打开GIF动画方便直接,同时可以实现与ImageView类似的功能。  有以下几种方法:(1)Glide; (2)GifImageV...

服务监控之 Spring Boot Admin.

一、概述  开始阅读这篇文章之前,建议先阅读下《SpringBoot 之Actuator》,该篇文章提到 Spring Boot Actuator 提供了对单个Spring Boot的监控,信息包含:应用状态、内存、线程、堆栈等等,比较全面的监控了Spring Boot应用的整个生命周期。但是美中不足的是: 所有的监控都需要调用固定的接口来查看,如果全面...

Android开发高级进阶——多进程间通信

一. 什么是多进程? 多进程就是多个进程的意思,那么什么是进程呢? 当一个应用在开始运行时,系统会为它创建一个进程,一个应用默认只有一个进程,这个进程(主进程)的名称就是应用的包名。 进程的特点: 进程是系统资源和分配的基本单位,而线程是调度的基本单位。 每个进程都有自己独立的资源和内存空间 其它进程不能任意访问当前进程的内存和资源 系统给每个进程分...

鼠标悬浮显示鼠标停留数据的内容 elementui + vue

先看效果图 直接上代码 <el-tabs v-model="activeName" @tab-click="handleClick"> <el-tab-pane :label="speaker.abscissa[0]" name="first"> <div...