一、View基础知识
1、什么是View:
View是一种界面层的控件的一种抽象,它代表了一个控件。
ViewGroup为控件组。
View中也可以包含多个控件,形成控件树。
ImageButton是一个View,LinearLayout是一个View,也是一个ViewGroup。
2、View的位置参数:
(1)视图坐标系:(子视图在父视图中的位置关系)
以父视图的左上角为原点。
在触控事件MotionEvent中,通过getX()、getY()所获得的坐标就是视图坐标系中的坐标。
View的位置对应View的四个属性:top、left、right、button。
所以说View的宽高分别可以这样表示:
width = right - left
height = bottom - top
获取方式:getLeft(),getRight(),getTop(),getBottom()。
从Android3.0开始,另外还有几个参数:x、y、translationX 和 translationY。
x、y是VIew左上角的坐标,translationX 和 translationY 是View左上角相对于父容器的偏移量。默认为0。
且 x = left + translationX,y = top + translationY
是不是还是不清楚,就是说在View平移的过程中,top、left、right、button这四个值是不会变化的,是原始位置,变化的是x、y、translationX 和 translationY这四个参数。
(2)Android坐标系(以屏幕的左上角为原点)
系统提供了 getLocationOnScreen(intlocation[])这样的方法来获取Android坐标系中点的位置,即该视图左上角在Android坐标系中的坐标。
另外在触控事件MotionEvent中使用getRawX()、getRawY()方法所获得的坐标同样是An坐标系中的坐标。
(3)总结获取各种坐标值的办法:
属于MotionEvent的:
getX(),getY(),(相对父容器的)
getRawX(),getRawY();(相对屏幕的)
属于View自身的:
getTop(),getRight(),getTop(),getBottom()。(相对父容器的)
3、MotionEvent 手指触摸事件类型
ACTION_DOWN、ACTION_MOVE、ACTION_UP 就是这三个啦。
在这里我们将常会获取坐标:
getX/getY:返回相对于当前View左上角的 x 和 y 坐标。(视图坐标系)
getRawX/getRawY:返回相对于手机屏幕左上角的 x 和 y 坐标。(Android坐标系)
-
-
-
- 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 系统能识别的最小滑动距离
获取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop() 。
我在程序中用的时候发现需要这样写才行:要用this才行:
- private int m = ViewConfiguration.get(DemoActivity_1.this).getScaledTouchSlop();
其中的get是为了获取一个ViewConfiguration类型的对象,然后这个对象再调用getScaledTouchSlop方法。
而它的默认值是定义在这里的:
里面的config.xml中:
- <!-- Base "touch slop" value used by ViewConfiguration as a
- movement threshold where scrolling should begin. -->
- <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
5、VelocityTracker速度追踪
用于追踪手指在滑动过程中的速度,包括水平和竖直。
它所谓的滑动速度指的是一段时间内手指划过的像素数,比如假设时间间隔为1s,速度就指的是手指在水平方向从左向右滑过100像素时,速度就是100,那如果向左滑动,速度就是负值啦。
-
- 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 v, MotionEvent event)方法,我们可以处理一些touch事件,但是这个方法太过简单,如果需要处理一些复杂的手势,用这个接口就会很麻烦(因为我们要自己根据用户触摸的轨迹去判断是什么手势)。
Android sdk给我们提供了GestureDetector(Gesture:手势Detector:识别)类,通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。虽然他能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。
GestureDetector这个类对外提供了两个接口:OnGestureListener,OnDoubleTapListener,还有一个内部类SimpleOnGestureListener。
GestureDetector.OnDoubleTapListener接口:用来通知DoubleTap事件,类似于鼠标的双击事件。
1、onDoubleTap(MotionEvent e):
在双击的第二下,按下时触发 。它不能和 onSingleTapConfirmed 共存。
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 mGestureDetector = new GestureDetector(this);
-
- mGestureDetector.setIsLongpressEnabled(false);
-
- boolean consume = mGestureDetector.onTouchEvent(event);
- return consume;
三、View的滑动
要实现View的滑动,就必须监听用户的触摸事件,并根据事件传入的坐标,动态且不断地改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。
不管采用哪一种方式,其实现的思想基本是一致的:
当触摸View时,系统记下当前触摸点的坐标,
当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对前一次坐标点的偏移量,并通过偏移量来修改View的坐标。
这样不断重复,从而实现滑动过程。
三种方法实现View的滑动:
(1)View本身提供的scrollTo/scrollBy。
(2)通过动画给View施加平移效果来实现滑动。
(3)通过改变View的LayoutParams使得View重新布局从而实现滑动。
(4)layout()方法。
(5)offsetLeftAndRight()与offsetTopAndBottom()。
(6)Scroller弹性滑动
1、使用scrollTo/scrollBy:(操作简单,适合对View内容的滑动)
scrollTo、scrollBy方法移动的是View的content内容,即让View的内容移动,
如果在ViewGroup中使用scrollBy、scrollTo方法,那么移动的将是所有的子View,
但如果在View中使用,那么移动的将是View的内容,
例如对TextView这个View而言,文本就是它的内容,对于ImageView而言,drawable就是它的内容,但TextView和ImageView本身的View却没有移动。
所以写的时候应该这样写:在View所在的ViewGroup中来使用:
- ((View)getParent()).scrollBy(offsetX, offsetY);
(1)源码位置在:sources\android\view\View.java
-
-
-
-
-
-
-
-
-
-
- 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();
- }
- }
- }
-
-
-
-
-
-
-
-
-
-
-
- public void scrollBy(int x, int y) {
- scrollTo(mScrollX + x, mScrollY + y);
- }
(2)其中的mScrollX和mScrollY,可以通过getScrollX和getScrollY来获得。
-
-
-
-
-
-
-
- public final int getScrollY() {
- return mScrollY;
- }
在滑动的过程中,mScrollX的值总是等于View的左边缘和View内容左边缘在水平方向的距离,
而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。
View边缘指的是View的位置,由四个顶点组成,
而View内容边缘指的是View中内容的边缘。
scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。假位移啦!!!!!!
这个图里面主要注意它们的那个正负值呐。
![](https://img-blog.csdn.net/20151205192257286)
2、使用动画:(操作简单,适用于没有交互的View和复杂动画效果的View)
(1)使用动画来移动View,主要是操作View的translationX和translationY属性。
(2)动画包括传统的View动画和属性动画。
(3)注意点:同上面的scrollBy 和scrollTo一样,View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽高。
并且如果希望动画后的状态得以保留还必须将 fillAfter属性设置为true,否则动画完成后其动画结果会消失。
(4)属性动画并不存在(3)这样的情况。
(5)情况(3)会导致一个严重的后果,就是移动后不能带着它的点击事件一起移动。那怎么办呢?有两种方法:
第一种:使用属性动画。
第二种:我们可以在新的位置上预先创建一个和目标Button一模一样的Button,它们连onClick事件也一样。所以移动后,设置显示和隐藏,来间接达到移动的目的。
(6)复杂效果用动画。
3、改变布局参数LayoutParams:(操作稍微复杂,适用于有交互的View)
(1)改变Button的参数:主要改变的是margin属性。
- MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
- params.width += 100;
- params.leftMargin += 100;
- mButton.requestLayout();
-
- params.leftMargin = getLeft() + offsetX;
- params.topMargin = getTop() + offsetY;
- LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)getLayoutParams();
用这个params的时候需要考虑父布局的类型,当然还可以使用ViewGroup.MarginLayoutParams来实现这个功能,效果是一样的,并且更加方便,不需要考虑父布局的类型:
- ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();
(2)在Button的左边放置一个空的View,这个空View的默认宽度为0。当我们需要向右移动Button时,只需要重新设置空View的宽度即可。
4、layout方法:
通过调用View的layout方法,给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;
-
- 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;
-
- layout(getLeft() + offsetX,
- getTop() + offsetY,
- getRight() + offsetX,
- getBottom() + offsetY,
- );
-
-
-
-
-
- lastX = rawX;
- lastY = rawY;
- break;
- }
- case MotionEvent.ACTION_UP: {
- break;
- }
- default:
- break;
- }
-
- return true;
- }
5、offsetLeftAndRight()与offsetTopAndBottom()
这个方法相当于系统提供了一个对左右、上下移动的API的封装。
当计算出偏移量后,只需要使用如下代码就可以完成View的重新布局,
效果与使用Layout方法一样,
代码如下所示:
-
- offsetLeftAndRight(offsetX);
-
- offsetTopAndBottom(offsetY);
6、Scroller弹性滑动
Scroller的工作机制:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTp方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。
使用Scroller类通常需要三个步骤:
(1)初始化Scroller:
- Scroller scroller = new Scroller(mContext);
(2)重写computeScroll()方法,实现模拟滑动:
它是scroller的核心,系统在绘制View的时候会在draw()方法中调用该方法。
这个方法实际上就是使用的scrollTo方法,再结合Scroller对象,帮助获取到当前的滚动值。
我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果:
- @Override
- public void computeScroll(){
- super.computeScroll();
-
- if(mScroller.computeScrollOffest()){
- ((View)getParent()).scrollTo(
- mScroller.getCurrX(),
- mScroller.getCurrY()
- );
-
- invalidate();
- }
- }
Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了
getCurrX()、getCurrY()方法来获得当前的滑动坐标。
还有一个注意点就是invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标。
但computeScroll()方法是不会自动调用的,只能通过invalidate()-> draw() -> computeScroll()来间接调用computeScroll()方法,
所以需要在上面的代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的。
而当模拟过程结束以后,scroller.computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动过程。
(3)startScroll开启模拟过程:
有两个重载方法:
- 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一样正负值相反。
(4)具体的使用:
就是在ACTION_UP的时候,写上面的(1)(2)就可以啦,但是要记得再次调用invalidate()来通知View进行重绘。
(5)注意点:
因为scrollTo/scrollBy的滑动过程是瞬间完成的,所以为了用户体验,需要设置弹性滑动。
Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
- Scroller scroller = new Scroller(mContext);
-
-
- private void smoothScrollTo(int destX, int destY){
- int scrollX = getScrollX();
- int delta = destX - scrollX;
-
- mScroller.startScroll(scrollX, 0, delta, 0, 1000);
- invalidate();
- }
-
- @Override
- public void computeScroll(){
- if(mScroller.computeScrollOffest()){
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY();
- postInvalidate();
- }
- }
所以呢,结合上面的(1)(2)(3)(4),我们规整为(5),就是说第一行scroller是必须创建的,下面的computeScroll方法是需要重写的,中间的smoothScrollTo方法是我们自己写的,里面主要是要调用startScroll方法,随后又调用了invalidate方法,这样就可以循环往复的一直调用了:computeScroll()方法是不会自动调用的,只能通过invalidate()-> draw() -> computeScroll()来间接调用computeScroll()方法。
四、弹性滑动
三种方法:
(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;
-
- mScroller.startScroll(scrollX, 0, delta, 0, 1000);
- invalidate();
- }
-
- @Override
- public void computeScroll(){
- if(mScroller.computeScrollOffest()){
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- postInvalidate();
- }
- }
但其实现的重点在于startCroll方法和computeScrollOffest方法的实现。
(2)源码地址:sources\android\widget\Scoller.Java
(3)其实在startScroll中什么都没有做,它只是保存了我们传递的几个参数:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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方法滑动到新的位置,如此反复,直到整个滑动过程结束。
-
-
-
-
-
-
-
-
-
- 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));
-
- mCurrX = Math.min(mCurrX, mMaxX);
- mCurrX = Math.max(mCurrX, mMinX);
-
- mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
-
- 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、事件传递顺序
Activity -> Window -> View
虽然是从上往下的传递,但是当一个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来处理事件。
-
-
-
-
-
-
-
-
-
-
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
-
- if (getWindow().superDispatchTouchEvent(ev)) {
-
- return true;
- }
-
- return onTouchEvent(ev);
- }
2、Window对点击事件的分发过程:
(1)源码位置在:sources\android\view\Window.java
(2)由Activity传递到Window,然后Window要将事件传递给ViewGroup。
(3)Window是一个抽象类,其中的方法都是抽象方法,所以superDispatchTouchEvent 也是一个抽象方法。
-
-
-
-
-
-
- public abstract boolean superDispatchTouchEvent(MotionEvent event);
(4)Window的唯一实现类是 PhoneWindow。唯一的!!!
(5)关于PhoneWindow,我并没有找到它的源码,呵呵。位置应该在:sources\android\policy\PhoneWindow.java 。看吧,PhoneWindow 又把事件分发的任务给了DecorView。
- public boolean superDispatchTouchEvent(MotionEvent event){
- return mDecor.superDispatchTouchEvent(event);
- }
3、DecorView对点击事件的分发过程:
反正最后是从DecorView传给了View了,过程先略。
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函数中讲解!
-
-
-
- @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;
-
-
-
-
-
-
- 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 {
-
-
-
-
-
-
- intercepted = true;
- }
-
-
-
- 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) {
- 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;
-
-
-
-
- removePointersFromTouchTargets(idBitsToAssign);
- final int childrenCount = mChildrenCount;
- if (newTouchTarget == null && childrenCount != 0) {
- final float x = ev.getX(actionIndex);
- final float y = ev.getY(actionIndex);
-
-
- final View[] children = mChildren;
-
-
- final boolean customOrder = isChildrenDrawingOrderEnabled();
-
-
-
-
- for (int i = childrenCount - 1; i >= 0; i--) {
- final int childIndex = customOrder ?
- getChildDrawingOrder(childrenCount, i) : i;
- final View child = children[childIndex];
-
-
-
-
-
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- continue;
- }
-
-
-
-
-
-
- newTouchTarget = getTouchTarget(child);
- if (newTouchTarget != null) {
-
-
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- break;
- }
-
-
- resetCancelNextUpFlag(child);
-
-
-
-
-
-
-
-
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
-
- mLastTouchDownTime = ev.getDownTime();
- mLastTouchDownIndex = childIndex;
- mLastTouchDownX = ev.getX();
- mLastTouchDownY = ev.getY();
-
-
-
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
- alreadyDispatchedToNewTouchTarget = true;
-
-
-
- break;
- }
- }
- }
-
-
- if (newTouchTarget == null && mFirstTouchTarget != null) {
-
-
- newTouchTarget = mFirstTouchTarget;
- while (newTouchTarget.next != null) {
- newTouchTarget = newTouchTarget.next;
- }
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- }
- }
- }
-
-
-
-
-
-
-
-
-
- if (mFirstTouchTarget == null) {
-
-
-
-
-
-
-
-
- handled = dispatchTransformedTouchEvent(ev, canceled, null,
- TouchTarget.ALL_POINTER_IDS);
- } else {
-
-
- 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;
- }
5、View对点击事件的处理过程
(1)源码位置在:sources\android\view\View.java
(2)在上面的顶级View中,如果顶级View没有处理事件,而顶级View的子元素也没有人处理这个事件,那就会到这里来由View来处理事件。
(3)View对点击事件的处理过程稍微简单一些。
(4)注意,这里的View不包含ViewGroup。只是简单的单个View的处理,因为他没有子元素因此不能向下传递事件,所以它只能自己处理事件。
(5)下面对View源码中的dispatchTouchEvent进行分析:
-
-
-
-
-
-
-
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onTouchEvent(event, 0);
- }
-
- if (onFilterTouchEventForSecurity(event)) {
-
-
-
-
-
-
- ListenerInfo li = mListenerInfo;
- if (li != null
- && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- return true;
- }
-
-
-
-
- if (onTouchEvent(event)) {
- return true;
- }
- }
-
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
- }
- return false;
- }
(6)下面对View源码中的onTouchEvent方法进行分析:
六、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:
-
-
- 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)内部拦截法:
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素。
如果子元素需要此事件就消耗,否则就交给父容器进行处理。
这种方法和Android中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来比外部拦截法要稍微复杂。
下面提供伪代码,主要是重写了子元素的 dispatchTouchEvent 方法:
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
-
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- switch (event.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;
- }
-
- mLastX = x;
- mLastY = y;
- return true;
- }
当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不同有改动。
除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,
(这里ACTION_DOWN不能让父元素默认拦截,因为一旦父元素拦截,剩下的指令序列就都由父元素来处理了)
这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false); 方法时,父元素才能继续拦截所需的事件。
父元素去哪里改呢,当然是 onInterceptTouchEvent 方法啦。
- public boolean onInterceptTouchEvent(MotionEvent event) {
- int action = event.getAction();
- if(action == MotionEvent.ACTION_DOWN) {
- return false;
- } else {
- return true;
- }
- }
我们去源码中看一下ViewGroup中的 requestDisallowInterceptTouchEvent 这个方法吧:是通过操控disallowIntercept 来达到目的的。
-
-
-
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-
- if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
-
- return;
- }
-
- if (disallowIntercept) {
- mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
- } else {
- mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- }
-
-
- 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";
-
-
-
-
-
- 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++) {
-
-
-
- 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));
-
-
-
- createList(layout);
- mListContainer.addView(layout);
- }
- }
-
-
-
-
-
- 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: {
-
-
-
- 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: {
-
-
-
- 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;
-
- 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>
-
-
- 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>
-
-
- 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;
- }
-
-
-
- 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()) {
-
-
-
- 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);
-
-
-
- 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();
-
-
-
-
-
-
-
- if (action == MotionEvent.ACTION_DOWN) {
- mLastX = x;
- mLastY = y;
- if (!mScroller.isFinished()) {
- mScroller.abortAnimation();
- return true;
- }
- return false;
- } else {
- return true;
- }
- }
推荐采用外部拦截法来解决常见的滑动冲突!!!