View系列:事件分发(二)
滑动冲突
常见场景:
- 内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
- 内外层滑动方向一致(如:RecyclerView嵌套)
一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截
父View
事件发送方,父View拦截。
父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。
- DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
- UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
- 在MOVE中根据逻辑需求判断是否拦截
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
        }
        return intercepted;
    }
子View
事件接收方,内部拦截
事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。
注意:申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。
    public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
        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;
            }
        }
        return super.dispatchTouchEvent(event);
    }
:cry:多点触控
多点触控相关的事件:
| 事件 | 简介 | 
|---|---|
| ACTION_DOWN | 第一个 手指 初次接触到屏幕 时触发。 | 
| ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。 | 
| ACTION_UP | 最后一个 手指 离开屏幕时触发。 | 
| ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已经有手指在屏幕上)。 | 
| ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。 | 
| 以下事件类型不推荐使用 | ---以下事件在2.0开始,在 2.2 版本以上被废弃--- | 
| 第 2 个手指按下,已废弃,不推荐使用。 | |
| 第 3 个手指按下,已废弃,不推荐使用。 | |
| 第 4 个手指按下,已废弃,不推荐使用。 | |
| 第 2 个手指抬起,已废弃,不推荐使用。 | |
| 第 3 个手指抬起,已废弃,不推荐使用。 | |
| 第 4 个手指抬起,已废弃,不推荐使用。 | 
多点触控相关的方法:
| 方法 | 简介 | 
|---|---|
| getActionMasked() | 与 getAction()类似,多点触控需要使用这个方法获取事件类型。 | 
| getActionIndex() | 获取该事件是哪个指针(手指)产生的。 | 
| getPointerCount() | 获取在屏幕上手指的个数。 | 
| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 | 
| findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 | 
| getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 | 
| getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 | 
index和pointId
在 2.2 版本以上,我们可以通过getActionIndex() 轻松获取到事件的索引(Index),Index 变化有以下几个特点:
1、从 0 开始,自动增长。 2、之前落下的手指抬起,后面手指的 Index 会随之减小。 (0、1、2 --> 第2个手指抬起 --> 第三个手指变为1 --> 0、1) 3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。 4、对 move 事件无效。 **getActionIndex()**获取到的始终是数值 0
| 相同点 | 不同点 | 
|---|---|
| 1. 从 0 开始,自动增长。 2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。 | Index 会变化,pointId 始终不变。 | 
pointerIndex 与 pointerId
pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。
| 类型 | 简介 | 
|---|---|
| pointerIndex | 用于获取具体事件,可能会随着其他手指的抬起和落下而变化 | 
| pointerId | 用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变 | 
这两个数值使用以下两个方法相互转换:
| 方法 | 简介 | 
|---|---|
| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 | 
| findPointerIndex(int pointerId) | 通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。 | 
自定义View示例

/**
 * Created by Varmin
 * on 2017/7/5  16:16.
 * 文件描述:left,content,right三个tag,在布局中给每个部分设置该tag。用于该ViewGroup内部给子View排序。
 * 功能:默认全部关闭左右滑动。分别设置打开
 */
