Android开发艺术探索——View的事件体系

这是我在学习过程中总结的知识
目的是希望日后回来看或者需要用的时候可以 一目了然 # 的回顾、巩固、查缺补漏
不追求详细相当于书本的精简版或者说是导读(想看详细的直接对应翻书),但会尽力保证读者都能快速理解和快速使用(随理解加深会总结的更加精简),但必要时会附上一些较详细解释的链接
脚注是空白的:表示还没弄懂的知识,了解后会添加

这一章大致讲的是深入对View的理解以便写出完美的自定义控件

3.1 View的基础知识

主要介绍的内容有:

  • View的位置参数
  • MotionEvent和TouchSlop对象
  • VelocityTracker
  • GestureDetector和Scroller对象

3.1.1 什么是View

  • View是Android中所有控件的基类
  • ViewGroup是控件组,内部包含了许多个控件即一组View
  • ViewGroup也继承了View,这就意味了View本身就可以是单个控件或多个控件组成的一组控件

例如:Button是一个View、LinearLayout不仅是一个View而且还是个ViewGroup

3.1.2 View的位置参数

  • top:左上角纵坐标
  • left:左上角横坐标
  • right:右下角横坐标
  • bottom:右下角纵坐标

View 的坐标是相对于父容器的左上角为中心的,x轴指向右,y轴指向下
Android3.0之后增加了4个参数

  • x、y:是View左上角的坐标
  • translationX、Y:是View左上角相对于父容器的偏移量

3.1.3 MotionEvent和TouchSlop

1. MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种

  • ACTION_DOWN——手指刚接触屏幕
  • ACTION_MOVE——手指在屏幕上移动
  • ACTION_UP——手指从屏幕上松开的一瞬间

手指触摸屏幕一般会出现如下情况:

  • 点击屏幕后松开,DOWN->UP(很可能松开过程中移动了一点点手指而被判定为下面的情况)
  • 点击屏幕滑动一会再松开,DOWN->MOVE->…>MOVE->UP

我们可以通过MotionEvent对象得到点击事件发生的x和y坐标
getX/Y返回的是相对于当前View左上角的x和y坐标
getRawX/Y返回的是相对于手机屏幕左上角的x和y坐标

2.TouchSlop

TouchSlop是一个最小距离,当手指滑动的距离小于这个值则系统判定为没有滑动(用来防止出现上面那种情况)
在不同的设备这个值可能是不同的
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取这个常量
在源码中还可以找到这个常量的定义

3.1.4 VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

速度追踪,用于追踪手指在滑动过程中的水平和竖直方向的速度
首先在View的ontouchEvent方法中追踪当前点击事件的速度

VelocityTracker vt=VelocityTracker.obtain();
vt.addMovement( event);

接着用下面方法获得速度

//用于计算速度
//1000ms表示在1s内手指从水平方向从左向右滑过100像素,那么水平速度就是100
vt. computeCurrentVelocity (1000);
int xVelocity=( int) vt. getXVelocity ();

速度=(终点-起点)/时间段

最后使用完毕后调用clear重置并回收内存

vt.clear ();
vt.recycle ();

2.GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为

//创建对象并实现OnGetureListenter接口
GestureDetector gd=new GetureDetectorthis;
//解决长按屏幕后无法拖动的现象
gd.setIsLongpressEnable(false);

接着,接管目标View的onTouchEvent 方法,在待监听View的onTouchEvent方法中添加如下实现

boolean consume = gd. onTouchEvent (event);
return consume;

表格
实际开发中其实也可以只使用View的onTouchEvent方法中实现所需的监听,作者推荐滑动时自己实现,双击时使用GetureDetector

3. Scroller

弹性滑动对象,用于实现View的弹性滑动(实现滑动过程)
View中使用的scrollTo/scrollBy进行滑动时是瞬间完成的
Scroller需要与View的computeScroll方法结合后可以实现滑动过程

Scroller scroller = new Scroller(mContext);
    private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - srollX;
        //1000ms内滑向destX,效果是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, 0, 1000);
    }

3.2 View的滑动

为了给用户呈现更多的内容,就需要使用滑动来隐藏和显示一些内容
以下3种为常见实现

  1. 通过View本身提供的scrollTo/scrollBy
  2. 通过动画给View施加平移效果来实现滑动
  3. 改变View的LayoutParams使得View重新布局从而实现滑动

3.2.1 使用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);
    }

从源码可以看出,scrollBy实际也是调用了scrollTo方法
关于mScrollX和mScrollY:
1.这两个属性是通过getScrollX/Y获得的
2.通过下面图片可知mScrollX/Y分别代表什么

在这里插入图片描述

