一、View基础知识
1、什么是View:
2、View的位置参数:
(1)视图坐标系:(子视图在父视图中的位置关系)
(2)Android坐标系(以屏幕的左上角为原点)
(3)总结获取各种坐标值的办法:
3、MotionEvent 手指触摸事件类型
/**
* 按下
*/
public static final int ACTION_DOWN = 0;
/**
* 抬起
*/
public static final int ACTION_UP = 1;
/**
* 移动
*/
public static final int ACTION_MOVE = 2;
/**
* 触摸动作取消
*/
public static final int ACTION_CANCEL = 3;
/**
* 触摸动作超出边界
*/
public static final int ACTION_OUTSIDE = 4;
/**
* 多点触摸按下动作
*/
public static final int ACTION_POINTER_DOWN = 5;
/**
* 多点离开动作
*/
public static final int ACTION_POINTER_UP = 6;
4、TouchSlop 系统能识别的最小滑动距离
private int m = ViewConfiguration.get(DemoActivity_1.this).getScaledTouchSlop();
而它的默认值是定义在这里的:
<!-- Base "touch slop" value used by ViewConfiguration as a
movement threshold where scrolling should begin. -->
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
5、VelocityTracker速度追踪
// 首先在View的onTouchEvent方法中获取追踪速度的对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
// 将追踪速度对象与事件绑定起来:
velocityTracker.addMovement(event);
// 获取速度之前先计算速度,这一句不可缺:
velocityTracker.computeCurrentVelocity(1000);
// 分别计算水平和竖直速度:
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
// 不需要使用它的时候,来重置和回收:
velocityTracker.clear();
velocityTracker.recycle();
6、GestureDetector手势检测
一般情况下,我们知道View类有个View.OnTouchListener内部接口,通过重写他的onTouch(View
Android
GestureDetector这个类对外提供了两个接口:OnGestureListener,OnDoubleTapListener,还有一个内部类SimpleOnGestureListener。
GestureDetector.OnDoubleTapListener接口:用来通知DoubleTap事件,类似于鼠标的双击事件。
1、onDoubleTap(MotionEvent e):
在双击的第二下,按下时触发
2、onDoubleTapEvent(MotionEvent e):
通知双击手势中的事件,包含down、up和move事件。
(这里指的是在双击之间发生的事件,例如在同一个地方双击会产生双击手势,而在双击手势里面还会发生down和up事件,这两个事件由该函数通知);
双击的第二下按下时,down和up都会触发,可用e.getAction()区分。
3,onSingleTapConfirmed(MotionEvent e):
用来判定该次点击是单击而不是双击,如果连续点击两次就是双击手势,如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为单击而不是双击,然后触发SingleTapConfirmed事件。这个方法不同于onSingleTapUp,他是在GestureDetector确信用户在第一次触摸屏幕后,没有紧跟着第二次触摸屏幕,也就是不是“双击”的时候触发
GestureDetector.OnGestureListener接口:用来通知普通的手势事件,该接口有如下六个回调函数:
1、onDown(MotionEvent e):
down事件;
2、onSingleTapUp(MotionEvente):
一次点击up事件;在touch down后又没有滑动(onScroll),又没有长按(onLongPress),然后Touch up时触发。
点击一下非常快的(不滑动)Touchup:
onDown->onSingleTapUp->onSingleTapConfirmed
点击一下稍微慢点的(不滑动)Touchup:
onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed
3、onShowPress(MotionEvent e):
down事件发生而move或则up还没发生前触发该事件;按下了还没有滑动时触发(与onDown,onLongPress)。
比较:onDown只要按下后一定立刻触发。而按下后停留一会儿且没有滑动,则先触发onShowPress再是onLongPress。
所以按下后一直不滑动按照:onDown->onShowPress->onLongPress这个顺序触发。
4、onLongPress(MotionEvent e):
长按事件;按下了不移动一直按着的时候触发。
5、onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY):
滑动手势事件;按下了滑动一点距离后,在ACTION_UP时才会触发参数:
e1 第1个ACTION_DOWN 事件并且只有一个;
e2 最后一个ACTION_MOVE 事件 ;
velocityX X轴上的移动速度,像素/秒
velocityY Y轴上的移动速度,像素/秒.
触发条件:X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒
6、onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY):
在屏幕上拖动事件。无论是用手拖动view,或者是以抛的动作滚动,都会多次触发,这个方法在ACTION_MOVE动作发生时就会触发抛:
手指触动屏幕后,稍微滑动后立即松开:
onDown-----》onScroll----》onScroll----》onScroll----》………----->onFling
拖动:
onDown------》onScroll----》onScroll------》onFiling
// 首先需要创建GestureDetector对象,实现OnGestureListener接口:下面这个语句写的有点问题
GestureDetector mGestureDetector = new GestureDetector(this);
// 解决长按屏幕后无法拖动的现象:
mGestureDetector.setIsLongpressEnabled(false);
// 接管目标View 的 onTouchEvent 方法,在待监听View的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
三、View的滑动
1、使用scrollTo/scrollBy:(操作简单,适合对View内容的滑动)
((View)getParent()).scrollBy(offsetX, offsetY);
/**
* 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
*/
/*
* 实现的是基于所传递参数的绝对滑动,到x和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);
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);
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
return mScrollY;
}
在滑动的过程中,mScrollX的值总是等于View的左边缘和View内容左边缘在水平方向的距离,
而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。
View边缘指的是View的位置,由四个顶点组成,
而View内容边缘指的是View中内容的边缘。
scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。假位移啦!!!!!!
这个图里面主要注意它们的那个正负值呐。
2、使用动画:(操作简单,适用于没有交互的View和复杂动画效果的View)
3、改变布局参数LayoutParams:(操作稍微复杂,适用于有交互的View)
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
// 或者mButton.setLayoutParams(params);
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)getLayoutParams();
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();
4、layout方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 每次进来获取以下现在的坐标值:
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 记录触摸点坐标:
lastX = x;
lastY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量(注意这个左上右下的顺序)
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY,
);
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 每次进来获取以下现在的坐标值:
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 记录触摸点坐标:
lastX = rawX;
lastY = rawY;
break;
}
case MotionEvent.ACTION_MOVE: {
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量(注意这个左上右下的顺序)
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY,
);
/*
* 这里是必须要注意的一点
* 使用绝对坐标系,在每次执行完ACTION_DOWN的逻辑后,
* 一定要重新设置初始坐标,这样才能准确的获取偏移量。
* */
lastX = rawX;
lastY = rawY;
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return true;
}
5、offsetLeftAndRight()与offsetTopAndBottom()
// 同时对left和right进行偏移:
offsetLeftAndRight(offsetX);
// 同时对top和bottom进行偏移:
offsetTopAndBottom(offsetY);
6、Scroller弹性滑动
Scroller scroller = new Scroller(mContext);
@Override
public void computeScroll(){
super.computeScroll();
// 判断Scroller是否执行完毕
if(mScroller.computeScrollOffest()){
((View)getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY()
);
// 通过重绘来不断调用computeScroll:
invalidate();
}
}
public void startScroll(int startX, int startY, int dx, int dy, int duration)
public void startScroll(int startX, int startY, int dx, int dy)
在获取坐标时,通常可以使用
getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过还是要注意正负值,和scrollTo、scrollBy一样正负值相反。
Scroller scroller = new Scroller(mContext);
// 缓慢滚动到指定位置:
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffest()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY();
postInvalidate();
}
}
四、弹性滑动
三种方法:
(1)使用Scoller。
(2)通过动画。
(3)使用延时策略
1、使用Scoller:
(1)刚刚已经写过了下面的这两个函数:这两个方法写在活动中:
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置:
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();//通知View进行重绘
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffest()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
但其实现的重点在于startCroll方法和computeScrollOffest方法的实现。
(2)源码地址:sources\android\widget\Scoller.java
(3)其实在startScroll中什么都没有做,它只是保存了我们传递的几个参数:
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
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;
}
(4)真正的滑动实现是在startScroll下面的 invalidate()方法中。
invalidate方法会导致View重绘,View重绘会调用draw方法,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现(它在View.java中),因此需要我们自己去重写实现。
具体过程:当View重绘后会在draw方法中调用computeScroll方法,而computeScroll方法又会去向Scroller获取当前的scrollX 和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘和上一次重绘过程一样的,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
/*
* 这个方法会根据事件的流逝来计算出当前scrollX和scrollY的值。
* 根据时间流逝的百分比来算出scrollX和scrollY改变的百分比并计算出当前的值
* 它返回true表示滑动还没有结束,false表示滑动已经结束。
* */
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
是不是很神奇?切
2、通过动画:
动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。
(1)下面的代码可以让一个View的内容在100ms内向左移动100像素:
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).satrt();
(2)我们可以利用动画的特性来实现一些动画不能实现的效果。我们可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int)(deltaX * fraction), 0);
}
});
animator.start();
在这里例子中,我们并没有在ofInt方法中直接对目标对象进行移动,而是在下面的scrollTo中进行实际的移动,这个思想和上面的Scroller滑动思想是相同的。
3、使用延时策略:
延时策略的工作机制:通过发送一系列延时消息从而达到一种渐近式的效果。
(1)两种方式:Handler或View的postDelayed方法,或使用线程的sleep方法。
(2)其实还是要计算滑动过程中的百分比的。这里有一个小例子:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount = 0;
private Handler mHandler = new Handler(){
public void handleMessage(Message 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.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
五、View的事件分发机制
点击事件的事件分发,就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View。
1、点击事件的分发过程由三个很重要的方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,dispatch派分。如果事件能够传递给当前View,这个方法就一定会被调用,
返回结果受当前View的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法的内部调用,用来判断是否拦截某个事件,如果当前View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,
返回结果表示是否拦截当前事件。true表示拦截。
public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法中调用,用来处理点击事件,
返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View再也无法接收到该事件的后续事件。
就是说如果消耗了down,还可以消耗move,up等,如果不消耗,后续的就都没有了。
但如果不消耗,该事件序列就要交由上一层来处理。
它们三者的关系用伪代码表示的话就是这样的:是不是很简单的样子?
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
2、OnTouchListener 和 OnTouchEvent 的关系
如果一个View并没有给它设置OnTouchListener,也就不存在什么问题了,就按上面的程序走。
但如果有设置了OnTouchListener,那么它里面的 onTouch 方法就会被调用。
这时候问题就来了,如果 onTouch 方法返回的是 false,则当前View的onTouchEvent 方法会被调用;
但如果onTouch 方法返回的是true,那么当前View的 onTouchEvent 方法就不会被调用了!
所以说呢,给View设置的 OnTouchListener 的优先级要比 onTouchEvent 要高,
两者只会有一个返回true,先询问 OnTouchListener,不行再去看看 OnTouchEvent。
在onTouchEvent方法中,还可能设置有 OnClickListener,那么它的 onClick 方法会被调用,
也就是说,只有 onTouchEvent被调用了,onClick 才有调用的机会,所以说onClick的优先级是最低的。
3、事件传递顺序
虽然是从上往下的传递,但是当一个View 的 onTouchEvent 返回false(参考下面Tips中的(4)),那么它的父容器的 onTouchEvent 将会被调用,如果还返回false就以此类推的往上推,直到Activity。
4、结论Tips
(1)同一个事件序列:是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。
其中会有一个down,多个move,一个up事件。
(2)正常情况下,一个事件序列只能被一个VIew拦截且消耗,因为一旦拦截,剩下的就都交给拦截的那个View了。不过可以采取特殊手段,比如说一个View将本该自己处理的事件通过onTouchEvent 强行传递给其他View处理。
(3)一个View一旦决定拦截,那剩下的事件序列都会给它,而且它的 onInterceptTouchEvent 就不会再被调用了。
(4)某个VIew一旦开始处理事件,也就是到了onTouchEvent的地步,如果它不消耗 ACTION_DOWN事件,也就是说onTouchEvent返回了false,那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理。就是说事件一旦交给一个VIew处理,他就必须消耗掉,不然同一事件序列中剩下的事件就不再交给它来处理了。
(5)(没看懂这条)如果VIew不消耗除 ACTION_DOWN 以外的其他事件(就是说消耗了ACTION_DOWN,却没消耗其他的),那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前View可以持续接到后续的事件,最终这些消失的点击事件会传递给Activity处理。
(6)ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent 方法默认返回 false。
(7)View没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
(8)View 的onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。
View的longClickable 属性默认都为false,clickable 属性要看具体的控件,比如Button为true,TextView为false。
(9)View的enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个View是disable状态的,只要它的clickable 或者 longClickable 有一个为true,那么它的onTouchEvent 就返回ture。
(10)onClick会发生的前提是当前的View 是可点击的,并且它收到了 down 和 up的事件。
(11)事件传递过程是由外向内的,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,就是下面的那个FLAG_DISALLOW_INTERCEPT标志位,但是ACTION_DOWN事件除外。
5、事件分发的源码解析
1、Activity对点击事件的分发过程:
(1)源码位置在:sources\android\app\Activity.java
(2)这里是一个事件发生时最先到达的地方。
(3)Activity调用它的dispatchTouchEvent来进行事件派发。
(4)看第二个if语句,首先Activity将事件交给Windows,然后Windows会调用它的superDispatchTouchEvent,如果成功了,说明Activity下面的子处理了该事件,返回true,否则就会由下级传回来来调用Activity最下面的那个onTouchEvent来处理事件。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 交给下级Window去处理
if (getWindow().superDispatchTouchEvent(ev)) {
// 下级处理成功,返回false。
return true;
}
// 下级处理失败,退回来自己在onTouchEvent中处理。
return onTouchEvent(ev);
}
2、Window对点击事件的分发过程:
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
(4)Window的唯一实现类是 PhoneWindow。唯一的!!!
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.policy.PhoneWindow, which you should instantiate when needing a
* Window. Eventually that class will be refactored and a factory method
* added for creating Window instances without knowing about a particular
* implementation.
*/
(5)关于PhoneWindow,我并没有找到它的源码,呵呵。位置应该在:sources\android\policy\PhoneWindow.java 。看吧,PhoneWindow 又把事件分发的任务给了DecorView。
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
3、DecorView对点击事件的分发过程:
4、顶级View对点击事件的分发过程:
我这里需要加一下我一直没搞懂的陈述:
(1)就是在ViewGroup中的 dispatchTouchEvent 方法,其实所有的事件序列中的事件包括ACTION_DOWN、ACTION_MOVE和ACTION_UP都会进入到这里来进行事件的分配。
(2)对于ACTION_DOWN,如果ViewGroup拦截了ACTION_DOWN,就会导致 mFirstTouchTarget == null,interception == true;这样的话当其他的后续的事件到来时,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 语句无法进入,所以ViewGroup拦截了ACTION_DOWN以后,这个事件的后续只能由ViewGroup来处理。
(3)如果ACTION_DOWN是由子元素拦截的,那么 mFirstTouchTarget != null,interception == false,这样呢当其他后续的事件到来时,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 语句还是可以进入的。然后可以执行到onInterceptTouchEvent 方法,默认的ViewGroup的这个方法是都返回false的,也就是ViewGroup不拦截任何的事件,并且这也意味着,一旦某个子元素拦截了ACTION_DOWN,那后续的事件序列也都交给这个子元素来处理了。但是我们在写自己的ViewGroup例如LinearLayout时,就可以重写onInterceptTouchEvent方法,然后让它可以在例如ACTION_MOVE的时候返回ture,这样ViewGroup就可以实现没有拦截ACTION_DOWN并交给了子元素,但是却拦截了后面的ACTION_MOVE,这也就是下面在滑动冲突中讲到的外部拦截法。
是不是很神奇呀!
顶级View一般是一个ViewGroup,所以我们去看ViewGroup。
(1)源码位置在:sources\android\view\ViewGroup.java
(2)下面我们将一直在ViewGroup的超长dispatchTouchEvent函数中讲解!
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/*
* 当新的一轮点击到来的时候,从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.
/*
* 至少我知道在这个函数中最终将mFirstTouchTarget设为null。
* mFirstTouchTarget代表的就是一个事件序列中第一个拦截的对象,
* 所以这里需要重置。
* */
cancelAndClearTouchTargets(ev);
/*
* 如果事件是ACTION_DOWN,
* ViewGroup就会在resetTouchState中重置下面的FLAG_DISALLOW_INTERCEPT标志位。
* 重置的方式是这样的:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
* */
resetTouchState();
}
// Check for interception.
/*
* 这个标识很重要,因为它一旦被标志位true,意味着下面的各种if语句都进不去了,
* 意味着本ViewGroup拦截了该事件,并且后续的事件序列直接由该ViewGroup处理,
* 而不是进入各种if中判断是否需要拦截。
* */
final boolean intercepted;// 拦截标识
/*
* 这个if中需要满足两个条件:
* (1)actionMasked == MotionEvent.ACTION_DOWN:
* 该事件是否为点击下按事件时成立,就是说新的一轮事件到来
* (2)mFirstTouchTarget != null:
* 当ViewGroup不拦截事件并将事件交给子元素处理时,成立,mFirstTouchTarget指向这个子元素。
* 而且在ViewGroup中,默认onInterceptTouchEvent返回false,它是不拦截任何事件的,
* 但是在LinearLayout中可能就会拦截啊,可以改写啊。
* 而且,当第二个条件成立时,此时发生的事件序列就是ACTION_MOVE或者ACTION_UP,都会进入到这个if语句中。
* */
/*
* 所以说呢,当子元素成功拦截了事件或者下按事件发生的时候就会进入if语句。
* 所以说呢,如果子元素没有处理,并且是move和up发生的时候就无法进入该if语句。
* 但为什么这样设定呢,因为如果子元素没有处理的话,事件序列中的其他事件就会直接由ViewGroup来处理了,
* 不需要来这里来判断一下到底要不要拦截事件了。那如果是move和up也是同样的,不需要来这里来判断要不要拦截事件。
* */
/*
* 也就相当于说,一个事件,第一次因为ACTION_DOWN进入这里,然后ViewGroup判断是否来拦截。
* 之后在子元素成功处理后,因为子元素是可以通过FLAG_DISALLOW_INTERCEPT标志位来干预父元素的事件分发过程,所以又来这里来要看是否拦截。
* */
/*
* 为什么总说一旦父元素拦截ACTION_DOWN以后其他的事件序列就只能由父元素来处理呢?
* 是因为如果父元素拦截了ACTION_DOWN,那么mFirstTouchTarget == null
* 当ACTION_MOVE和ACTION_UP到来的时候,这条if语句就不会进入了,
* 然后intercepted = true;表示事件序列由父元素全拦截了。
* */
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/*
* 通常事件传递过程是由外向内的,
* 但是通过 requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,
* 不过ACTION_DOWN事件除外。
* 干预表现在子元素已经拦截了事件,
* 但是可以通过requestDisallowInterceptTouchEvent来控制
* ACTION_MOVE和ACTION_UP能不能够进入到这里来。
* */
/*
* FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截处理ACTION_DOWN以外的其他点击事件了。
* 因为在事件分发时,ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT标志位,表示另一次事件开始。
* */
/*
* 子View干涉ViewGroup的过程:
* 初始化:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
* 在子View中FLAG_DISALLOW_INTERCEPT被重置,也就是要去干扰,
* 然后mGroupFlags & FLAG_DISALLOW_INTERCEPT为1
* 然后(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 为true
* 然后disallowIntercept为true
* 然后导致if (!disallowIntercept)无法进入。
* */
/*
* FLAG_DISALLOW_INTERCEPT标志位有什么用呢?
* 当面对滑动冲突时,我们可以考虑用这种方法去解决问题。
* */
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
/*
* 所以说onInterceptTouchEvent并不是每次事件都会被调用的。
* 而dispatchTouchEvent却会在每次都调用。
* 对于原始的ViewGroup,onInterceptTouchEvent会返回false,
* 但是对于你自己写的LinearLayout,则可以修改这个函数,
* 让它对ACTION_DOWN、ACTION_MOVE、ACTION_UP做出不同的选择。
* */
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.
/*
* 就是说没有子元素mFirstTouchTarget,而且事件也不是ACTION_DOWN,
* 没人管那就只能自己拦截了。
* */
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
/*
* 当ViewGroup不拦截事件的时候,intercepted=false,事件会向下分发由它的子View进行处理
* 所以说一旦ViewGroup拦截了事件,intercepted=true,
* 意味着事件序列中的任何事件都不再会传给子元素了,由父元素全权处理。
* 所以intercepted=true一定要谨慎设置。
* */
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
/*
* 遍历ViewGroup的所有子元素,判断子元素是否能够接收到点击事件。
* */
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
/*
* 判断子元素是否能够接收到点击事件:
* (1)canViewReceivePointerEvents:子元素是否在播动画。
* (2)isTransformedTouchPointInView:点击事件的坐标是否落在子元素的区域内。
* */
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
/*
* 如果上面那个if语句没有成立,说明这个子元素是可以拦截事件的,
* 所以新的TouchTarget出现了,就是这个子元素。
* */
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实际上调用的就是子元素的dispatchTouchEvent方法。
* 下面的第三个参数中child一定不为null,所以child的dispatchTouchEvent一定会被调用。
* 子元素的dispatchTouchEvent返回true,
* 意味着dispatchTransformedTouchEvent也返回ture,
* 表示事件被子元素分发成功,并break跳出循环。
* */
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/*
* 分发成功后,在addTouchTarget会对mFirstTouchTarget进行赋值
* */
newTouchTarget = addTouchTarget(child, idBitsToAssign);
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;
}
}
}
/*
* 有两种情况遍历所有的子元素后事件也没有处理:
* (1)ViewGroup根本没有子元素
* (2)子元素的dispatchTouchEvent都返回了false。
* 这种情况下只能ViewGroup自己来处理事件了。
* */
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
/*
* 注意第三个参数:null,在上面变量子元素的时候这里放的是child。
* 如果是null,dispatchTransformedTouchEvent内部就会调用:
* super.dispatchTouchEvent(event);
* 很显然,这里就转到了View的dispatchTouchEvent(event)方法,即点击事件开始交由View来处理。在View中有onTouchEvent。
* 其实父元素ViewGroup的onTouchEvent就是指的是View中的onTouchEvent方法,它自己这里是没有的。因为ViewGroup是继承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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
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;
}
5、View对点击事件的处理过程
(1)源码位置在:sources\android\view\View.java
(2)在上面的顶级View中,如果顶级View没有处理事件,而顶级View的子元素也没有人处理这个事件,那就会到这里来由View来处理事件。
(3)View对点击事件的处理过程稍微简单一些。
(4)注意,这里的View不包含ViewGroup。只是简单的单个View的处理,因为他没有子元素因此不能向下传递事件,所以它只能自己处理事件。
(5)下面对View源码中的dispatchTouchEvent进行分析:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
/*
* 首先会判断有没有设置OnTouchListener。
* 如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会调用,
* 这样做的好处是方便外界处理点击事件。
* */
ListenerInfo li = mListenerInfo;
if (li != null
&& li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
/*
* 优先级低于OnTouchListener
* */
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
(6)下面对View源码中的onTouchEvent方法进行分析:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
/*
* 当View处于不可用状态下时,View照样会消耗点击事,
* 但它并不对事件做出任何的反映
* */
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
/*
* 如果View设置有代理,那么还会执行mTouchDelegate的onTouchEvent方法,
* 这个onTouchEvent的工作机制看起来和OnTouchListener类似,这里我们不做研究
* */
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/*
* 这里是对点击事件的具体处理。
* 可以发现的是View的CLICKABLE和LONG_CLICKABLE只要有一个为true,
* 那么这个View就消耗这个事件,即onTouchEvent返回ture,不管他是不是DISABLE状态。
* 这个证明了前面(8)(9)(10)的结论。
* */
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
/*
* 当up事件发生时,就会触发performClick()方法。
* */
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true);
}
if (!mHasPerformedLongPress) {
// 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)) {
/*
* 如果View设置了OnClickListener,
* 那么performClick()方法内部会调用它的onClick方法
* */
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
六、View的滑动冲突
1、常见的滑动冲突场景:
(1)外部滑动方向和内部滑动方向不一致。
(2)外部滑动方向和内部滑动方向一致。
(3)上面两种情况的嵌套。
2、滑动冲突的处理规则:
具体来说:就是根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
也就是说可以根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。
对于场景(1)如下解决办法:(一左一右,或者一上一下)
(1)可以根据滑动路径和水平方向的夹角。
(2)可以根据水平方向和竖直方向的距离差。
(3)可以根据水平方向和竖直方向的速度差。
对于场景(2)如下解决办法:(同上同下,或者同左同右)
一般需要在业务上寻找突破点。
比如业务上有规定:当初与某种状态时需要外部View相应用户的滑动,而处于另一种状态时需要内部View来响应View的滑动。
对于场景(3)也只能从业务上寻找突破点。
3、滑动冲突的解决方式:
(1)外部拦截法:
所谓外部拦截法就是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。
外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截即可。
下面给出伪代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 必须为fasle,不然父类容器拦截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
}
(2)内部拦截法:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 这里的意思是设置让父容器无法拦截ACTION_DOWN事件:
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此类点击事件){
// 如果需要的话就让父类容器可以接收ACTION_MOVE事件。
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不同有改动。
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
/**
* {@inheritDoc}
*/
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
(3)举例:在一个水平布局的LinearLayout中添加三个并列的ListView,所以父容器左右移动,子容器上下移动,造成冲突。
首先看一下Activity中的初始化代码:
package com.ryg.chapter_3;
import java.util.ArrayList;
import com.ryg.chapter_3.R;
import com.ryg.chapter_3.ui.HorizontalScrollViewEx;
import com.ryg.chapter_3.utils.MyUtils;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;
public class DemoActivity_1 extends Activity {
private static final String TAG = "DemoActivity_1";
/*
* HorizontalScrollViewEx mListContainer是父容器,是自定义View。
* 下面的代码中向父容器中添加了三个ListView。
* */
private HorizontalScrollViewEx mListContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo_1);
Log.d(TAG, "onCreate");
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
for (int i = 0; i < 3; i++) {
/*
* 这个layout就是一个TextView和ListView的组合布局,它的父容器是mListContainer
* */
ViewGroup layout = (ViewGroup) inflater.inflate(
R.layout.content_layout, mListContainer, false);
layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.title);
textView.setText("page " + (i + 1));
layout.setBackgroundColor(Color
.rgb(255 / (i + 1), 255 / (i + 1), 0));
/*
* 为layout中的TextView和ListView添加内容:
* */
createList(layout);
mListContainer.addView(layout);
}
}
/*
* 为layout中的TextView和ListView添加内容,
* ListView中的每个Item又是一个TextView文本。
* */
private void createList(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.list);
ArrayList<String> datas = new ArrayList<String>();
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
R.layout.content_list_item, R.id.name, datas);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(DemoActivity_1.this, "click item",
Toast.LENGTH_SHORT).show();
}
});
}
}
我们只需要修改父容器需要拦截事件的条件即可。对于本例来说,父容器的拦截条件就是滑动过程中水平距离差比竖直距离差大,在这种情况下,父容器就拦截当前点击事件,根据这一条件进行相应修改(正常情况下父容器的onInterceptTouchEvent都是默认返回false不拦截的),修改后的HorizontalScrollViewEx父容器的onInterceptTouchEvent方法如下所示:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
/*
* 这里必须是false,不然父容器拦截了ACTION_DOWN以后,子容器再也接收不到任何事件了
* */
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
/*
* 主要的逻辑就这么点啦,呵呵
* */
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
/*
* 这里也必须是false,不然如果是子容器拦截了上面的事件,子容器将接收不到up事件,就无法结束了
* */
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
下面给出HorizontalScrollViewEx的完整代码:
package com.ryg.chapter_3.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span> * 这里必须是false,不然父容器拦截了ACTION_DOWN以后,子容器再也接收不到任何事件了
<span style="white-space:pre"> </span> * */
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
/*
* 主要的逻辑就这么点啦,呵呵
* */
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span> * 这里也必须是false,不然如果是子容器拦截了上面的事件,子容器将接收不到up事件,就无法结束了
<span style="white-space:pre"> </span> * */
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
/*
* 要根据移动的距离来判断当前显示哪个ListView。
* */
case MotionEvent.ACTION_UP: {
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
} else {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
/*
* 弹性滑动
* */
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
/*
* 弹性滑动
* */
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
我们只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父容器拦截ACTION_MOVE和ACTION_UP事件即可。为了重写ListView的dispatchTouchEvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改:
package com.ryg.chapter_3.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ListView;
public class ListViewEx extends ListView {
private static final String TAG = "ListViewEx";
private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;
// 分别记录上次滑动的坐标:
private int mLastX = 0;
private int mLastY = 0;
public ListViewEx(Context context) {
super(context);
}
public ListViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListViewEx(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setHorizontalScrollViewEx2(
HorizontalScrollViewEx2 horizontalScrollViewEx2) {
mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
}
/*
*
* */
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
/*
* 不允许父容器拦截ACTION_DOWN:
* */
case MotionEvent.ACTION_DOWN: {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
/*
* 判断逻辑,根据逻辑来决定是否允许父容器拦截ACTION_MOVE:
* */
if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
我们还需要修改HorizontalScrollViewEx父容器的onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
/*
* 设置父容器不拦截ACTION_DOWN,交由子容器处理。
* 子容器在接收到ACTION_DOWN以后在dispatchTouchEvent中对父容器设置不允许拦截事件。
* 然后在子容器的dispatchTouchEvent中的ACTION_MOVE中判断子容器是否要拦截此事件,
* 如果子容器不拦截,就解开对父容器不允许拦截事件的条件,
* 这样事件又从子容器推回到父容器中。
* */
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
推荐采用外部拦截法来解决常见的滑动冲突!!!