做android开发对touch事件是要清晰明了的,如果心存疑问,那么本博客
可以帮你清晰的屡清楚源码是如果实现
onLongClickListener
onDoubleClickListener
onClickListener
并且再次基础上我们可以随意添加我们想要的事件
例如:长按抬起 长按超过三秒 长按移动到view之外等事件
有了这个再也不怕产品的各种触摸事件了
初步了解MotionEvent
public static final int ACTION_DOWN = 0;单点触摸动作
public static final int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE = 2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
public static final int ACTION_OUTSIDE = 4;触摸动作超出边界
public static final int ACTION_POINTER_DOWN = 5;多点触摸动作
public static final int ACTION_POINTER_UP = 6;多点离开动作
//以下是一些非touch事件
public static final int ACTION_HOVER_MOVE = 7;
public static final int ACTION_SCROLL = 8;
public static final int ACTION_HOVER_ENTER = 9;
public static final int ACTION_HOVER_EXIT = 10;
自定义onLongClickListener
API原理
http://www.2cto.com/kf/201402/276490.html
1. 满足一些先决条件。例如:当前视图非禁用状态、当前视图允许点击或者长按(详见 一、onTouchEvent整体结构)之后通过系统反馈的动作来进行判断
ACTION_DOWN:当前是否为滚动视图,如果不是,当前视图先显示为按下状态,且在500毫秒后执行长按操作。
ACTION_MOVE:如果手指移动出当前视图范围内,清理以上设置的所有状态,并且如果长按还没有执行不会触发
ACTION_UP:如果MOVE时没有进行清理,且还没有执行长按操作,执行点击操作(详见 五、ACTION_UP)
ACTION_CANCEL:清理所有状态
仿照api自定义长按事件
在Down的时候开启一个线程然后延迟300ms(长按触发的时间)后执行,然后再UP的时候判断如果按住的时间没有超过了300ms那么代码长按事件没有触发,然后执行连续点击事件的逻辑,同时删除掉Handler队列中的处理长按事件的线程,如果执行了Move事件也需要删除Handler队列里面的长按处理线程。
自定义onClickListener 单击 双击 多击事件
实现多击判断是记录点击的次数,也就是在Down的时候对点击次数+1,在Up的时候开启一个线程来等待是否在短时间内有下一次点击到来。定义了连续点击间隔时间为
private static final long CLICK_SPACING_TIME = 300;
这里利用Handler来让click线程延迟300ms执行,如果在线程还没有开始执行之前又一Down事件来了,那么在Handler的队列中删除这个线程,然后再Up里面重新往Handler里面添加一个线程来执行,这样就完成了间隔时间的等待,线程里面处理逻辑非常简单,就是把点击次数清空,然后回调一共连续点击了多少次。
自定义长按超过n秒事件
在发起长按监听的事件我们通过handler延迟n 毫秒来执行LongTimePressThread线程,在线程中执行回调,并且将longClickThreeMuch
状态改为true
自定义长按超过n秒且移动到外头的事件
在ACTION_MOVE时,判断移动的点是不是在本view内,并且longClickThreeMuch
状态为true,那么回调此事件
自定义长按在view 内外部抬起事件
在ACTION_UP时,首先先判断抬起的时间与最开始down的时间有没有超过设定的最大时间,超过了那么就是长按事件的抬起监听,继续判断抬起的点是不是在本view内 然后回调
资源释放 防止内存泄漏
将所有的handler post出去的线程,remove掉
源码
package com.meelive.ingkee.ui.listener;
import android.graphics.Rect;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.Calendar;
/**
* Created by walkingmen on 2016/12/17.
* 处理复杂的点击 长按 长按抬起 长按滑动等事件
*/
public class BeautyTouchListener implements View.OnTouchListener {
private static final String TAG = "SHF";
private OnBeautyTouchLisener mOnBeautyTouchLisener;
private long longPressTipTime;
public long getLongPressTipTime() {
return longPressTipTime;
}
public void setLongPressTipTime(long longPressTipTime) {
this.longPressTipTime = longPressTipTime;
}
public void setOnBeautyTouchLisener(OnBeautyTouchLisener onBeautyTouchLisener) {
mOnBeautyTouchLisener = onBeautyTouchLisener;
}
public BeautyTouchListener() {
}
public BeautyTouchListener(OnBeautyTouchLisener onBeautyTouchLisener) {
mOnBeautyTouchLisener = onBeautyTouchLisener;
}
private boolean touchOutside = false;
private boolean touchInSide = false;
private boolean longClickThreeMuch = false;
private boolean haveTip = false;
/**
* ----------------
*/
private static final long CLICK_SPACING_TIME = 300;
private static final long LONG_PRESS_TIME = 500;
/**
* 当前触摸点相对于屏幕的坐标
*/
private int mCurrentInScreenX;
private int mCurrentInScreenY;
/**
* 触摸点按下时的相对于屏幕的坐标
*/
private int mDownInScreenX;
private int mDownInScreenY;
/**
* 点击次数
*/
private int mClickCount = 0;
/**
* 当前点击时间
*/
private long mCurrentClickTime;
/**
* 开始长按监听的时间
*/
private long mLongClickStartTime = -1;
private Handler mBaseHandler = new Handler();
/**
* 长按线程
*/
private LongPressedThread mLongPressedThread;
/**
* 点击等待线程
*/
private ClickPressedThread mPrevClickThread;
/**
* 长按等待三秒的进程
*/
private LongTimePressThread mLongTimePressThread;
@Override
public boolean onTouch(View v, MotionEvent event) {
//获取相对屏幕的坐标,即以屏幕左上角为原点
float x;
float y;
//获取相对屏幕的坐标,即以屏幕左上角为原点
mCurrentInScreenX = (int) event.getRawX();
mCurrentInScreenY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
haveTip = false;
/**----------------*/
x = event.getRawX();
y = event.getRawY();
if (isInside(v, x, y)) {
touchInSide = true;
touchOutside = false;
} else {
touchInSide = false;
touchOutside = true;
}
Log.i(TAG, "SHF--onTouch---> ACTION_DOWN");
/**----------------*/
//记录Down下时的坐标
mDownInScreenX = (int) event.getRawX();
mDownInScreenY = (int) event.getRawY();
//记录当前点击的时间
mCurrentClickTime = Calendar.getInstance().getTimeInMillis();
//刷新长按的开始时间
mLongClickStartTime = -1;
//点击次数加1
mClickCount++;
//取消上一次点击的线程
if (mPrevClickThread != null) {
mBaseHandler.removeCallbacks(mPrevClickThread);
}
mLongPressedThread = new LongPressedThread();
mBaseHandler.postDelayed(mLongPressedThread, LONG_PRESS_TIME);
break;
case MotionEvent.ACTION_MOVE: {
/**----------------*/
x = event.getRawX();
y = event.getRawY();
if (isInside(v, x, y)) {
touchInSide = true;
touchOutside = false;
} else {
touchInSide = false;
touchOutside = true;
}
//如果长按超过了三秒 并且是碰到了外头 那么回调长按移动到了外头
if (longClickThreeMuch && touchOutside && !haveTip) {
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_OUT_MOVE);
haveTip = true;
}
}
/**----------------*/
mClickCount = 0; // 只要移动了 就没有点击事件了
break;
}
case MotionEvent.ACTION_UP: {
Log.i(TAG, "onTouch: ACTION_UP");
//如果按住的时间超过了长按时间,那么其实长按事件已经出发生了,如果没有触发长按事件那么触发点击事件
if (Calendar.getInstance().getTimeInMillis() - mCurrentClickTime <= LONG_PRESS_TIME) {
Log.i(TAG, "onTouch: 取消长按事件 触发click");
//取消注册的长按事件
mBaseHandler.removeCallbacks(mLongPressedThread);
mLongPressedThread = null;
mPrevClickThread = new ClickPressedThread();
mBaseHandler.postDelayed(mPrevClickThread, CLICK_SPACING_TIME);
} else {//长按抬起事件处理
Log.i(TAG, "onTouch: 长按抬起");
if (mLongTimePressThread != null) {
mBaseHandler.removeCallbacks(mLongTimePressThread);
}
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_CLICK_UP);
}
/**----------------*/
x = event.getRawX();
y = event.getRawY();
if (isInside(v, x, y)) {
touchInSide = true;
touchOutside = false;
} else {
touchInSide = false;
touchOutside = true;
}
Log.i(TAG, "onTouch: TOUCH_UP--touchInSide--" + touchInSide + "---longClickThreeMuch-" + longClickThreeMuch);
/**
* 如果如果触摸到外头并且长按超过了三秒
*/
if (touchOutside && longClickThreeMuch) {
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_OUT_UP);
}
return true;
}
/**
* 如果如果触摸到外头并且长按没有超过了三秒
*/
if (touchOutside && !longClickThreeMuch) {
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_FEW_OUT_UP);
}
return true;
}
/**
* 如果在内部抬起 并且长按超过三秒
*/
if (touchInSide && longClickThreeMuch) {
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_IN_UP);
}
return true;
}
/**
* 如果在内部抬起 并且长按不超过三秒
*/
if (touchInSide && !longClickThreeMuch) {
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_FEW_IN_UP);
}
return true;
}
}
if (mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.TOUCH_UP_COMPLETE);
}
resetTouchEvent();
break;
}
default:
break;
}
return true;
}
private void resetTouchEvent() {
touchOutside = false;
touchInSide = false;
longClickThreeMuch = false;
mLongClickStartTime = -1;
haveTip = false;
}
/**
* 判断是否移动
*
* @return
*/
private boolean isMoved() {
Log.i(TAG, "isMoved: mDownInScreenX---" + mDownInScreenX + "---mCurrentInScreenX---" + mCurrentInScreenX);
//允许有5的偏差 在判断是否移动的时候
return !(Math.abs(mDownInScreenX - mCurrentInScreenX) <= 5 && Math.abs(mDownInScreenY - mCurrentInScreenY) <= 5);
}
private class LongPressedThread implements Runnable {
@Override
public void run() {
//这里处理长按事件
if (mOnBeautyTouchLisener != null) {
if (longPressTipTime > 0) {
mLongTimePressThread = new LongTimePressThread();
mBaseHandler.postDelayed(mLongTimePressThread, longPressTipTime);
}
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_CLICK);
mLongClickStartTime = Calendar.getInstance().getTimeInMillis();
}
mClickCount = 0;
}
}
private class ClickPressedThread implements Runnable {
@Override
public void run() {
//这里处理连续点击事件 mClickCount 为连续点击的次数
if (mClickCount == 1 && mOnBeautyTouchLisener != null) {
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.CLICK);
}
mClickCount = 0;
}
}
private class LongTimePressThread implements Runnable {
@Override
public void run() {
if (mLongClickStartTime != -1) {
longClickThreeMuch = true;
mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_TOUCH_MUCH_TIME);
}
}
}
private boolean isInside(View view, float x, float y) {
if (view == null) {
return false;
}
//获取控件在屏幕的位置
int[] location = new int[2];
view.getLocationOnScreen(location);
//控件相对于屏幕的x与y坐标
int xScreen = location[0];
int yScreen = location[1];
Rect rect = new Rect(xScreen,
yScreen,
xScreen + view.getWidth(),
yScreen + view.getHeight());
return rect.contains((int) x, (int) y);
}
public void release() {
if (mLongPressedThread != null) {
mBaseHandler.removeCallbacks(mLongTimePressThread);
}
if (mLongTimePressThread != null) {
mBaseHandler.removeCallbacks(mLongTimePressThread);
}
if (mPrevClickThread != null) {
mBaseHandler.removeCallbacks(mPrevClickThread);
}
}
public interface OnBeautyTouchLisener {
/**
* 点击
*/
int CLICK = 0;
/**
* 长按
*/
int LONG_CLICK = 1;
/**
* 长按并且超出了longPressTipTime秒 并且移动出了view外 只会提示一次
*/
int LONG_MUCH_OUT_MOVE = 2;
/**
* 长按并且超出了lptt秒 并且在view外抬起
*/
int LONG_MUCH_OUT_UP = 3;
/**
* 长按并且超出了lptt秒 并且在view内抬起
*/
int LONG_MUCH_IN_UP = 4;
/**
* 长按没有超过lptt秒 并且在view内抬起
*/
int LONG_FEW_IN_UP = 5;
/**
* 长按没有超过十秒 在view外抬起
*/
int LONG_FEW_OUT_UP = 6;
/**
* 长按抬起
*/
int LONG_CLICK_UP = 7;
/**
* 抬起完成
*/
int TOUCH_UP_COMPLETE = 8;
/**
* 长按没松手超过了lptt秒
*/
int LONG_TOUCH_MUCH_TIME = 9;
void onBeautyTouchLisener(int type);
}
}
Activity中的运用
package com.walkingmen.sunapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private ImageView img;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
img = (ImageView) findViewById(R.id.img);
listener.setLongPressTipTime(3000);
img.setOnTouchListener(listener);
}
BeautyTouchListener listener = new BeautyTouchListener(new BeautyTouchListener.OnBeautyTouchLisener() {
@Override
public void onBeautyTouchLisener(int type) {
switch (type) {
case BeautyTouchListener.OnBeautyTouchLisener.CLICK:
Toast.makeText(MainActivity.this, "CLICK", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: CLICK");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_CLICK:
Toast.makeText(MainActivity.this, "LONG_CLICK", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_CLICK");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_CLICK_UP:
Toast.makeText(MainActivity.this, "LONG_CLICK_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_CLICK_UP");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_FEW_IN_UP:
Toast.makeText(MainActivity.this, "LONG_FEW_IN_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_FEW_IN_UP");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_IN_UP:
Toast.makeText(MainActivity.this, "LONG_MUCH_IN_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_IN_UP");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_OUT_MOVE:
Toast.makeText(MainActivity.this, "LONG_MUCH_OUT_MOVE", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_OUT_MOVE");
break;
case BeautyTouchListener.OnBeautyTouchLisener.TOUCH_FEW_IN_UP:
Toast.makeText(MainActivity.this, "TOUCH_FEW_IN_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: TOUCH_FEW_IN_UP");
break;
case BeautyTouchListener.OnBeautyTouchLisener.TOUCH_UP_COMPLETE:
Toast.makeText(MainActivity.this, "TOUCH_UP_COMPLETE", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: TOUCH_UP_COMPLETE");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_OUT_UP:
Toast.makeText(MainActivity.this, "LONG_MUCH_OUT_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_OUT_UP");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_TOUCH_MUCH_TIME:
Toast.makeText(MainActivity.this, "LONG_TOUCH_MUCH_TIME", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_TOUCH_MUCH_TIME");
break;
case BeautyTouchListener.OnBeautyTouchLisener.LONG_FEW_OUT_UP:
Toast.makeText(MainActivity.this, "LONG_FEW_OUT_UP", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onBeautyTouchLisener: LONG_FEW_OUT_UP");
break;
}
}
});
@Override
protected void onDestroy() {
super.onDestroy();
listener.release();
}
}
API源码
源文章 http://www.2cto.com/kf/201402/276490.html
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;
}
- 当前视图是否处于禁用状态(如果是抬起手指,清理掉按下状态)
- 是否在其他视图的扩大范围内(通过TouchDelegate实现)
- 如果以上两者都不成立,并且当前视图处于点击或者长按状态
- 如果以上3者都不符合条件,返回false表明当前视图不消耗此次触摸事件
、
- mViewFlags是全局变量,用于存放视图状态信息。
- TouchDelegate 设置视图的点击区域(增大或者缩小可点击区域)
- DISABLED 当前视图禁用状态。通过使用View.setEnabled(false)设置视图为禁用状态
- CLICKABLE 可点击。通过使用View.setClickable(true)设置
- LONG_CLICKABLE 可长按。通过使用View.setLongClickable(true)设置
点击与长按手势处理
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;
/**
* 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;
}
/**
* @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。
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);
}
}
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时调用,也就是再次期间添加视图的话,不会满足条件。
/**
* 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;
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();
removeTapCallback源码
/**
* Remove the tap detection timer.
*/
private void removeTapCallback() {
// 移除轻触探测定时器
// 此Runnable是在滚动视图是才会创建
if (mPendingCheckForTap != null) {
// 去除预按下状态
mPrivateFlags &= ~PREPRESSED;
// 从消息队列中删除mPendingCheckForTap
removeCallbacks(mPendingCheckForTap);
}
}
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;
UnsetPressedState 类源码
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
ACTION_CANCEL
case MotionEvent.ACTION_CANCEL:
// 清理按下状态
mPrivateFlags &= ~PRESSED;
// 刷新一下
refreshDrawableState();
// 清除轻触状态
removeTapCallback();
break;
ViewConfiguration是系统配置,各手机厂商可能会根据自身手机特点修改这些参数。
满足一些先决条件。例如:当前视图非禁用状态、当前视图允许点击或者长按(详见 一、onTouchEvent整体结构)之后通过系统反馈的动作来进行判断
ACTION_DOWN:当前是否为滚动视图,如果不是,当前视图先显示为按下状态,且在500毫秒后执行长按操作。(详见 三、ACTION_DOWN)
ACTION_MOVE:如果手指移动出当前视图范围内,清理以上设置的所有状态,并且如果长按还没有执行不会触发。(详见 四、ACTION_MOVE)
ACTION_UP:如果MOVE时没有进行清理,且还没有执行长按操作,执行点击操作(详见 五、ACTION_UP)
ACTION_CANCEL:清理所有状态