3.2.2 使用动画

主要是操作View的translationX/Y

1.传统的View动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
//需要设置这个属性为true才会保留动画后的状态,否则会还原
    android:fillAfter="true"
    android:zAdjustment="normal" >

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100" />

</set>

这个操作并不能真正改变View的位置参数,包括宽/高

2.采用属性动画

//按顺序对应括号内的参数(目标view,水平移动,0位置,到100位置,100ms内移动)
ObjectAnimation.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();

在Android3.以下无法使用这个操作,需要通过nineoldandroid来实现

有一个问题,假设单纯使用传统的View动画滑动一个Button会导致新位置的Button无法点击,因为他只是移动了Button的影像
建议使用属性动画就可以解决这个问题

3.2.3 改变布局参数

即改变LayoutParams
一、比如将一个Button右移100px:将这个Button的L ayoutParams 里的marginLeft参数的值增加100px
二、在Button左边放一个空的View,然后设置这个View的宽度来挤动Button(意思这个改变参数布局的可以灵活使用)

例子:

MarginLayoutParams params = (MarginLayoutParams) mButton 1. getLayoutParams ();
params.width += 100;
params.leftMargin += 100;
//下面是应用这个改动
mButton 1. requestLayout ();
//  mButton 1. setLayoutParams (params);

3.2.4 各种滑动方式的对比

  • scrollTo/By:操作简单,适合对View内容的滑动(只能滑动View的内容不能滑动View本身)
  • 动画:适用于没有交互的View和实现复杂的动画效果(使用属性动画则没有明显缺点)
  • 改变参数布局:操作稍微复杂,适用于有交互的View

一个全屏滑动的例子

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
		//获取当前手指的坐标
		//注意不能使用getX/Y,因为这是全屏滑动,所以需要获取当前点击事件在屏幕中的坐标而不是相对于View本身的坐标
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            break;
        }
        case MotionEvent.ACTION_MOVE: {
			//获得位移,这样才可以移动View
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
            int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;
            int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
			//ViewHelper提供的动画效果
            ViewHelper.setTranslationX(this, translationX);
            ViewHelper.setTranslationY(this, translationY);
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

3.3 弹性滑动

为了优化用户的滑动体验,我们要实现弹性滑动(渐进的)
实现方法:

  1. 使用Scroller
  2. Handler#postDelayed
  3. Thread#sleep
    它们都有一个共同的思想:将一次大的滑动分成若干次小的滑动

3.3.1 使用Scroller

Scroller scroller = new Scroller(mContext);
    private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - srollX;
        //1000ms内滑向destX,效果是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, 0, 1000);
		//由下面代码可知startScroll只是单纯的保存了参数
		//invalidarte方法会导致View重绘,然后View中的draw方法(后面会提到)会调用computeScroll方法
		invalidarte();
    }
	
	 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;
    }
	
	//这个方法原本是空的,以下是为了实现弹性滑动而写的
	    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
			//向Scroller获取当前的scrollX/Y,通过scrollTo实现滑动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			//使用这个方法进行第二次重绘,再调用computeScroll,一直反复到滑动过程结束
            postInvalidate();
        }
    }

再看一下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;
        }

		//由这个关键词可知这个方法是根据时间流逝来计算当前scrollX/Y的值
        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;
		····
        }
		//返回这个表示滑动未结束
        return true;
    }

Scroller小结

  • Scroller本身并不实现View的滑动
  • Scroller配合View的conputeScroll方法才能完成弹性滑动的效果
  • conputeScroll不断让View重绘,每一次重绘计算时间间隔和滑动距离
  • 通过时间间隔就可以得出View当前的滑动位置
  • 知道了滑动位置就可以通过scrollTo方法来完成滑动
  • 然后每一次重绘都是一次小滑动,连续之后便是弹性滑动

3.3.2 通过动画

动画本身就是一种渐进的过程,用它实现具有天然的的弹性效果

//让一个view在100ms内向右滑动100像素
ObjectAnimatior.ofFloat(targetView,"translationX",0,100).setDuration(100).start;

同时我们还可以利用动画的特性来实现一些动画不能实现的效果
下面是模仿Scroller来实现View的弹性滑动

final int starX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimation.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimationUpdateListener({
	@Override
	public void onAnimationUpdate(ValueAnimation animator){
		//获取动画的每一帧和之前的Scroller类似
		float faction = animator.getAnimatedFraction();
		mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
	}
});
animator.start();

使用这种方法还可以实现其他动画效果,只需要在onAnimationUpdate方法中加上我们需要的操作

3.3.3 使用延时策略

