Android开发艺术探索读书笔记: View的事件体系

View基础知识

                          x
 ------------------------------->
 |       ^            ^
 |   top |            |
 |       |            |
 |       v            | 
 |<----->+---------+  |  bottom
 |  left |         |  | 
 |       |   View  |  |
 |       |         |  |
 |       +---------+  v
 |<---------------->
y|      right            ViewGroup
 v    

这些位置参数都是相对于View所在的容器ViewGroup,并且有以下关系:
width = right - left;
height = bottom - top;

View的滑动

弹性滑动

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.

下面是一个例子并加上了一些注释,更详细的用法可以参考这篇博客


public class DragViewGroup extends ViewGroup {

    private static final String TAG = "DragViewGroup";

    private float density;

    private ViewDragHelper viewDragHelper;


    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        //用于处理被触摸的View是否能够被拖动,ACTION_DOWN被调用
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            Log.e(TAG,String.format("child id = %s,pointerId = %s",child.getId(),pointerId));
            return true;
        }

        //相对于Parent Y方向拖动位置
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            Log.e(TAG, String.format("child id = %s,top = %s,dy = %s", child.getId(), top, dy));
            return top;
        }

        //相对于Parent X方向拖动位置
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.e(TAG,String.format("child id = %s,left = %s,dx = %s",child.getId(),left,dx));
            return left;
        }

        //当child view能够消耗事件的情况下(比如clickable),需要根据情况重写下面两个方法
        //并且返回值大于零才能处理拖动
        @Override
        public int getViewVerticalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }


        @Override
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }

        //拖动的View释放
        //The fling velocity is also supplied:
        //xvel - X velocity of the pointer as it left the screen in pixels per second.
        //yvel - Y velocity of the pointer as it left the screen in pixels per second.
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            Log.e(TAG,String.format("releasedChild id = %s,xvel = %s,yvel = %s",releasedChild.getId(),xvel,yvel));

            //用于平滑移动View,需要重写View#computeScroll,因为是用Scroller实现的
            viewDragHelper.settleCapturedViewAt(releasedChild.getLeft() + 100, releasedChild.getTop() + 100);
            invalidate();
        }

//        //在边界拖动时回调
        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            if(edgeView != null) {
                viewDragHelper.captureChildView(edgeView, pointerId);
            }
        }
    };

    @Override
    public void computeScroll() {
        if(viewDragHelper.continueSettling(true)) {
            postInvalidate();
        }
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        density = context.getResources().getDisplayMetrics().density;
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        viewDragHelper = ViewDragHelper.create(this,1.0f,callback);
        viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private View edgeView;

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        edgeView = getChildAt(0);
        final int childCount = getChildCount();
        for(int i = 0; i < childCount; i ++) {
            View childView = getChildAt(i);
            childView.layout(0, (int) (40 * i * density),childView.getMeasuredWidth(),(int) (40 * (i + 1) * density));
        }
    }
}

View的事件分发机制

这三个方法的关系:
dispatchTouchEvent(MotionEvent ev)
onInterceptTouchEvent(MotionEvent event)
onTouchEvent(MotionEvent event)
作者用的伪代码描述:(简直是淋漓尽致,言简意赅!!!)

public void dispatchTouchEvent(MotionEvent ev) {
    boolean isConsume = false;
    if(onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    }else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

1.事件从Activity的dispatchTouchEvent开始传递 -> Window -> ViewGroup -> View。

2.给View设置 onTouchListener 其执行优先级比 onTouchEvent 要高,如果事件被 onTouchListener#onTouch 消耗,那么 onTouchEvent 将不会被调用。给View设置 onClickListener 其执行优先级比 onTouchEvent 要高。

3.如果一个View的onTouchEvent没有被消耗掉,会传回父View的onTouchEvent,以此类推,如果都没有那么最终会出现在Activity#onTouchEvent。

4.当一个View没有处理MotionEvent.ACTION_DOWN时,后续的事件将不会到来(因为处理也没有意义),如果处理了之后后续的事件被拦截,那么将会收到ACTION_CANCEL,所以我们应当根据情况处理ACTION_CANCEL这个事件。

5.如果View不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,父元素的 onTouchEvent 也不会被调用最终消失的点击事件会传递给Activity处理。

6.ViewGroup 默认不拦截任何事件即 onInterceptTouchEvent 默认返回false。

7.View没有onInterceptTouchEvent方法,一旦事件传递给它,那么它的onTouchEvent将会被调用。

8.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。

9.View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable的。

另外一份比较好的总结

  规则较多,个人认为写自定义控件的时候尽量不要去利用dispatchTouchEvent来实现,容易出错,这主要用来分发事件给到子View,或者在自己的onTouchEvent里面消耗,或者事件传递回给父View。当我们自己实现ViewGroup重点实现onInterceptTouchEvent和onTouchEvent,根据逻辑是否拦截,实现View的时候就只实现onTouchEvent实现我们需要的效果。

View的滑动冲突

滑动冲突解决方式

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    switch(event.getAction()) {
        /* 一旦拦截,后续的事件就不会传递给子View了
         * ACTION_DOWN不受FLAG_DISALLOW_INTERCET控制,父容器一旦拦截,
         * 子View调用requestDisallowInterceptTouchEvent将无法实现内部
         * 拦截
         *
         *这里还需要注意,如果MotionEvent.ACTION_DOWN没有拦截,被子View给消耗了
         *onTouchEvent就收不到,所以根据需要也不一定要返回false
         */
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            if(需要拦截当前的点击事件) {//子元素将收到ACTION_CANCEL
                intercepted = true
            }else {
                intercepted = false;
            }
            break;
        }

        case MotionEvent.ACTION_UP: {
            intercepted = false;
        }

        default:
            break;
    }
    return intercepted;
}
//重写子View的dispatchOnTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            //True if the child does not want the parent to intercept touch events
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE:{
            if(父容器需要此类点击事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        default:
            break;

    }
    return super.dispatchTouchEvent(event);
}

//好需要结合父View的onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    }else {
        return true;
    }
}