View
1.View 事件体系
1.1 基础知识
-
位置参数
getRawX() / getRawY() //获取当前View 相对于手机屏幕的x和y坐标 getX() / getY() //获取相对于当前view左上角的x和y坐标 int translationX //移动量 int translationY
-
点击与滑动
MotionEvent :ACTION_DOWN , ACTION_MOVE, ACTION_UP
-
TouchSlop : 滑动的最小距离单位,获取:
ViewConfiguration.get(getContext()).getSacledTouchSlop();
-
VeloCity Tracker
用来在onEvent()中获取滑动速度
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); //1000ms内移动的像素数 int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity(); velocityTracker.clear(); velocityTracker.recycle();
-
GestureDetector
用来做手势检测,支持并包含onEvent()中的各种手势,同时额外的支持:onLongPress,onDoubleTap
final GestureDetector gestureDetector = new GestureDetector(this); //解决长按屏幕无法拖动 gestureDetector.setIsLongpressEnabled(false); mButton.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //接管onTouchEvent return gestureDetector.onTouchEvent(event); } });
-
Scroller
弹性滑动
1.2 View的滑动
View的滑动主要有三种方式
-
View本身的scrollTo / scrollBy
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ 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); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
scrollTo() 其实是调用onScrollChanged() 来进行绝对滑动。
这里解释下所谓的滑动:通常我们所理解的一个Layout布局文件只是该视图的显示区域,超过了这个显示区域将不能显示到父视图的区域中 ,也就是说其实内容只是超出了他所在的view的显示区域,因此才不显示的。这里的scrollTo/srollBy 只能移动内容的位置,不能移动view本身的位置。这里的mScrollX / mScrollY 当向左滑动或者向上滑动时取正值,反之取负值。
内容移动,位置不移动,背景不移动,点击事件不移动
-
施加平移动画
动画仅仅移动一个影像而已,但是实际并没有发生移动。
带来的问题:view影像移动到了新的位置,但是系统并不认为他移动了,点击事件同样也在原来位置,点击移动后的View没有响应。
解决方案:使用属性动画 / 在新位置设置一个新的View不显示
内容移动,位置移动,背景移动 (肃然都是假的) 点击事件不移动
<set xmlns:android="http://schemas.android.com/apk/res/android" > android:fillAfter = "true" <translate android:duration="500" android:fromYDelta="-100%" android:toYDelta="0%" > <alpha android:duration="500" android:fromAlpha="0.0" android:interpolator="@android:anim/decelerate_interpolator" android:toAlpha="1.0" /> </translate> </set> ObjectAnimator .ofFloat(view, "rotationX", 0.0F, 360.0F) .setDuration(500) .start();
-
改变View的LayoutParams重新绘制
内容移动,位置移动,点击事件移动
1.3 弹性滑动
目前上面的平移方式都很粗暴,视觉上看会很粗暴,需要一个平缓的滑动,而不是瞬间完成。弹性滑动的基本原理是将一次大的华东分成若干个小的滑动。
实现方法也有三种
-
Scroller
Scroller mScroller = new Scroller(MainApplication.getContext());
private void smoothScroll(int destX, int destY) {
int scrollX = getScrollX();
int deltaX = destX - scrollX;
mScroller.startScroll(scrollX, 0, deltaX, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
首先看下startScroll()
```java
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
startScroll中只是初始化相关参数,并没有实质功能.实际的实现实在invalidate().invalidate()方法会引起View的重绘,也就是会调用onDraw()方法,onDraw()又会调用ViewGroup中的computScroll()方法,但是该方法是个空的方法,需要自己去重写实现。看下我们实现的方法内容。很简单,首先获取Scroller的scrollX和scrollY,然后调用scrollTo移动到指定位置。接着再去调用invalidate()发起第二次重绘.....循环下去。
那么这个scrollX是怎么变化的,可以看到在scrollTo前调用了computeScrollOffset()方法:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
}
}
}
首先获取得到已经滑动的时间,接着需要注意一个变量mDurationReciprocal ,它是什么呢,我们在startScroll时有初始化它mDurationReciprocal = 1.0f / (float) mDuration 。
那么timePassed * mDurationReciprocal 就是已经滑动的时间占据总滑动时间的百分比,接着大家就可以理解了,计算得到当前要移动到的位置,并返回true,如果已经滑动结束,那么就会返回false,不在进行下面的滑动。
-
值动画
与Scroller的机制大致一样,逐渐移动。
float int startx = 0; float final int deltax = 0; ValueAnimator animator = ValueAnimator.ofFloat(0,1).setDuration(1000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float percent = (int)animation.getAnimatedValue(); iv.scrollTo(0 + (int)(percent * deltax),0 ); } });
-
延时策略
- Handler 延时发送message去移动
- View的PostDelayed()
- 使用线程的sleep方法,while循环移动并 sleep
1.4 View的事件分发机制
-
首先时间的分发机制主要会涉及到三分方法
public boolean dispatchTouchEvent(MotionEvent event) public boolean onInterceptTouchEvent(MotionEvent event) public boolean onTouchEvent(MotionEvent event)
-
事件传递逻辑顺序:
对于一个ViewGroup,当点击事件发生时,它的dispatchTouchEvent() 会被调用,如果这个ViewGroup的onIntercaptTouchEvent()返回true,表示它要拦截当前事件,那么事件就会交给当前ViewGroup的TouchEvent来处理;如果返回false表示不拦截,那么就会viewGroup的子元素就会调用dispatchTouchEvent(),如此反复直至事件被处理。
-
事件响应优先级
onTouchListener > onTouchEvent >onClickListener
-
事件传递顺序 activity ->window ->view 如图:
整体传递和处理呈U字型逻辑。
-
事件传递的源码解读
public boolean dispatchTouchEvent(MotionEvent ev) {
// 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();
}
// 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;
}
}
理解下这段源码:两种情况下会去判断是否要拦截。第一种就是按下时MotionEvent.ACTION_DOWN,第二种是mFirstTouchTarget!=null ,这个mFirstTouchTarget 可以这样理解,它会在当前view不处理,交给子view处理时将mFirstTouchTarget 置为false。在交给过子view处理过后,后面每一次都要进行判断是否拦截。也就是只要当前的ViewGroup拦截一次事件,那么后面不需要进行onInterceptEvent判断是否需要拦截,直接进行拦截。 再换个说法,只要当前ViewGroup处理过一次事件(除开按下),那么后面的事件都由他处理。
这里还有个标志位的判断:FLAG_DISALLOW_INTERCEPT;这个标志位一旦被设置,那么它将无法在拦截除ACTION_DOWN之外的事件,因为ACTION_DOWN会重置该标志位。因此在ACTION_DOWN时,必然会调用onInterceptEvent。可以看到 // Handle an initial down 这部分代码对标志位进行了重置.
接下来会去循环 判断子元素是否能够接收到事件,能否接收到有了两个条件:1,在上一级view的区域内2,没有在播放动画。 子元素会去调用它的dispatchTouchEvent。如果当前的子元素的dispatchTouchEvent返回false说明没有处理,那么就会接着for循环,调用同一级的下一个子元素的dispatchTouchEvent;如果的当前的dispatchTouchEvent 返回true;说明子元素处理了改时间,那么就会将mFirstTouchTarget 赋值。也就是我们最开始说的逻辑。
如果循环结束事件都没有被处理,有两种情况:1.ViewGroup没有子元素 2,子元素处理了点击事件,但是在dispatchTouchEvent 返回了false,因为这个方法可以重写。 这两种情况下,viewGroup 交给他的父类即View的dispatchTouchEvent来处理,最终会调用到onTouchEvent来处理。
1.5 滑动冲突处理
滑动冲突主要有三种情况:
- 外部滑动方向和内部滑动方向不一致
- 外部滑动方向和内部滑动方向一致
- 外部滑动方向和内部滑动方向一致 + 不一致
解决方案:
基本思想,根据需求如果需要外部滑动时,就在外部进行拦截,否则不拦截。
具体的实践也有两种实现方式:
- 重写外部的onInterceptTouchEvent,根据需求判断是否拦截
- 外部拦截除了ACTION_DOWN之外的事件,其余全部交给内部处理,当需要时调用外部的处理。
1.6 总结点
在自定义的底层View的onTouchEvent中最好不要直接返回true或者false,而是调用super.onTouchEvent(),去让上一层view去处理返回结果。这里考虑的主要点在于,onClick是触发在View的ACTION_UP,因此必须去调用父类View的onTouchEvent,来触发onClick。否则直接返回结果是不会触发onClick的。
onClic是在ACTION_UP时才会触发,如果在当前View触发了ACTION_DOWN和ACTION_MOVE,但是MOVE出了当前的View范围,就会导致当前的View并不会接收到ACTION_UP,也就不会触发ACTION_DOWN.
(存在疑虑)如果当前的View没有设置OnClick,那么在ACTION_DOWN时就会返回false,也就是说所有的ACTION都会移交给上层的ViewGroup来处理,当前VIew不处理任何ACTION
-
滑动拦截
外部拦截:大于某个值时才进行拦截;一旦拦截,那么后续操作都会由外部来处理,所以要滑动大于某个值才进行拦截。其中外部的ACTION_UP必须要设为false,因为点击时候,可能会触发ACTION_MOVE,但是移动的距离很小,没有触发拦截,也就是说子类View是应该要触发OnClick的,但是如果在ACTION_UP时,父类return true,name就会拦截掉,导致ACTION_UP传递不到View中,也就不会触发OnClick。
-
内部拦截:使用到了getParent().requestDisallowInterceptTouchEvent() 表示ViewGroup是否不拦截;
ViewGroup要把ACTION_DOWN设为不拦截,这样才能到达View,把ACTION_MOVE和ACTION_UP设为拦截。
可以在view的ACTION_DOWN时进行调用getParent().requestDisallowInterceptTouchEvent(true),表示viewGroup不进行拦截,操作交给当前View来处理。当满足某个条件时,让ViewGroup来进行处理,getParent().requestDisallowInterceptTouchEvent(false),即进行拦截,接着调用onInterCeptTopuchEvent,即我们刚才设置拦截。这样就会在上一层进行处理了。