核心思想是通过发送一系列延时消息而达到的渐进式效果
使用Handler或者View的postDelayed方法/线程的sleep方法

下面的代码是大约1000ms(因为线程调度不会很精确稳定)内将View的内容向左移动100像素

    private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;

    private Button mButton1;
    private View mButton2;

    private int mCount = 0;

    @SuppressLint("HandlerLeak")
    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);
                    mButton1.scrollTo(scrollX, 0);
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                }
                break;
            }

            default:
                break;
            }
        };
    };

上面几种弹性滑动的实现方法,侧重更多的是实现思想
实际使用可以灵活扩展

3.4 View的事件分发机制

重点难点,滑动冲突的解决基础就是事件分发机制

3.4.1 点击事件的传递规则

我们分析的主要对象就是MotionEvent
所谓点击事件的事件分发,
就是当一个MotionEvent产生以后
系统需要把这个事件传递给一个具体的View
这个过程就是分发过程

以下介绍三个分发过程中很重要的三个方法:

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前的View,那么这个方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前的事件

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法内部调用。用来判定是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列里,此方法不会被再次调用

public boolean onTouchEvent(MotionEvent event)

在dispatchToucchEvent方法中调用,用来处理点击事件,返回结果是是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接收到事件1

上述三个方法的关系可用下面的伪代码表示:

//对于一个根ViewGroup来说,点击事件产生后首先会传递给它
public boolean dispatchTouchEvent ( MotionEvent ev){
	boolean consume = false ;
	// Intercept 返回true就表示它要拦截当前事件,意思就是:这个我拿来用了!
	if( onInterceptTouchEvent ( ev ){
	//那么这个事件就会交给这个ViewGroup处理,调用onTouchEvent
		consume = onTouchEvent (ev );
	} else {
	//父不用才轮到儿子用不用
			consume = child. dispatchTouchEvent ( ev );
		}
		return consume ;
}

当一个View需要处理事件时
1、如果它设置了OnTouchListener,那么其中的onTouch方法会被回调,如果返回true就代表事件由这个监听处理
2、如果返回的是false当前View的onTouchEvent方法会被调用
3、如果在onTouchEvent中有设置OnClickListenter,那么它的onClick方法将会被调用
由此可见事件传递处理的优先级

点击事件的传递顺序

Activity-> Window ->顶级View->View分发事件
顶级View接收到事件后,就会按照事件分发机制去分发事件
如果一个View的onTouchEvent返回false,那么它的父容器2的onTouchEvent将会被调用
如果所有View都不处理,最终将由Activity的onTouchEvent处理

传递机制的小结

(1) 同一个事件序列是从手指接触屏幕的那一刻起(down)加上中间数量不定的(move),到手指离开屏幕(up)结束
(2) 正常来说,一个事件序列只能被一个View拦截且消耗。原因见(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内所有事件都会直接交给它处理。特殊情况:这个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
(3) 某个View一旦决定拦截,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent不会被调用,故这个事件不会被拦截到别的元素
(4) 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会交给它来处理,并且将事件重新交给它的父元素处理,即父元素的onTouchEvent会被调用。事件一旦交给一个View处理,那么它就必须被消耗否则就会永远失去这次机会
(5) 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,最终传递给Activity处理。其他正常运转
(6) ViewGroup默认不拦截任何事件
(7) 如果View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent会被直接调用
(8) View的enable属性不影响onTouchEvent返回默认值。
(9) 事件传递过程是由外向内的,即事件总是传递给父元素,再由父元素分发给子View

3.4.2 事件分发的源码解析

点击事件由MotionEvent来表示
点击操作发生时,事件最先传给当前的Activity
具体是由Activity中的Window来分发给DecorView(底层容器,setContentView所设置的父容器)
1.点击事件从Activity到Window到DecorView

//Activity中的dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
		//这个方法将事件传递给DecorView处理,DecorView以及其分发之后的View都不处理就返回false
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
		//如果所有的View都不处理,则最后交由Activity处理
        return onTouchEvent(ev);
    }
	

2.点击事件到达顶级View以及其分发过程
分发:
顶级View(一般是ViewGroup)调用dispatchTouchEvent

拦截处理:
如果ViewGroup的onInterceptTouchEvent为true,那么这个事件就由ViewGroup处理
如果ViewGroup的mOnTouchListener被设置,onTouch会被调用
否则onTouchEvent调用
在onTouchEvent中,如果设置了OnClickListener,则OnClick调用

分发
如果ViewGroup没有拦截,就传递给点击事件链上的子View,如此重复到完成

分发过程详解

其实主要实现是在ViewGroup的 dispatchTouchEvent 方法中
下面是当ViewGroup第一次接收到DOWN事件时

            // Check for interception.
            final boolean intercepted;
			//这个方法会在onInterceptTouchEvent前判断是否拦截
			//第一次DOWN进来时判断为true
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
					//标志位定为已经接收了DOWN
                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;
            }

