View 是 Android 中所有空间的基类。
View 的位置主要有四个顶点决定的, top、left、right、bottom. 这些顶点的坐标是相对于 View 的父容器来说。
从 Android 3.0 增加的参数: x、 y、 translationX 和 translationY. translationX 和 translationY 是 View 左上角相对于父容器的偏移量。
x = left + translationX, y = top + translationY;
在 View 平移的过程中, top 和 left 表示的是原始左上角的位置信息, 其值并不会发生变化, 改变的是 x, y, traslationX 和 translation Y.
3、 MotionEvent 和 TouchSlop
MotionEvent 分为 ACTION_DOWN, ACTION_MOVE, ACTION_UP
获取点击事件的坐标
getX/getY 获取当前 View 左上角的的 x, y 坐标;
getRawX/getRawY 返回的是相对于手机屏幕左上角的 x 和 y 的坐标(即手指在屏幕中的坐标);
ToushSlop
系统所能识别出的被认为是滑动的最小距离。 常量, 和设备有关,不同的设备上可以有所不同。
通过 get(getContext()).getScaledTouchSlop() 获取。
4、VelocityTracker, GestureDetector 和 Scroller
VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。在 Support Library 中是VelocityTrackerCompat.
官网的连接 , 使用如下:
<span style="white-space:pre"> </span>// 1、在 View 的 onTouchEvent 方法中追踪当前点击事件的速度 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); // 2、获取当前的速度, 单位以 1000 ms 为例 velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity(); // 3、最后进行回收 velocityTracker.recycle();
GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
Gathering data about touch events;
Interpreting the data to see if it meets the criteria for any of gestures your app supports.
// 1、创建一个 GestureDetector 对象,并实现 OnGestureListener 接口。 GestureDetector gestureDetector = new GestureDetector(AnimationActivity.this, this); // 2、接管目标 View 的 onTouchEvent 方法, 在待监听 View 的 onTouchEvent 方法中添加实现 boolean consume = gestureDetector.onTouchEvent(event); return; consume;
OnGestureListener 接口的方法
onDrown : 手指轻轻触摸屏幕的瞬间,由一个 ACTION_DOWN 触发;
onShowPress : 手指轻触屏幕,尚未松开或者拖动, 由一个 ACTION_DOWN 触发;
onSingTapUp : 手指松开,由 ACTION_DOWN 触发, 这是单击行为;
onScroll: 手指按下屏幕并拖动,由一个 ACTION_DOWN , 多个 ACTION_MOVE 触发, 这是拖动行为;
onLongPress: 用户长久地按着屏幕不放,即长按;
onFling: 用户按下触摸屏,快速移动后松开,由一个 ACTION_DWON、多个 ACTION_MOVE 和一个 ACTION_UP 触发,快速滑动行为;
OnDoubleTapListener 接口中的方法
onDoubleTap: 双击,由两次连续的单击组成,不能和 onSingleTapConfirmed 共存;
onSingleTapConfirmed: 单击行为;
onDoubleEvent: 表示双击行为,在双击的期间, ACTION_DOWN、ACTION_MOVE、ACTION_UP 都不会触发此回调。
如果只是监听滑动相关的,可以在 onTouchEvent 方法中实现,如果要监听类似双击这样的行为,使用 GestureDetector.
Where or not you use GestureDetector.OnGesturelistener, it's best practive to implement onDraw() method that reture true. This is beacause all getstures begin with an OnDraw() message, if you reture false for onDraw(), the system assumes methods of GestureDetector.OnGestureListener never get called.
5.Scroller
用于实现 View 的弹性滑动
通过 View 本身提供的 srollTo/srollBy 方法来实现滑动;
通过动画给 View 施加平移效果来实现滑动;
通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动。
1、使用 ScrollTo/ScrollBy
滑动过程是通过改变 View 的 mScrollX 和 mScrollY 的值,实现滑动。在滑动过程中, mScrollX 的值总是等于 View 左边缘和 View 内容左边缘在水平的距离。
ScrollTo/ScrollBy 只能改变 View 内容的位置,而不能改变 View 在布局中的位置。
2、使用动画
使用动画来移动 View, 主要是操作 View 的translationX 和 translationY 属性。
如果使用 View 动画滑动时, View 动画是对 View 的影响操作,它 并不能改变 View 的位置参数,包括宽高,如果希望动画后的状态得以保留,设置 fillAfter 为 true, 否则动画完成后其动画结果就会消失。
使用属性动画则无问题。
3、改变布局参数
即通过改变 LayoutParams.
例子:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams(); params.width +=100; params.leftMargin += 100; mButton.requestLayout(); // 或者 mButton.setLayoutParams(params);
.scrollTo/scrollBy : 操作简单, 适合对 View 内容的滑动;
.动画:操作简单, 主要适用于没有交互的 View 和实现复杂的动画效果;
.改变布局参数:操作稍微复杂,适用于有交互的 View.
实现弹性滑动的共同点,即将一次大的滑动分成若干次小的滑动,并在一个时间段内完成。
Scroller mScroller = new Scroller(mContext); // 缓慢滚动到指定的位置 private void smoothScrollTo(int destX, int destY){ int scrollX = getScrollX(); int deltaX = destX - scrollX; // 以 1000ms 内滑向 destX, 效果是慢慢滑动 mScroller.startScroll(scrollX, destY, deltaX , 0, 1000); // View 的重绘 invalidate(); } @Override public void computeScroll() { // 重写 computeScroll 方法,并在内部完成平滑滚动的逻辑 if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 再次进行重绘 postInvalidate(); } }
使用 Scroller 内部的整个流程
Scroller 的工作原理:
Scroller 本身不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果。通过不断地让 View 重绘,而每一次重绘距离滑动其实起始时间会有一个时间间隔,通过这个时间间隔 Scroller 得出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法完成 View 的滑动。 View 的每一次重绘都会导致 View 的小幅度滑动,而多次的小幅度滑动组成了弹性滑动,这就是 Scroller 滑动的工作机制。
Scroller 的使用步骤:
1、创建 Scroller 实例;
2、调用 startScroll(...) 方法来初始化滑动数据并刷新界面;
3、重写 computeScroll() 方法, 并在其内部完成滑动的逻辑。
可参考郭霖的博客http://blog.csdn.net/guolin_blog/article/details/48719871;
通过属性动画
ObjectAnimatior.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
通过 ValueAnimator,这里只是改变 Button 的内容
private void scroller(){ final int startX = 0; final int deltax = 1000; final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(3000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animator.getAnimatedFraction(); mButton.scrollTo(startX +(int)(deltax * fraction), 0); } }); animator.start(); }3、使用延时策略
通过发送一系列的消息从而达到一种渐进式的效果,具体来说就是使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。
所谓点击事件的分发,其实就是对 MotionEvent 事件分发的过程,即当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View, 而这个传递的过程就是分发过程。
所有 Touch 事件都被封装成了 MotionEvent 对象,包括 Touch 的位置、时间、历史记录以及几个手指(多指触摸)等。
每个事件是以 ACTION_DOWN 开始,ACTION_UP 结束。
对事件的处理包括三类:
传递:dispatchTouchEvent(...);
拦截:onInterceptTouchEvent(...);
消费:onTouchEvent 和 OnTouchListener;
它们之间的关系
public boolean dispatchTouchEvent(MotionEvent event){ boolean consume = false; if (onInterceptTouchEvent(envetn)){ consume = onTouchEvent(event); } else { consume = child.dispatchTouchEvent(event); } return consume; } }
事件中的优先级 onTouchListener > onTouchEvent > onClickListener;
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { // 空方法,不执行任何东西 onUserInteraction(); } // 调用 Window.superDispatchTouchEvent(...), Window 的实现类是 PhoneWindow // 所以,对调用 PhoneWindow.superDispatchTouchEvent(...) if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }下面是Activity 传递点击事件的流程
PhoneWindow 为 Window 的实现类, 在 Activity#attch(...) 方法中实例
// Actvity # attach(...)方法 final void attach(...) { ... mWindow = new PhoneWindow(this); ... }PhoneWindow#superDispatchTouchEvent(...) 方法
// PhoneWindow#superDispatchTouchEvent(..) // 在 PhoneWindow 中 mDecor 为 DecorView, DecorView 继承 FrameLayout public boolean superDispatchTouchEvent(MotionEvent event) { // 将点击事件传递给 DecorView return mDecor.superDispatchTouchEvent(event); }
如果Activity 中的 View 不拦截或者不消耗点击事件, 则会执行 Activity 的 onTouchEvent(...) 方法。
// Activity#onTouchEvent(...) public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
Activity#onTouchEvent(...) 方法会调用 Window#shouldCloseOnTouche(...) 方法,如果该方法返回 true,则结束当前 Activity ,并让 Activity#onTouchEvent(...) 返回 true,表示当前的 Activity 消耗点击事件;否则让Activity#onTouchEvent(...) 返回 false, 表示当前Activity 不消耗点击事件。
在看看 Window#shouldCloseOnTouch(...)源码
// Window#shouldCloseOnTouch(...) 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:windowCloseOnOutside 属性觉得的。可以通过 Activity#setFinishOnTouchOutside(...) 方法设定,Activity#setFinishOnTouchOutside(...)方法会调用 Window#setCloseOnTouchOutside(...) 方法;
.isOutofBounds(context, event) 判断 event 的坐标是否在 context(对于当前来说是 Activity)之外;
.peekDecorView() 返回 PhoneWindow 的 mDecor;
mCloseOnTouchOutside 这个变量非常有用,特别是 Activity 使用 Dialog 的 style 时,如果想点击弹窗外部不结束 Activity ,可以设置 Activity#setFinishOnTouchOutsidee(false) 。因为该方法会通过 Window#setCloeOnTouchOutside(...) 方法,使 mCloseOnTouchOutside 为 false , 最终Activity 不消耗该点击事件。
ViewGroup#dispatchTouchEvent(...)
// VieGroup#dispatchTouchEvent(...) // 1. action 为 ACTION_DOWN 或者 mFirstTouchTarget 不为空是,执行 if 里面的内容,否则执行 // else 里面的内容,intercept = true // 2. mFirstTouchTarget 是接收点击事件的 View 组成的单链表 // Check for interception. final boolean intercepted; if(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null){ // FLAG_DISALLOW_INTERCEPT 标记位,通过调用 requestDisallowInterceptTouchEvent() 设置, // 1.设置之后,FLAG_DISALLOW_INTERCEPT 为 true, 禁止其父类对点击事件进行拦截, 可参考 ViewPager // 2.设置之后,ViewGroup 将无法拦截 ACTION_DOWN 以外的点击事件。 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 如果 FLAG_DISALLOW_INTERCEPT 为 false, 调用 onInterceptTouchEvent(ev) 方法,并返回是否拦截; // onInterceptTouchEvent(...) 方法为 false,如果想拦截,则重写该方法,让它返回 true; intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } }else{ // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
如果没有被取消并且没有被拦截,则对将点击事件分发给子 View
// ViewGroup#dispatchTouchEvent(...) // 没有被取消并且非拦截的状态下 if(!canceled&&!intercepted){ 。。。 // 遍历所有的子 View,并对点击事件进行分发 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); 。。。 // 1.canViewReceivePointerEvents(...) 判断子 View 能否接收到点击事件 // 判断的条件是 child 可见 或者 不可见但出于动画状态 // 2. isTransformedTouchPointInView(...) 判断点击坐标(x,y) 是否在 child 可视范围之内 // 如果当前的 child 能接收点击事件并且点击的坐标在 child 的可视范围只能,点击事件交给当前的 child 处理, // 否则执行 continue。 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // 调用 dispatchTransformedTouchEvent(...) 将点击事件分发给子 View, if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 如果 child 接受点击事件,即 child 对点击事件进行了消耗或者拦截 // 调用 addTouchTarget(...) 将 child 添加到 mFirstTouchTarget 链表的表头,并返回链表 TouchTarget // 将 alreadyDispatchedToNewTouchTarget 设置为true newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } // Dispatch to touch targets. // 进一步分发点击事件 // (01) 如果mFirstTouchTarget为null,意味着还没有任何View来接受该触摸事件; // 此时,将当前ViewGroup看作一个View; // 将会调用"当前的ViewGroup的父类View的dispatchTouchEvent()"对触摸事件进行分发处理。 // 即,会将触摸事件交给当前ViewGroup的onTouch(), onTouchEvent()进行处理。 // (02) 如果mFirstTouchTarget不为null,意味着有ViewGroup的子View或子ViewGroup中, // 有可以接受触摸事件的。那么,就将触摸事件分发给这些可以接受触摸事件的子View或子ViewGroup。 if(mFirstTouchTarget==null){ // No touch targets so treat this as an ordinary view. // 第三年参数为空,则意味中交给当前 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }else{ // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } }说明:dispatchTransformedTouchEvent(...)对点击事件从新打包分发
如果第三个参数为 null 时,则会调用 super.dispatchTouchEvent(...), 而ViewGroup 是继承 View 的,这时就将点击事件传到 View#dispatchTouchEvent 方法;如果不为空,则会调用 child.dispatchTouchEvent(...), 将点击事件分发给子 View。
View#dispatchTouchEvent(...) 源码
// View#dispatchTouchEvent(...) public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; ... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; // 1、是否设置了 onTouchListener // 2. 是否设置 ENABLED, 在 xml 中是 android: enabled 或者 在 java 代码中 设置 View#setEnabled // 3. OnTouchLisetener.onTouch(...) 方法返回 // 如果mOnTouchListener.onTouch(...) 方法返回 true, 则不会执行后面的 onTouchEvent(...) 方法,从这里可以看出 // OnTouch(...)方法比 onTouchEvent(...)高级。 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 执行 onTouchEvent(...) 方法 if (!result && onTouchEvent(event)) { result = true; } } ... return result; }View#onTouchEvent(...) 的源码
// View#onTouchEvent(...) public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // 即使 View 被禁用了, 也会消耗点击事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == 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) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } // 是否设置了代理 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 点击状态时 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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() 方法,里面会调用 playSoundEffect(...) 方法和 // OnClickListener.onClick(...) 方法 performClick(); } } } .... } break; ... } return true; } return false; }在 View#onTouchEvent(...) 中的 ACTION_UP中会调用 performClick(...) 方法,并在里面调用 OnClickListener.onClick(...) 方法。
关于在源码中分析点击传递过程,可参考下面的的博文,本文有部分也是从下面博文中摘取的。
《 Android 触摸事件机制(二)Activity 中触摸事件详解》
《Android 触摸事件机制(三)View 中触摸事件详解》
《Android 触摸事件机制(四)ViewGroup 中触摸事件详解》
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; // 获取点击的坐标 int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (父容器需要当前点击事件){ intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }说明:
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事就直接消耗,否则就交由父容器进行处理。这种方法和 Android 中的事件分发不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。
重写子 View 的 dispatchTouchEvent(...) 方法
public boolean dispatchTouchEvent(MotionEvent ev) { int x = ev.getX(); int y = ev.getY(); switch (ev.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; default: break; } return super.dispatchTouchEvent(ev); }父容器也要做相应的改动
public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN){ return false; } else { return true; } }