Android进阶学习(三)View的事件体系

文本是阅读《Android开发艺术探索》的学习笔记记录,详细内容可以自行阅读书本。

View的事件体系

1 View基础知识

1.1 什么是View

  View是Android中所有控件的基类。不管是简单的Button和TextView还是复杂的RelativeLayout和ListView,它们的共同基类都是View。还有ViewGroup也继承了View,它内部可以包含多个View。

1.2 View的位置参数

  View的位置主要由它四个顶点确定,分别对应View的四个属性:top、left、right、bottom。在Android中,它的坐标系与我们所看到的有区别,如图所示。

在这里插入图片描述

1.3 MotionEvent和TouchSlop

  1)MotionEvent

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

ACTION_DOWN ——手指刚接触屏幕
ACTION_MOVE ——手指在屏幕上移动
ACTION_UP ——手指从屏幕上松开

  2)TouchSlop
  TouchSlop是系统所能够识别出的被认为是滑动的最小距离。可以通过ViewConfiguration.get(this).getScaledTouchSlop()获取。

1.4 Scroller

  弹性滑动对象,用于实现View的弹性滑动。当使用View的scrollTo/scrollBy时,其过程是瞬间完成的。这个时候就可以用Scroller来实现过渡滑动效果。它需要和View的computeScroll方法配合使用,如下:

scroller = new Scroller(context);

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

2 View的滑动

  通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy方法;第二种是通过动画实现滑动;第三种是通过修改View的LayoutParams使得View重新布局从而实现滑动。

2.1使用scrollTo/scrollBy

  先来看看两个方法的源码

    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);
    }

  首先scrollBy方法也是调用了scrollTo方法,scrollBy方法是相对当前位置移动,而scrollTo是指定位置的绝对移动。通过先判断传进来的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就调用onScrollChanged()方法来通知界面发生改变,然后重绘界面。
  举个例子,调用两次scrollTo(-10, 0),View第一次会向左滑动10,第二次不变化。调用两次scrollBy(-10, 0),View第一次向左滑动10,第二次再向左滑动10。
  我们要先理解View 里面的两个成员变量mScrollX, mScrollY,X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,相对的不是屏幕的原点,而是View的左边缘和上边缘。

  变化规律如图所示:
在这里插入图片描述

2.2 使用动画

  上一篇介绍过了,直接使用属性动画。

        ObjectAnimator.ofFloat(stopWebBt, "translationY", -startWebBt.getHeight()).start();

2.3 改变布局参数

  修改LayoutParams参数

ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams)
                        button.getLayoutParams();
                marginLayoutParams.leftMargin += 100;
                button.requestLayout();

3 弹性滑动

  弹性滑动的方式很多,比如通过Scroller、动画、延时策略等等

3.1 使用Scroller

	//弹性滑动到指定位置
    public void smoothScrollto(int destX, int destY) {
        int x = getScrollX();
        int deltaX = destX - x;
        //1000ms内
        scroller.startScroll(x, 0, deltaX, 0, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

  下面探索一下Scroller的内部工作原理,我们调用Scroller的startScroll方法,其实Scroller内部什么也没有做,这是保存了传递进去的参数,源码如下。

    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;
    }

  真正推动弹性滑动的是我们的invalidate()方法,它导致View重绘,View重绘时又调用我们实现的computeScroll()方法,其中又调用postInvalidate()方法进行二次重绘,直到滑动结束。源码如下:

    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        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;
            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;
    }

  该方法主要两个作用,一个是判断滑动是否结束,一个是如果未结束,根据时间流失的百分比来给CurrX和CurrY赋值,帮助滑动。

3.2 通过动画

  动画上篇结束了,就不多说了。动画实现弹性滑动非常轻松,也提供了许多丰富的api。

3.3 延时策略

  它的核心思想结束通过发送延时消息来达到渐进式的效果,可以使用Handler的postDelayed方法;线程的sleep方法等等。

4 View的事件分发机制