接收完DOWN之后

            // Check for interception.
            final boolean intercepted;
			//这时候接收的是MOVE或UP
			//当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null,假如这个为true
			//假如为false的话就拦截,看其他判断便知
            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
					//直接跳到这里,不拦截MOVE和UP
                } 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;
            }

总结一下
1.onInterceptTouchEvent不是任何时候都被调用的,如果要提前处理所有点击事件,要选择 dispatchTouchEvent 方法
2. FLAG_DISALLOW_INTERCEPT 可以用来解决滑动冲突

分发到子View时

判断子元素是否能接收到点击事件
1.子元素是否在播放动画
2.点击事件坐标是否落在子元素区域内

如果遍历所有子元素后事件没有被处理这是因为
1.ViewGroup没有子元素
2.子元素处理了点击事件,但是 dispatchTouchEvent 返回了false(一般是因为子元素的onTouchEvent返回了false)

这样的话最后都由ViewGroup自己处理
如果mFirstTouchTarget==null,那么点击事件就会交由View处理

View对点击事件的处理

View点击事件和之前说的一样(Touch的优先级),其他要注意的有:

这个View是一个单独的元素,不包含ViewGroup和子元素,必须自己处理点击事件

只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就一定会消耗这个事件,尽管它是DISABLE状态

3.5 View的滑动冲突

只要在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。解决问题有一个固定的套路

3.5.1 常见的滑动冲突场景

  • 1、外部滑动方向与内部不一致
  • 2、内外滑动方向一致
  • 3、以上两种情况的嵌套

情况一

常见的是ViewPager和Fragment结合实现页面左右滑动效果
然而每个页面中又有一个ListView上下滑动,因为ViewPager内部处理了这种滑动冲突,所以不会出现问题
如果我们使用的事ScrollView而不是ViewPager,那就必须手动处理冲突,否则只有其中一层可以滑动

情况二

系统无法知道用户到底想让哪一层滑动,要么只有一层滑动,要么两个都滑动的很缓慢

情况三

几乎就只是单一的滑动冲突的叠加,因此只需要分别处理内层、中层和外层之间的滑动冲突即可

3.5.2 滑动冲突的处理规则

情况一

处理规则:用户左右滑动时让外部View拦截,上下滑动时就让内部View拦截点击事件
我们可以根据特征来判断

  1. 根据滑动过程中两个点的坐标判断是竖直还是水平滑动
  2. 判断滑动路径与水平方向所形成的夹角
  3. 根据水平方向和竖直方向的距离差
  4. 特殊情况还可以根据水平方向和竖直方向的速度差来判断

情况二

无法根据滑动的角度,距离差和速度来判断
这时候通过规定业务状态来区分:

情况三

也是从业务方面去判断

3.5.3 滑动冲突的解决方式

问题关键:滑动规则不同
解决基础:事件分发机制

1.外部拦截法

点击事件先经过父容器的拦截处理,需要重写父容器的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,不然之后的事件都给它接收了
            intercepted = false;
            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,否则子元素的onClick可能无法触发
			//同时父容器无论如何最后都会处理这个事件
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

2.内部拦截法

父容器不拦截任何事件,所有都交给了子元素
子元素需要就直接消耗,否则还给父容器处理
不符合事件分发机制,需要配合requestDisallowInterceptTouchEvent

    @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,不然之后的事件都给它接收了
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
			//父容器需要的点击事件
			//面对不同的滑动策略只需要修改这个条件,其他的不能改动
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
				//调用这个方法解除父元素拦截所需事件的限制
				//这个方法设置标志位FLAG_DISALLOW_INTERCEPT使父容器不再拦截事件,前提是父容器不拦截DOWN
                parent.requestDisallowInterceptTouchEvent(false);
            } 
            break;
        }
        case MotionEvent.ACTION_UP: {
			//父容器的这里必须为false,否则子元素的onClick可能无法触发
			//同时父容器无论如何最后都会处理这个事件
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;


        return super.dispatchTouchEvent(event);
    }

父元素代码

    public boolean onInterceptTouchEvent(MotionEvent event) {
		int action = event.getAction();
		//除了DOWN不拦截,其他都一定要拦截
		if(action == MotionEvent.ACTION_DOWN){
			return false;	
		} else{
		 return true;
		}
	}

情况二解决方案

3


  1. ↩︎

  2. 为什么是父容器 ↩︎

  3. 看不太懂 ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值