left、right、top、bottom
View在平移的过程中,top和left表示的是原始左上角的位置信息,值不会发生改变,发生改变的是x、y、translationX和translationY这四个参数
通过getX/getY 返回相对于当前VIew的左上角的x和y坐标
通过getRawX/getRawY 返回相对于屏幕左上角的x和y坐标
获取这个常量:ViewConfiguration.get(getContext()).getScaledTouchSlop()
VelocityTracker velocityTracker = VelocityTracker.obtain();
VelocityTracker.addMovement(event);
VelocityTracker.computeCurrentVelocity(1000); //1000像素
int xVelocity = (int)velocityTracker.getXVelocity();
VelocityTracker.clear();
VelocityTracker.recycle();
GestureDetector mGestureDetector = new GestureDetector(this);
mGestureDetector.setIsLongpressEnabled(false);//解决长按屏幕无法拖动现象
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
建议:监听滑动相关,建议自己在onTouchEvent中实现,双击行为,建议使用GestureDetector
使用View的scrollTo/scrollBy 方法进行滑动时,其滑动效果是瞬间完成的,这时就可以用Scroller来实现有过渡效果的滑动
Scroller需要和View的computeScroll方法配合使用,典型代码如下:
Scroller scroller = new Scroller(mContext);
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果是慢慢滑动
scroller.starlScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@override
public void computeScroll() {
if (scroller.computeScrollOffset) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
3种方式
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
注意:使用View动画可能带来问题,平移后的View将无法响应点击事件,因为点击事件实在View的初始区域注册的
改变LayoutParams
MarginLayoutParams params = (MarginLaoutParams)mButton.getLayoutParams();
params.width += 100;
params.laftMargin += 100;
mButton.requestLayout();
Scroller scroller = new Scroller(mContext);
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果是慢慢滑动
scroller.starlScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@override
public void computeScroll() {
if (scroller.computeScrollOffset) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
再看一下computeScrollOffset方法的实现:
public boolean computeScrollOffset() {
int timePassed = (int)(AnimationUtils.currentAnimationTImeMills() - mStartTime);
if (timePassed < mDuration) {
switch (MODE) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + MAth.round(x * mDeltaX);
break;
···
}
}
return true;
}
上面是Scroller的典型使用方法
工作原理:
以上,View的每一次重绘都会导致VIew进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,同时整个过程对View没有丝毫引用,也没有内部计时器
基本实现:
ObjextAnimator.odFloat(targetView, "translationX", 0, 100).setDuration(100).start();
模仿Scroller实现:
final int StartX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUppdateListener(new AnimatorUpdateListener() {
@override
public void onAnimatorUpdate(ValueAnimator animator) {
float duration = animator.getAnimatedFraction();
mButton.scrollTo(startX + (int)(deltaX * fraction), 0);
}
})
上述实现本质上没有作用于任何对象,只是在1000ms内在动画的每一帧获取动画完成比例,根据这个比例计算出VIew要滑动的距离,针对VIew内容而非VIew本身
大约1000ms内将VIew向左移动100像素
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 30;
private int mCount = 0;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMaeeage(Messaeg msg) {
switch(msg.what) {
case Message_SCROLL_TO: {
mCount ++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int)(fraction * 100);
mButton.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayd(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
参考:https://www.gcssloop.com/customview/dispatch-touchevent-theory
http://wuxiaolong.me/2015/12/19/MotionEvent/
类型 | 相关方法 | Activity | VIewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | ✔️ | ✔️ | ✔️ |
事件拦截 | onInterceptTouchEvent | ❌ | ✔️ | ❌ |
事件消费 | onTouchEvent | ✔️ | ✔️ | ✔️ |
三个事件关系伪代码:
public Boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
如果最后分发到View,如果这个View也没有处理事件怎么办,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View
所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
……
参考:https://www.gcssloop.com/customview/dispatch-touchevent-source
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
事件开始交给Activity中的WIndow进行分发,返回flase意味着事件没人处理,那么Activity的 onTouchEvent会被调用
Window是一个抽象类,superDispatchTouchEvent也是抽象方法,其实现类是 PhoneWindow,PhoneWindow中处理:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow会将事件传递给DecorView,DecorView就说当前界面的底层容器(即setContentView所设置的View的父容器)
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
DecorView 继承自FrameLayout且是Activity根View的父View,所以事件到这里已经传递给顶级View了,即在Activity中通过setContentView所设置的VIew,顶级View也叫根View,一般是VIewGroup
事件达到顶级View(一般是一个ViewGroup)后,会调用ViewGroup的dispatchTouchEvent,主要工作:
源码分析:
public boolean dispatchTouchEvent(MotionEvent ev) {
// 调试用
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 判断事件是否是针对可访问的焦点视图(很晚才添加的内容,个人猜测和屏幕辅助相关,方便盲人等使用设备)
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 处理第一次ACTION_DOWN.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 清除之前所有的状态
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否需要拦截.
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); // 恢复操作,防止被更改
} else {
intercepted = false;
}
} else {
// 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
intercepted = true;
}
// 判断事件是否是针对可访问的焦点视图
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 检查事件是否被取消(ACTION_CANCEL).
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果没有取消也没有被拦截 (进入事件分发)
if (!canceled && !intercepted) {
// 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。
// 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。
// 我们检测并避免保持这种状态,因为这些事非常罕见。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清除此指针ID的早期触摸目标,防止不同步。
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex); // 获取触摸位置坐标
final float y = ev.getY(actionIndex);
// 查找可以接受事件的 ChildView
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
// 如果有一个视图具有可访问性焦点,我们希望它首先获取事件,
// 如果不处理,我们将执行正常的分派。
// 尽管这可能会分发两次,但它能保证在给定的时间内更安全的执行。
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 检查View是否允许接受事件(即处于显示状态(VISIBLE)或者正在播放动画)
// 检查触摸位置是否在View区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// getTouchTarget 中判断了 child 是否包含在 mFirstTouchTarget 中
// 如果有返回 target,如果没有返回 null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// ChildView 已经准备好接受在其区域内的事件。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break; // ◀︎已经找到目标View,跳出循环
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 没有找到 ChildView 接收事件
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 分发 TouchTarget
if (mFirstTouchTarget == null) {
// 没有 TouchTarget,将当前 ViewGroup 当作普通的 View 处理。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 分发TouchTarget,如果我们已经分发过,则避免分配给新的目标。
// 如有必要,取消分发。
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;
}
}
// 如果需要,更新指针的触摸目标列表或取消。
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
事件调度:onTouchListener > onTouchEvent > onLongClickListener > onClickListene
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
/**
* 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
* 如果 onTouch 方法返回值为 true,就设置 result 为 true。
*/
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/**
* 如果 result 为 false,则调用自身的 onTouchEvent。
* 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
*/
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
onTouchEvent中
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
// 检查各种 clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
removeLongPressCallback(); // 移除长按
...
performClick(); // 检查单击
...
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0); // 检测长按
...
break;
...
}
return true; // ◀︎表示事件被消费
}
return false;
}
注意:
点击事件都先经过父容器的拦截处理,如果父容器需要此事就拦截,不需要就不拦截,符合点击事件的分发机制
重写onInterceptTouchEvent 方法:
指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,和事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法