本文为过往笔记整理, 在此只作记录,不做严谨的技术分享。
基础相关
View坐标系
MotionEvent
当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象
事件类型 | 具体动作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_CANCEL | 结束事件 |
MotionEvent.ACTION_OUTSIDE | 事件发生在视图范围外 |
辅助类
ViewConfiguration
获取 Android 系统常用的距离、速度、时间等常量
VelocityTracker
跟踪触摸事件的速度。此设置对于手势标准中包含速度的手势(例如滑动)非常有用。
GestureDetector
手势检测,该类支持的一些手势包括 onDown()、onLongPress()、onFling() 等。可以将 GestureDetector 与onTouchEvent() 方法结合使用。
OverScroller
回弹工具类,不同的回弹效果可以自定义不同的动画插值器
TouchDelegate
扩展子视图的可轻触区域
view1.post(new Runnable() {
@Override
public void run() {
Rect bounds = new Rect();
// 获取View2占据的矩形区域在其父View(也就是View1)中的相对坐标
view2.getHitRect(bounds);
// 计算扩展后的矩形区域Bounds相对于View1的坐标
bounds.left -= 100;
bounds.top -= 50;
bounds.right += 100;
bounds.bottom += 50;
TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
// 为View1设置TouchDelegate
view1.setTouchDelegate(touchDelegate);
}
});
复制代码
事件处理
- 每一个DOWN / MOVE / UP / CANCLE都是一个事件,并不是连起来才是一个事件
- 事件的消费,是看返回true/false,而不是看有没有处理操作
- Activity、ViewGroup、View
- 都有分发、消费事件的能力
- 只有ViewGroup有拦截事件的能力
事件分发
window中的View是树形结构,可能会重叠在一起,当我们点击的区域有多个View都可以响应的时候,事件分发机制决定了这个点击事件应该给谁处理。
分发机制类似洋葱模型、责任链模式、冒泡…
分发:Activity -> PhoneWindow -> DecorView -> ViewGroup -> @1 -> ... -> View
消费:Activity <- PhoneWindow <- DecorView <- ViewGroup <- @1 <- ... <- View
复制代码
- 如果事件被消费,就意味着事件信息传递终止
如果在@1处消费事件,就不在往下传递了,直接返回 - 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃事
View
优先级:
- OnTouchListener.onTouch
- onTouchEven
注意:OnTouchListener.onTouch返回false,并不代表该View不消费事件了,得看dispatchTouchEvent返回的结果
public boolean dispatchTouchEvent(MotionEvent event) {
...
// 被遮盖,不响应事件
if (onFilterTouchEventForSecurity(event)) {
...
//setOnTouchListener设置的监听,优先级高
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 系统已实现好的,优先级低。
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
复制代码
onTouchEvent:
- View即使设置了setEnable(false),只要是可点击状态就会消费事件,只是不做出回应
- 只要进入CLICKABLE判断,就返回true消费时间
事件 | 处理 |
---|---|
DOWN | 发送LongClick延迟消息,过期触发 |
MOVE | 移除LongClick消息 |
CANCLE | 移除LongClick消息 |
UP | 移除LongClick消息 触发Click事件 |
<!--只关注事件的分发,不关注其它状态的变化-->
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int action = event.getAction();
//View被禁用的话,如果是可以点击的,一样返回true,表示消费了事件。只是不作出回应。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 委托:扩大点击事件、委托其它处理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/**
* 只要进入该if,就返回true,消费事件
*/
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_DOWN:
if (isInScrollingContainer) {
} else {
//长按事件,发送延时消息到队列
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_MOVE:
if (!pointInView(x, y, mTouchSlop)) {
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//移除长按事件的消息。
removeLongPressCallback();
setPressed(false);
}
}
break;
case MotionEvent.ACTION_UP:
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除长按事件的消息
removeLongPressCallback();
//点击事件: 可知onclick事件是在UP的时候触发
if (!focusTaken) {
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
//移除长按事件
removeLongPressCallback();
mHasPerformedLongPress = false;
break;
}
return true;
}
return false;
}
复制代码
ViewGroup
- DOWN事件:
- 清除之前状态,mFirstTouchTarget = null
- 进入逻辑1、2寻找接收事件的子View
- mFirstTouchTarget = null,进入逻辑3
- mFirstTouchTarget != null, 进入逻辑4
- MOVE/UP事件:
- mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
- mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
- CANCLE事件:
- mFirstTouchTarget = null,注释2处不满足逻辑1判断条件,进入逻辑3
- mFirstTouchTarget != null,注释2处不满足逻辑1判断条件,进入逻辑4
总结,
-
DOWN事件就是用来清理状态、寻找新接收事件子View的
-
DOWN事件的后续事件:
- 未找到子View接收情况下,直接自己处理
- 找到子View接收的情况下,直接给子View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
....
// 如果该View被遮蔽,并且在被遮蔽时不响应点击事件,则不分发该触摸事件,即返回false。
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* step1:DOWN事件的时候,表示最初开始事件,清除之前的状态。
*/
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 关键:每次DOWN的时候,清除前一个手势的mFirstTouchTarget = null
cancelAndClearTouchTargets(ev);
// 清除状态
resetTouchState();
}
/**
* step2:拦截判断
*/
final boolean intercepted;
// ACTION_DOWN(初始状态)或 有子View处理事件:判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//默认没有该标记位,返回false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
//默认返回false,并不是每次都会调用
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {//requestDisallowInterceptTouchEvent(true)
intercepted = false;
}
} else {
//[注释1],没有子View接收事件,拦截
intercepted = true;
}
/**
* step3:找能接收事件的子View,并赋值给mFirstTouchTarget
*/
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
// *****每次都会初始化这两个变量****
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//如果在这一层不满足判断条件,直接就到[逻辑3,4]了。
//[逻辑1]
if (!canceled && !intercepted) {
View childWithAccessibilityFocus =
ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
/**
* 逻辑2的作用就是在最初的Down事件的时候,找到接收事件的子View,并各种赋值
* 如果没有找到接收事件的view,mFirstTouchTarget = null,
* 之后的事件就不用判断是否拦截(move/up,==null不满足if条件),
* 直接进入[逻辑3]给ViewGroup自己了。
*/
//[逻辑2]
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {//倒序
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 记录接收事件的view链表中找该child,如果有的话就break,因为已经找到了
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
break;
}
//第2,3参数决定,进入后调用子View的dispatch方法。
if (dispatchTransformedTouchEvent(ev, false, child,
idBitsToAssign)) {
// 能进入到这里,就说明child消费事件了
// addTouchTarget里给mFirstTouchTarget赋值,下面就会进入[逻辑4]。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 因为已经消费了down事件,所以在[逻辑4],直接返回true。
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}//[逻辑2]
}//[逻辑1]
/*
step4:到这,已经跳出了上面的大嵌套判断!--上面的大嵌套就是用来找接收事件的子View的。
一旦确定找到了或者没有接收者,后面的事件:
1. 检查intercepte状态。
2. 进入下面的逻辑,后面的事件直接确定分发给谁
*/
// 没有找到接收事件的View,以后的move/up也通过这一步给ViewGroup
[逻辑3] if (mFirstTouchTarget == null) {
//没有接收事件的子View,调用自己的dispatchTouchEvent
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 在DOWN找到接受事件的子View时,赋值alreadyDispatchedToNewTouchTarget = true
// 此时已经消费了事件,所以直接返回true
// 后面的其它事件中,alreadyDispatchedToNewTouchTarget被重置,不在满足该条件
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 子View消费事件
//如果cancelChild为true,给子View发送cancle事件
[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
}
return handled;
}
复制代码
Activity
Touch事件先是传递到Activity,接着由Activity传递到最外层布局,然后一层层遍历循环到View
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 交互 空实现
onUserInteraction();
}
// DecorView实际是ViewGroup的dispatchTouchEvent方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// down点击到外部区域,消费事件,finish
return onTouchEvent(ev);
}
复制代码
onUserInteraction()
这是一个空实现,用的也比较少,不深究:
此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。
onTouchEvent(event)
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
复制代码
mWindow即使PhoneWindow,该方法是@hide,并且在Window类中定义。
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
复制代码
- mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。
- isOutOfBounds(context, event)是判断该event的坐标是否在context(对于本文来说就是当前的Activity)之外。是的话,返回true;否则,返回false。
- peekDecorView()则是返回PhoneWindow的mDecor。
总的来说:
如果设置了android:windowCloseOnTouchOutside为true,并且是DOWN事件点击了Activity外部区域(比如Activity是一个Dialog),返回true,消费事件,并且finish。
ACTION_CANCEL
子View在接收事件过程中,被中断,父View会传给子View一个CANCEL事件
[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted; //注释1
//如果cancelChild为true,给子View发送cancle事件
[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
//...
}
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//发送CANCEL事件给子View
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//...
}
复制代码
ACTION_OUTSIDE
设置了FLAG_WATCH_OUTSIDE_TOUCH,事件发生在当前视图的范围之外
例如,点击音量键之外的区域取消音量键显示:
//frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
// 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH
mDialog = new CustomDialog(mContext);
mWindow = mDialog.getWindow();
mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
......
// 重写onTouchEvent并处理ACTION_OUTSIDE事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mShowing) {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
return true;
}
}
return false;
}
复制代码
事件拦截
只有ViewGroup有事件拦截的能力,View可根据情况申请父View进行拦截
View
View没有拦截事件的能力,只能根据不同需求调用mParent.requestDisallInterceptTouchEvent(true/false) 申请父View是否进行拦截。
注意:
如果在子View接收事件的过程中被父View拦截,父View会给子View一个CANCEL
事件,注意处理相关逻辑。
ViewGroup
onInterceptTouchEvent
- 设置了FLAG_DISALLOW_INTERCEPT标记时,不会调用
- 其它时候都会调用
/**
* ViewGroup事件分发时的拦截检查机制
*/
//默认没有该标记位,返回false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//注释1
if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
intercepted = onInterceptTouchEvent(ev);//默认返回false
} else {
intercepted = false;//requestDisallowInterceptTouchEvent(true)
}
/**
* 默认返回false
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
/*
* disallowIntercept = true时,不允许拦截,注释1为true
* disallowIntercept = false时,允许拦截,注释1为false
*/
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// We're already in this state, assume our ancestors are too
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 添加标记,使得注释1为true
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 清除标记,使得注释1为false
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
复制代码
requestDisallowInterceptTouchEvent
- true,不允许拦截,注释1为true,不会调用onInterceptTouchEvent
- false,允许拦截,注释1为false(默认),调用onInterceptTouchEvent
注意
:调用requestDisallowInterceptTouchEvent(false)申请拦截,并不会真的就被父View拦截了。它只是一个标记,使得父View会检查onInterceptTouchEvent这个方法(默认也会调用)。
它只会影响 mGroupFlags & FLAG_DISALLOW_INTERCEPT
值,真正决定要不要被拦截是看 onInterceptTouchEvent
的返回值。如果为true:
在注释1处cancelChild = true
,会导致给子类发送CANCEL
事件,然后修改mFirstTouchTarget
,不再给子View传递事件。
[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted; //注释1
// 子View消费事件
//如果cancelChild为true,给子View发送cancle事件
[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//...
}
复制代码
Activity
Activity没有onInterceptTouchEvent方法,也没有mParent,不具备主动或被动拦截能力
滑动冲突
常见场景:
- 内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
- 内外层滑动方向一致(如:RecyclerView嵌套)
一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截
父View
事件发送方,父View拦截。
父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。
- DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
- UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
- 在MOVE中根据逻辑需求判断是否拦截
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
}
return intercepted;
}
复制代码
子View
事件接收方,内部拦截
事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。
注意:
申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。
public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);//不许拦截
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);//申请拦截
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return super.dispatchTouchEvent(event);
}
复制代码
:cry:多点触控
多点触控相关的事件:
事件 | 简介 |
---|---|
ACTION_DOWN | 第一个 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。 |
ACTION_UP | 最后一个 手指 离开屏幕时触发。 |
ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已经有手指在屏幕上)。 |
ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。 |
以下事件类型不推荐使用 | ---以下事件在2.0开始,在 2.2 版本以上被废弃--- |
第 2 个手指按下,已废弃,不推荐使用。 | |
第 3 个手指按下,已废弃,不推荐使用。 | |
第 4 个手指按下,已废弃,不推荐使用。 | |
第 2 个手指抬起,已废弃,不推荐使用。 | |
第 3 个手指抬起,已废弃,不推荐使用。 | |
第 4 个手指抬起,已废弃,不推荐使用。 |
多点触控相关的方法:
方法 | 简介 |
---|---|
getActionMasked() | 与 getAction() 类似,多点触控需要使用这个方法获取事件类型。 |
getActionIndex() | 获取该事件是哪个指针(手指)产生的。 |
getPointerCount() | 获取在屏幕上手指的个数。 |
getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 |
getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 |
getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 |
index和pointId
在 2.2 版本以上,我们可以通过getActionIndex() 轻松获取到事件的索引(Index),Index 变化有以下几个特点:
1、从 0 开始,自动增长。
2、之前落下的手指抬起,后面手指的 Index 会随之减小。
(0、1、2 –> 第2个手指抬起 –> 第三个手指变为1 –> 0、1)
3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
4、对 move 事件无效。
**getActionIndex()**获取到的始终是数值 0
相同点 | 不同点 |
---|---|
1. 从 0 开始,自动增长。 2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。 |
Index 会变化,pointId 始终不变。 |
pointerIndex 与 pointerId
pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。
类型 | 简介 |
---|---|
pointerIndex | 用于获取具体事件,可能会随着其他手指的抬起和落下而变化 |
pointerId | 用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变 |
这两个数值使用以下两个方法相互转换:
方法 | 简介 |
---|---|
getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
findPointerIndex(int pointerId) | 通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。 |
自定义View示例
/**
* Created by Varmin
* on 2017/7/5 16:16.
* 文件描述:left,content,right三个tag,在布局中给每个部分设置该tag。用于该ViewGroup内部给子View排序。
* 功能:默认全部关闭左右滑动。分别设置打开
*/
public class SlideView extends ViewGroup implements View.OnClickListener, View.OnLongClickListener {
private static final String TAG = "SlideView";
public final String LEFT = "left";
public final String CONTENT = "content";
public final String RIGHT = "right";
private Scroller mScroller;
/**
* scroller滑动时间。默认250ms
*/
public static final int DEFAULT_TIMEOUT = 250;
public static final int SLOW_TIMEOUT = 500;
/**
* 左右View的宽度
*/
private int leftWidth;
private int rightWidth;
private GestureDetector mGesture;
private ViewConfiguration mViewConfig;
public SlideView(Context context) {
super(context);
init(context);
}
public SlideView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mScroller = new Scroller(context);
//都是自己处理的,这里没有用到该手势方法
//缺点:误差有些大。这种精确滑动的,最好自己判断
mGesture = new GestureDetector(context, new SlideGestureDetector());
mViewConfig = ViewConfiguration.get(context);
//默认false
setClickable(true);
}
/**
* 所有的子View都映射完xml,该方法最早能获取到childCount
* 在onMeasuer/onLayout中获取,注册监听的话,会多次被调用
* 在构造方法中,不能获取到childCount。
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initListener();
}
private void initListener() {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.setClickable(true);
childView.setOnClickListener(this);
if (CONTENT.equals(childView.getTag())) {
childView.setOnLongClickListener(this);
}
}
}
@Override
public void onClick(View v) {
String tag = (String) v.getTag();
switch (tag) {
case LEFT:
Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
break;
case CONTENT:
Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
closeAll(SLOW_TIMEOUT);
break;
case RIGHT:
Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
break;
}
}
@Override
public boolean onLongClick(View v) {
Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
return true;
}
/**
* 每个View的大小都是由父容器给自己传递mode来确定。
* 每个View的位置都是由父容器给自己设定好自己在容器中的左上右下来确定位置。
* 所以,继承至ViewGroup的容器,要在自己内部实现对子View大小和位置的确定。
*/
/**
* 子View不会自己测量自己的,所以在这里测量各个子View大小
* 另外,处理自己是wrap的情况,给自己一个确定的值。
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//测量自己
//默认是给该ViewGroup设置固定宽高,假设不纯在wrap情况,onlayout中也不考虑此情况
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
String tag = (String) childView.getTag();
switch (tag) {
case LEFT:
leftWidth = childWidth;
childView.layout(-childWidth, 0, 0, childHeight);
break;
case CONTENT:
childView.layout(0, 0, childWidth, childHeight);
break;
case RIGHT:
rightWidth = childWidth;
childView.layout(getMeasuredWidth(), 0,
getMeasuredWidth() + childWidth, childHeight);
break;
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean handled = super.onInterceptTouchEvent(ev);
if (handled) {
return true;
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitX = (int) ev.getX();
mInitY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = (int) (ev.getX() - mInitX);
int offsetY = (int) (ev.getY() - mInitY);
/**
* 判断可以横向滑动了
* 1,拦截自己的子View接收事件
* 2,申请父ViewGroup不要看拦截事件。
*/
if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
requestDisallowInterceptTouchEvent(true);
return true;
}
break;
case MotionEvent.ACTION_UP:
//重置回ViewGroup默认的拦截状态
requestDisallowInterceptTouchEvent(false);
break;
}
return handled;
}
private int mInitX;
private int mOffsetX;
private int mInitY;
private int mOffsetY;
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
mOffsetX = (int) (event.getX() - mInitX);
mOffsetY = (int) (event.getY() - mInitY);
if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {//横向触发条件
//预估,偏移offsetX后的大小
int mScrollX = getScrollX() + (-mOffsetX);
if (mScrollX <= 0) {//向右滑动,显示leftView:110
//上面的是预估,如果预估大于目标:你不能return放弃了,要调整mOffsetX的值使其刚好等于目标
if (Math.abs(mScrollX) > leftWidth) {
mOffsetX = leftWidth - Math.abs(getScrollX());
//return true;
}
}else {//向左滑动,显示rightView:135
if (mScrollX > rightWidth) {
mOffsetX = getScrollX() - rightWidth;
//return true;
}
}
this.scrollBy(-mOffsetX,0);
mInitX = (int) event.getX();
mInitY = (int) event.getY();
return true;
}
break;
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
if (upScrollX > 0) {//向左滑动,显示rightView
if (upScrollX >= (rightWidth/2)) {
mOffsetX = upScrollX - rightWidth;
}else {
mOffsetX = upScrollX;
}
}else {//向右,显示leftView
if (Math.abs(upScrollX) >= (leftWidth/2)) {
mOffsetX = leftWidth - Math.abs(upScrollX);
}else {
mOffsetX = upScrollX;
}
}
// this.scrollBy(-mOffsetX,0);//太快
// startScroll(-mOffsetX, 0, 1000);//直接放进去,不行?
/**
* 注意startX。dx表示的是距离,不是目标位置
*/
mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
invalidate();
break;
}
if (!handled) {
handled = super.onTouchEvent(event);
}
return handled;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
/**
* 虽然传入的dx、dy并不是scrollTo实际要到的点,dx,dy只是一小段距离。
* 但是computeScroll()我们scrollTo的是:现在位置+dx的距离 = 目标位置
*
* @param dx //TODO *距离!距离!并不是说要到达的目标。*
* @param dy
* @param duration 默认的滑动时间是250,复位的时候如果感觉太快可以自己设置事件.
*
*/
private void startScroll(int dx, int dy, int duration) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
//mScroller.extendDuration(duration); 在250ms基础上增加。构造函数传入的话,就是duration的时间。
invalidate();
}
/**
* 是否打开,ListView中复用关闭
* @return
*/
public boolean isOpened(){
return getScrollX() != 0;
}
public void closeAll(int duration){
mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration);
invalidate();
}
}
复制代码
Tips
scrollTo/By
通过三种方式可以实现View的滑动:
-
通过View本身提供的scrollTo/scrollBy方法;
-
通过动画使Veiw平移。
-
通过改变View的LayoutParams属性值。
**setScrollX/Y、scrollTo: **移动到x,y的位置
**scrollBy: **移动x,y像素的距离
public void setScrollX(int value) {
scrollTo(value, mScrollY);
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
}
}
复制代码
**注意:**假如scrollTo(30,10),按照View右下正,左上负的概念,因该是向右滑动30,向下滑动10。