1.基础知识
-
位置参数:
- left/top/right/bottom是左上和右下的原始坐标位置,不会改变。
- translation*是相对于left和top的偏移量,默认是0。
- x/y是左上角当前(所看到的)的坐标。
- x = left + translationX
- y = top + translationY
-
MotionEvent:触摸事件
- ACTION_DOWN/ACTION_MOVE/ACTION_UP
- 触点位置getX/getY/getRawX/getRawY
-
TouchSlop:被认为是滑动的最小距离
ViewConfiguration.get(getContext()).getScaledTouchSlop()
-
VelocityTracker:速度追踪
- 绑定event
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event);
- 计算速度
velocityTracker.computeCurrentVelocity(1000); float xVelocity = velocityTracker.getXVelocity(); float yVelocity = velocityTracker.getYVelocity();
- 回收
velocityTracker.clear(); velocityTracker.recycle();
- 绑定event
-
GestrueDetector:手势检测
- 创建一个实例,实现并绑定一个OnGestrueListener接口,双击行为则实现并绑定OnDoubleTapListener。
- 接管目标View的onTouchEvent方法:
boolea consume = mGestureDetector.onTouchEvent(event); return consume;
- 常用
onSingleTapUp(单击)/onFling(快速滑动)/onScroll(拖动)/onLongPress(长按)/onDoubleTap(双击)
-
Scroller:平滑滚动
2.View的滚动(平移)
- 使用scrollTo(x, y)/scrollBy(x, y)
- scrollTo是绝对滚动,scrollBy是相对滚动。
- 这两个方法实际上改变的是View content的显示区域,而不是view在布局中的位置。以x方向为例,当参数x为正值,表示显示区域向右侧移动,相对应的content就好像向左滚动了。
- offsetLeftAndRight/offsetTopAndBottom
- setTranslationX/setTranslationY
- 改变布局参数
MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams;
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
- 平滑滚动
- Scroller
Scroller可以帮助计算平滑滚动过程中的offset,View通过该offset,调用scrollTo方法去执行滚动,直到滚动结束。所以,该方法的使用场景是滚动View的内容,而不是View本身。
自定义的zoomIn方法调用invalidate()进行重绘,darw()中会调用computeScroll()方法决定是否继续滚动,computeScroll()默认为空,需要自己实现。如果滚动没有结束,调用scrollTo执行滚动,并调用postInvalidate()(可以非UI线程调用,底层通过Handler在UI线程调用invalidate())进行下一次重绘,继续执行可能的滚动。private Scroller mScroller = new Scroller(context); public void zoomIn() { // Revert any animation currently in progress mScroller.forceFinished(true); // Start scrolling by providing a starting point and // the distance to travel mScroller.startScroll(0, 0, 100, 0); // Invalidate to request a redraw invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { // Get current x and y positions int currX = mScroller.getCurrX(); int currY = mScroller.getCurrY(); scrollTo(currX, currY); postInvalidate(); } }
- 动画
- 延时策略
Handler/view.postDelay()
- Scroller
3.View的事件分发机制
- Activity事件分发过程
- 先交由Activity的DecorView的顶层View(即通过setContentView设置的View)去处理:
Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent -> DecorView#dispatchTouchEvent - 如果View中没有处理,则交由自己处理:
Activity#dispatchTouchEvent -> Activity#onTouchEvent
- 先交由Activity的DecorView的顶层View(即通过setContentView设置的View)去处理:
- ViewGroup事件分发
关键方法:dispatchTouchEvent onInterceptTouchEvent- event首先分发到ViewGroup的dispatchTouchEvent方法,当为ACTION_DOWN时,会初始化touch target和touch state,并清空FLAG_DISALLOW_INTERCEPT设置。这样即使子view调用了getParent().requestDisallowInterceptTouchEvent,对于ACTION_DOWN是不起作用的。
// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); }
- 如果event是ACTION_DOWN或者touch target不为null,即产生了一次新的touch或者当前有子view正在消耗event:如果子view没有设置FLAG_DISALLOW_INTERCEPT,那么onInterceptTouchEvent会被调用,决定是否应该intercept event。所以,如果已经进入了event序列,而且target为null,则当前ViewGroup中不存在处理该event的View,直接交由自己处理,不必继续向下分发。
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { 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; }
- 若果不intercept event,则遍历所有的子View,根据view是否可接收点击事件等条件,决定是否将event分发给子view。注意在进入遍历之前,判断了event的action,只处理了ACTION_DOWN相关的情况,对于ACTION_MOVE和ACTION_UP不会走这个流程。dispatchTransformedTouchEvent具体处理子view的event分发,如果子view消耗了event,返回值即为true,该子view就会添加到target view中:newTouchTarget = addTouchTarget(child, idBitsToAssign);
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); 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); 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(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; }
- dispatchTransformedTouchEvent中就会将event传递给参数中child的dispatchTouchEvent方法。
// If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); }
- 之后的event就交由child去处理。如果dispatchTransformedTouchEvent返回true,即当前的子view消耗了该event,则ViewGroup就不再继续遍历。
- 上面是没有intercept的情况,可以将ACTION_DOWN分发到新的子View中。之后,如果当前没有子view正在消耗event,那么event交给ViewGroup自己处理;有view正在消耗event的话,将event分发给他(ACTION_MOVE/ACTION_UP就是在这里被分发出去的),已经刚刚分发到ACTION_DOWN的view不会再次分发。
同时这里是处理intercept的地方:会直接向所有的touch target发送ACTION_CANCEL,同时touch target会被设置位NULL,这样在下一次处理event的时候,会交由ViewGroup处理。
// Dispatch to touch targets. 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; } }
- 最后dispatchTouchEvent会返回一个handled标志,来标识当前ViewGroup或其子view是否有消耗event,以告知上一层view。
- View事件处理
关键方法:dispatchTouchEvent onTouchEvent onClick onTouch- 当event分发到View的dispatchTouchEvent方法,首先会判断onTouch回调是否有设置,如果有会优先处理;如果没有则交由自己的onTouchEvent处理。onTouch只影响onTouchEvent在此次event的调用。
if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement 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; } }
- 如果onTouch没有消耗掉event,则在onTouchEvent中:如果view是clickable的,就会处理并消耗ACTION_DOWN/ACTION_MOVE/ACTION_UP等event,就算view是disable状态,也会消耗,只是不做任何处理。在ACTION_UP的时候,触发performClick方法,onClick回调即会被调用。
- 总结:
- 如果onTouch中不消耗event,即返回false,想要收到后续event,可以将view的clickable设为true。因为只要view是clickable的,onTouchEvent就会返回true,就会消耗event,就可以收到后续event,因为onTouch和onTouchEvent同在view的dispatchTouchEvent中调用,且在onTouchEvent之前,所以同样可以收到。
- View没有onInterceptTouchEvent方法。
- View的onTouchEvent主要处理了click和longclick。接收到ACTION_DOWN以后,通过message queue在合适的时刻执行longclick的动作;在ACTION_UP执行click的动作(如果longclick没有执行)。所以如果通过onTouch回调,拦截了ACTION_DOWN,click和longclick就直接失效,但是并不影响onTouchEvent在之后被调用的机会。
- getParent().requestDisallowInterceptTouchEvent无法作用于ACTION_DOWN,ViewGroup想要intercept掉ACTION_DOWN,子view没有办法干预。
- ACTION_DOWN的分发非常重要,当子view收到并处理了ACTION_DOWN,ViewGroup会将其添加到touch target中,后续的event都会继续分发给他。如果他收不到ACTION_DOWN,无法收到后续event;如果所有子view收不到ACTION_DOWN,后续event交由ViewGroup自己处理。
- 当在分发出ACTION_DOWN之后,如果ViewGroup拦截了之后的event,则之前被分发到ACTION_DOWN的子view会收到ACTION_CANCEL,同时touch target会被设置位NULL,这样在下一次处理event的时候,ViewGroup的onTouchEvent就会被调用。
- 关于onInterceptTouchEvent:
4.View滑动冲突
处理滑动冲突的重点,在于将ACTION_MOVE事件合理的分配到合适的View或ViewGroup。而任何一个事件序列,都始于ACTION_DOWN,所以ACTION_DOWN一定要向子View分发出去。
- 外部拦截
ViewGroup通过重写onInterceptTouchEvent,决定事件是自己处理,还是交由子View处理。public boolean onInterceptTouchEvent(MotionEvent evnet) { boolean intercepted = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (parent need to handle the event) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; } return intercepted; }
- 内部拦截
ViewGroup不拦截任何事件,交由子View处理;子view视情况恢复父ViewGroup的拦截,交由ViewGroup处理。// ViewGroup public boolean onInterceptTouchEvent(MotionEvent evnet) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return false; default: return true; } }
// View public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (parent need to handle the event) { parent.requestDisallowInterceptTouchEvent(false); } break; } return super.dispatchTouchEvent(evnet); }