4.1 点击事件的传递

  点击事件的分发由三个很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。
  dispatchTouchEvent,进行事件分发,如果事件传递给当前View,此方法一定被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent影响,表示是否消耗当前事件。
  onInterceptTouchEvent,内部调用,用来拦截某个事件,如果当前View拦截了事件,那么同一个事件序列中,此方法不再调用,返回结果表示是否拦截当前事件。
  onTouchEvent,在dispatchTouchEvent方法中调用,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
  当一个点击事件产生后,它的传递顺序:Activity -> Window -> View。

4.2 事件分发的源码分析

  1)Activity对点击事件的分发过程

  当一个点击操作发生时,事件最先传递给当前Activity,由activity的dispatchTouchEvent进行事件分发,具体工作由activity内部的Window来完成。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。

  先从activity的dispatchTouchEvent开始分析

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //Window进行分发
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //未有view消耗事件,则调用activity的onTouchEvent
        return onTouchEvent(ev);
    }

  首先事件交给activity所附属的Window进行分发,如果返回true,整个事件结束。否则调用activity的onTouchEvent。

  2)Window点击事件的分发过程

  由于Window唯一的实现是PhoneWindow,从源码上看:

    @Override
    public boolean superDispatchGenericMotionEvent(MotionEvent event) {
        return mDecor.superDispatchGenericMotionEvent(event);
    }

  PhoneWindow将事件直接传递给了mDecor ,而mDecor = (DecorView) preservedWindow.getDecorView();也就是事件一定会传递到view。

  3)顶层View点击事件的分发过程

  顶层ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由顶层ViewGroup处理,这时如果顶层ViewGroup的OnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。如果顶层ViewGroup不拦截事件,则事件会传递给它所在的点击事件链的子View。
  先看ViewGroup的拦截源码

           // Check for interception.
            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); // 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;
            }

  代码中可以看出,ViewGroup两种情况下会判断是否拦截事件:ACTION_DOWN和mFirstTouchTarget。从后面代码可知子View如果成功处理事件,会将mFirstTouchTarget赋值。即当ViewGroup不拦截事件交给子View处理时,mFirstTouchTarget != null。反之ViewGroup自己处理了该事件后,mFirstTouchTarget == null,onInterceptTouchEvent不再被调用,其余点击事件由ViewGroup自己处理。

  特殊情况,FLAG_DISALLOW_INTERCEPT标记位,这个是通过子View调用设置。原来控制ViewGroup不拦截除了ACTION_DOWN以外的其他点击事件。因为当ACTION_DOWN事件到来时,ViewGroup会重置FLAG_DISALLOW_INTERCEPT标记位。源码如下:

            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.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

  从上面源码分析,得知当ViewGroup决定拦截事件后,后续点击事件不再调用onInterceptTouchEvent,直接交给它处理。

  再看ViewGroup不拦截事件时,事件会向下分发给它的子View处理。

                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;

  从上段代码可以看出,遍历ViewGroup的子View,判断子View是否能够接收到点击事件。两点判断:是否再播放动画和点击事件坐标是否落在子View区域内。满足则调用dispatchTransformedTouchEvent方法,部分源码如下:

            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

  该方法内如果传入参数child 不为空,则调用子View的dispatchTouchEvent方法。该方法返回true,则跳出for循环,并在addTouchTarget方法内对mFirstTouchTarget赋值。

  4)View对点击事件的处理过程
  View是单独元素,没有子View进行传递,部分dispatchTouchEvent源码:

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

  首先,判断是否设置mOnTouchListener,有则返回true,并且不调用onTouchEvent。

5 View的滑动冲突

5.1 常见滑动冲突场景

  场景一:外部滑动和内部滑动方向不一致

  场景二:外部滑动和内部滑动方向一致

  场景三:上面两种情况的嵌套

在这里插入图片描述

5.2 滑动冲突的处理规则

  场景一:根据滑动是水平还是竖直来判断谁来拦截事件

  场景二:需要根据业务来判断,由谁来拦截事件

  场景一:需要根据业务来判断,由谁来拦截事件

5.3 滑动冲突的解决方法

  1.外部拦截法
  是指经过父类容器的拦截处理,父类容器需要则拦截此事件,反之传递给子View处理。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            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;
        }

        return intercepted;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值