public class SlideView extends ViewGroup implements View.OnClickListener, View.OnLongClickListener {
    private static final String TAG = "SlideView";
    public final String LEFT = "left";
    public final String CONTENT = "content";
    public final String RIGHT = "right";
    private Scroller mScroller;
    /**
     * scroller滑动时间。默认250ms
     */
    public static final int DEFAULT_TIMEOUT = 250;
    public static final int SLOW_TIMEOUT = 500;
    /**
     * 左右View的宽度
     */
    private int leftWidth;
    private int rightWidth;
    private GestureDetector mGesture;
    private ViewConfiguration mViewConfig;
    public SlideView(Context context) {
        super(context);
        init(context);
    }
    public SlideView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        mScroller = new Scroller(context);
        //都是自己处理的,这里没有用到该手势方法
        //缺点:误差有些大。这种精确滑动的,最好自己判断
        mGesture = new GestureDetector(context, new SlideGestureDetector());
        mViewConfig = ViewConfiguration.get(context);
        //默认false
        setClickable(true);
    }
    /**
     * 所有的子View都映射完xml,该方法最早能获取到childCount
     * 在onMeasuer/onLayout中获取,注册监听的话,会多次被调用
     * 在构造方法中,不能获取到childCount。
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        initListener();
    }
    private void initListener() {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            childView.setClickable(true);
            childView.setOnClickListener(this);
            if (CONTENT.equals(childView.getTag())) {
                childView.setOnLongClickListener(this);
            }
        }
    }
    @Override
    public void onClick(View v) {
        String tag = (String) v.getTag();
        switch (tag) {
            case LEFT:
                Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
                break;
            case CONTENT:
                Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
                closeAll(SLOW_TIMEOUT);
                break;
            case RIGHT:
                Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
                break;
        }
    }
    @Override
    public boolean onLongClick(View v) {
        Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
        return true;
    }
    /**
     * 每个View的大小都是由父容器给自己传递mode来确定。
     * 每个View的位置都是由父容器给自己设定好自己在容器中的左上右下来确定位置。
     * 所以,继承至ViewGroup的容器,要在自己内部实现对子View大小和位置的确定。
     */
    /**
     * 子View不会自己测量自己的,所以在这里测量各个子View大小
     * 另外,处理自己是wrap的情况,给自己一个确定的值。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //测量子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //测量自己
        //默认是给该ViewGroup设置固定宽高,假设不纯在wrap情况,onlayout中也不考虑此情况
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            String tag = (String) childView.getTag();
            switch (tag) {
                case LEFT:
                    leftWidth = childWidth;
                    childView.layout(-childWidth, 0, 0, childHeight);
                    break;
                case CONTENT:
                    childView.layout(0, 0, childWidth, childHeight);
                    break;
                case RIGHT:
                    rightWidth = childWidth;
                    childView.layout(getMeasuredWidth(), 0, 
                                     getMeasuredWidth() + childWidth, childHeight);
                    break;
            }
        }
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean handled = super.onInterceptTouchEvent(ev);
        if (handled) {
            return true;
        }
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitX = (int) ev.getX();
                mInitY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = (int) (ev.getX() - mInitX);
                int offsetY = (int) (ev.getY() - mInitY);
                /**
                 * 判断可以横向滑动了
                 * 1,拦截自己的子View接收事件
                 * 2,申请父ViewGroup不要看拦截事件。
                 */
                if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
                    requestDisallowInterceptTouchEvent(true);
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                //重置回ViewGroup默认的拦截状态
                requestDisallowInterceptTouchEvent(false);
                break;
        }
        return handled;
    }
    private int mInitX;
    private int mOffsetX;
    private int mInitY;
    private int mOffsetY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = false;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                mOffsetX = (int) (event.getX() - mInitX);
                mOffsetY = (int) (event.getY() - mInitY);
                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {//横向触发条件
                    //预估,偏移offsetX后的大小
                    int mScrollX = getScrollX() + (-mOffsetX);
                    if (mScrollX <= 0) {//向右滑动,显示leftView:110
                        //上面的是预估,如果预估大于目标:你不能return放弃了,要调整mOffsetX的值使其刚好等于目标
                        if (Math.abs(mScrollX) > leftWidth) {
                            mOffsetX = leftWidth - Math.abs(getScrollX());
                            //return true;
                        }
                    }else {//向左滑动,显示rightView:135
                        if (mScrollX > rightWidth) {
                            mOffsetX = getScrollX() - rightWidth;
                            //return true;
                        }
                    }
                    this.scrollBy(-mOffsetX,0);
                    mInitX = (int) event.getX();
                    mInitY = (int) event.getY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                int upScrollX = getScrollX();
                if (upScrollX > 0) {//向左滑动,显示rightView
                    if (upScrollX >= (rightWidth/2)) {
                        mOffsetX = upScrollX - rightWidth;
                    }else {
                        mOffsetX = upScrollX;
                    }
                }else {//向右,显示leftView
                    if (Math.abs(upScrollX) >= (leftWidth/2)) {
                        mOffsetX = leftWidth - Math.abs(upScrollX);
                    }else {
                        mOffsetX = upScrollX;
                    }
                }
               // this.scrollBy(-mOffsetX,0);//太快
               // startScroll(-mOffsetX, 0, 1000);//直接放进去,不行?
                /**
                 * 注意startX。dx表示的是距离,不是目标位置
                 */
                mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
                invalidate();
                break;
        }
        if (!handled) {
            handled = super.onTouchEvent(event);
        }
        return handled;
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
    /**
     * 虽然传入的dx、dy并不是scrollTo实际要到的点,dx,dy只是一小段距离。
     * 但是computeScroll()我们scrollTo的是:现在位置+dx的距离 = 目标位置
     *
     * @param dx //TODO *距离!距离!并不是说要到达的目标。*
     * @param dy
     * @param duration 默认的滑动时间是250,复位的时候如果感觉太快可以自己设置事件.
     *
     */
    private void startScroll(int dx, int dy, int duration) {
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
        //mScroller.extendDuration(duration); 在250ms基础上增加。构造函数传入的话,就是duration的时间。
        invalidate();
    }
    /**
     * 是否打开,ListView中复用关闭
     * @return
     */
    public boolean isOpened(){
        return getScrollX() != 0;
    }
    public void closeAll(int duration){
        mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration);
        invalidate();
    }
}
Tips
scrollTo/By
通过三种方式可以实现View的滑动:
- 通过View本身提供的scrollTo/scrollBy方法; 
- 通过动画使Veiw平移。 
- 通过改变View的LayoutParams属性值。 
**setScrollX/Y、scrollTo: **移动到x,y的位置
**scrollBy: **移动x,y像素的距离
    public void setScrollX(int value) {
        scrollTo(value, mScrollY);
    }
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + 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);
        }
    }
**注意:**假如scrollTo(30,10),按照View右下正,左上负的概念,因该是向右滑动30,向下滑动10。
作者:Varmin_
链接:https://juejin.cn/post/6972431645429202980
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 
			
