View事件分发篇学习笔记

由于本片是View学习篇,那么也就是说,从认识View开始,一直学习下去,这将会是一个漫长的过程,很多Android开发工程师同学们大多都是只知道会用和几个简单的回调,甚至只是有一个概念知道这个View 是个视图,View是所有展示在界面上的控件的父类,那么今天我们就来开始重新认识一下这个View,正巧我现在工作任务不是特别的繁重,我也想重新认识一下它。

正如上面所说View 是所有View 的父类,比如Button,TextView ,EditText(继承于TextVView 都继承与ViewGroup 这是一种符合型View,而ViewGroup 的父类是View ,ViewGroup也是一个容器,里面可以包含很多子View,这里面的子View 也可以是很多ViewGroup。

现在我们大概的了解了一下View 的关系,接下来我们来详细的了解一下View 在屏幕上的位置呈现。 一个View 有四个顶点来决定,分别对应View的四个属性。Top(上),left(左),right(右),bottom(下)。一个View 默认位置根据他的父View 的左上角顶点来决定的。也就是说是根据父View左侧对其的。 left 是子View左侧边框距离父View 的左侧边框距离 ,right 是子View右侧边框距离父View 左侧边框的距离,top 是子View上边框距离父View 上边框的距离,bottom 是子View 下边框距离 父View上边框的距离。这下我们就非常清晰了,这几个属性的关系。所以一个View 的宽高关系就是width(宽) = right - left  ,height(高) = bottom - top。接下来我们来看一下View里面的详细方法是如何获得这几个相关参数的。

在View的源码中分别是mLeft,mRight,mBottom,mTop,几个参数,他们的获取方法是非常简单,就是getLeft(),getRight(),getBottom(),getTop(),这里需要注意,这几个参数都是相对父View 的,不是相对屏幕的哦!在Android 3.0 之后有几个属性分别是x、y、translationX和translationY,其中x、y针对的是View 左上角相对父View的坐标(这里我要说一下有什么用,可能有很多Android新同学看不懂X,y的作用,这两个参数的作用就是不改变View的大小只改变View的坐标,而且设置left right top bottom 会改变View的大小哦),translationX和translationY也是针对于父View的,表示的是View相对于父View的偏移量(什么是偏移量呢???就是相对于初始位置的偏移值)。x=left+translationX       y=top+translationY。  在平移的时候原有left  right bottom top 值不会改变,因为是平移!所以改变的是x、y、translationX和translationY值。如果改变了left  right bottom top 值那么View 的大小也会改变,所以是不可取的哦。接下来我们看一下View 的触摸屏幕事件MotionEvent与TouchSlop。

我们首先来接触一下MotionEvent (首先他是个类!我们嵌入不分析他的源码,毕竟主要目标不是他),我们看看常用的几个标志常量,分别代表的是几个手势,ACTION_DOWN ==  手机刚触摸屏幕触发的事件,ACTION_MOVE == 手机在屏幕上移动触发的事件(持续触发),ACTION_UP == 手机从屏幕松开的一瞬间触发的事件。顺序分别是ACTION_DOWN  --- ACTION_MOVE........--- ACTION_UP,我们还可以通过这个MotionEvent类获得按下屏幕位置的坐标x, y值。获得这个坐标值有两组方法,getX/getY和getRawX/getRawY 这两组方法我来说 一下是做什么用的,getX/getY针对的是这个View 的左上角坐标,这个View可以是个布局ViewGroup类型,也可以是一个大型的View。而getRawX/getRawY就相对简单多了,这个就是实际上我们手指在屏幕上相对于坐上角的X,Y 轴位置

接下来我们认识一下TouchSlop,TouchSlop 是一个参数,是int 整形常量,作用是系统识别的最小滑动距离!为什么有这个值呢?如果手指在屏幕上滑动的起点和终点之间的距离小于TouchSlop 这个数,那么系统就不会认为你在滑动。而且在不同的设备上, 这个值还有可能不同哦。ViewConfiguration.get(getContext()).getScaledTouslop()是获取这个TouchSlop参数的方法。

我们知道View与View之间嵌套是会产生滑动冲突的,那么我们接下来看的这个知识就是解决滑动冲突中需要用到的东西   速度追踪器VelocityTracker。

VelocityTracker这个类,我们不做详细的解读,只讲使用方法,他是用来追踪手指在滑动过程中的速度,他对我们的课题并不是特别重要也不在课题范围内。使用方法就是在View的onTouchEvent方法中追踪当前单击事件的速度。调用方法是这样:

 

@Override
public boolean onTouchEvent(MotionEvent event) {
        // 创建VelocityTracker
    VelocityTracker velocityTracker = VelocityTracker.obtain();
        //  添加需要追踪速度的动作类
    velocityTracker.addMovement(event);
        //  设置追踪的时间
    velocityTracker.computeCurrentVelocity(1000);
        //  获取追踪时间内的滑动距离   注意每次获取这个滑动距离之前都要设置一下时间单位
    float xVelocity = velocityTracker.getXVelocity();
        //  获取追踪时间内的滑动距离   注意每次获取这个滑动距离之前都要设置一下时间单位
    float yVelocity = velocityTracker.getYVelocity();
        //  在不使用的时候我们需要回收资源
   /* velocityTracker.clear();
    velocityTracker.recycle();*/
    return super.onTouchEvent(event);
}

这里需要注明一下,这个距离单位是像素px ,不是dp,时间参数是毫秒,而且这个得到的值可以是负数从左往右滑动得到的值是正数,从右向左滑动得到的数值是负数。 时间公式:速度 = (起点  -  终点 )/ 时间段

接下来我们将会介绍另一个类GestureDetector 从字面意思上就很容易理解 手势检测器,用于辅助检测用户的各种手势行为,比如单击,双击,长按,滑动等行为。接下来我们了解一下这个类的使用:

首先我们先创建这个类的实例,然后接管一下View 的MotionEvent 。

private GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
    // 手指轻轻触摸屏幕的一瞬间触发的方法,由一个ACTION_DOWN触发 就是MotionEvent.ACTION_DOWN
    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }
    // 手指轻轻触摸屏幕,没有松开,也没有移动时候调用的方法,由一个ACTION_DOWN触发
    @Override
    public void onShowPress(MotionEvent e) {

    }
    // 手指轻轻触摸屏幕后松开,由ACTION_UP触发,也就是MotionEvent.ACTION_UP,表示单击行为。
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }
    // 手指按下屏幕并且拖动,由一个ACTION_DOWN 和多个ACTION_MOVE触发,表示拖动行为
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }
    // 用户手指长按屏幕不放,表示长按行为
    @Override
    public void onLongPress(MotionEvent e) {

    }
    // 用户按下屏幕,快速滑动后松开,由一个ACTION_DOWN 和多个ACTION_MOVE 以及一个ACTION_UP触发,表示快速滑动行为
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }
});
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 接管MotionEvent
    boolean consume = gestureDetector.onTouchEvent(event);
    
    return consume;
}

顺带介绍一下OnDoubleTapListener 的回调!

// 由两次双单击组成的双击行为
@Override
public boolean onDoubleTap(MotionEvent e) {
    return super.onDoubleTap(e);
}

// 表示双击行为 不能和onSingleTapConfirmed 共存
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
    return super.onDoubleTapEvent(e);
}

// 严格的单击行为 表示触发了一次单击 后面不可能再触发一次单击行为,例如双击中的单击!
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
    return super.onSingleTapConfirmed(e);
}

接下来介绍Scroller这个类,字面意思大家都可以看懂这个是管理弹性滑动的类,他叫做弹性滑动对象我们来看一下这么类是怎么用的然后再来说一下原理这个很有意思哦:

首先我们需要自己定义一个类继承我们想要用到的控件。然后在内部重写相应方法,创建公共方法进行调用。开始上代码:

@SuppressLint("AppCompatCustomView")
public class TestTextView extends TextView {

    private final Scroller scroller;

    public TestTextView(Context context) {
        super(context);
        scroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }
    public void smoothScrollTo(int destX,int destY) {
// 获取相对于父View 的偏移量 
        int scrollX = getScrollX();
// 需要滑动到的位置坐标减去已经滑动的偏移量就等于距离
        int delta = destX -scrollX;
        // 1000ms内滑向destX,效果就是慢慢滑动  四个参数分别是,X的起点,Y的起点,X的距离,Y的距离,时间毫秒~
        scroller.startScroll(scrollX,0,delta,0,1000);
        invalidate();
    }
}

很简单,在构造方法中创建一个Scroller类,然后我们自定义一个方法smoothScrollTo ,传入了需要移动到的X,y坐标。内部进行计算。这里我们需要注意一下 scroller.startScroll(scrollX,0,delta,0,1000);其实这个并不是开始滑动的意思,只是把这些参数存入Scroller 的全局变量中取去了。Scroller不会对View做出任何改变,也不会持有View其实,他就是一个插值器不停的计算滑动距离并且在自己内部保存!通过调用invalidate();重新绘制View并且在computeScroll()回调中进行计算和真正的滑动!

在上面我们使用了scrollTo的方法,但是还有一个熟知的方法scrollBy 他的内部调用的也是scrollTo。我们来看看这两个方法分别都做了什么,有什么区别首先我们看一下scrollTo方法:

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

再来看一下scrollBy:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

 这个scrollBy,一眼我们其实基本上就看懂了,就相对上一个位置再移动X距离和Y距离,而且scrollTo 就是 直接从上一个位置 移动到我们传递的参数的那个位置,明白了吗?这个值都是PX 像素,而且改变的是View 的内容位置,不是View本身的位置哦。如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。

 上面我实现了滑动View 内容的滑动,运用到了ScrollTo方法,可是还有一种更简单的方法,就是大家都熟知的动画,通过动画我们可以使View 平移,旋转…………实现很多功能,在动画中有只更改视觉效果不更改位置的View动画 帧动画,有多个图片按照顺序播放的View动画 补间动画,也有我们用的最多的属性动画更改动画的同时View的位置也进行更改。这里需要提示一下View动画是不会改变View原有的位置的他就是一个动画效果,属性动画是可以改变位置的。当然Android 3.0 之前是没有属性动画的。那么应该怎么适配就是仁者见仁智者见智的事情了,可以在最后的位置创建一个隐藏的View,动画结束的时候隐藏动画的View,然后显示事先创建的View。或者通过一个空的View进行挤压平移都可以。这里就需要我们设置View的布局参数了,如何改变View的布局参数呢,我们继续往下看。

改变布局参数就是改变LayoutParams(这个类的名字就是布局参数(复数)的意思。)我来简单说一下LayoutParams 这个使用方法这里引用书中的一段我就不去写大量代码了:

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
             params.width += 100;
             params.leftMargin += 100;
             mButton1.requestLayout();
             //或者mButton1.setLayoutParams(params);

下面我来介绍一下弹性滑动,什么是弹性滑动?弹性滑动就是将一次大的滑动分成若干次小的滑动,并且在一段时间内完成。那下面我们来学习几种弹性滑动的实践吧。

文章之前介绍过Scroller 弹性滑动对象,也点到某一些源码中看了一下。现在我们再进一步的查看一下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;
}

我来解释一下参数 start 是起点,dx,dy 是距离,duration 是完成的总时间毫秒。我们可以看到这个方法都是计算和保存参数的并没有做什么实质性的动作。那么是怎么让View进行滑动的呢?是因为我们调用了invalidate方法,会导致View进行重新绘制,View在draw方法中会调用computeScroll方法,我们重写了computeScroll方法加入了判断mScroller.computeScrollOffset() 这个语句,意思是是否完成滑动如果完成了,就返回false,没文成会执行判断条件里面的代码,进一步的进行滑动,看到我们取得的是scrollTo 中传递的参数是scroller.getCurrX(), scroller.getCurrY() ,这个就我们在内部规定时间内每滑动的距离,如果没有滑动完成继续滑动不断的调用。

我们接下来看一下computeScroll()回调方法我们重写的判断语句  if (scroller.computeScrollOffset()) {.....} 这个判断条件里面具体是什么:

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;
       .....
        }
    }
   ....
    return true;
}

有没有一种豁然开朗的感觉,这就是一个插值器嘛…………不断的计算参数。根据时间判断是否继续滑动。 

Scroller这个滑动对象介绍完毕,接下来介绍另一种弹性滑动就是通过动画,在这里我们运用到了属性动画,比如
以下代码可以让一个View的内容在100ms内向左移动100像素。

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

实现非常非常的简单是吧,但是我们也可以不通过这种方式实现:

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

我们在ValueAnimator.addUpdateListener 添加了个回调,然后监听这个改变状态,根据返回当前动画执行进度进行计算,我们需要滑动多少~也可以运用Handler 发送延时请求进行滑动,方法有很多。

上面说完了弹性滑动,下面我们说说View中比较重要的一环吧,就是View的事件分发机制,也是面试中最常见的问题,View的事件分发整个都是围绕MotionEvent的传递过程,这个事件分发的顺序是Activity -> Window -> View,其中Activity 不言而喻,是我们最常用的一个组件,Window 是管理窗口的,在这里我们调用的是PhoneWindow这个类,Activity 把事件传递给他,然后PhoneWindow 把又给了View,这个View的名字还不是我们概念中的控件View,而是DecorView,这个DecorView是我们这个Activity的底层容器,是setContentView 的父容器。然后才是从DecorView进行分发到子View一步一步的向下分发。

下面我先介绍一下事件分发的几个方法:

public boolean dispatchTouchEvent(MotionEvent ev)

从字面意思上就可以知道,这个是事件分发的第一个方法,分发触摸事件任何View都会有这个方法,一旦有事件被分发到这个View,第一个调用的就是这个方法,他的返回值 返回true 代表消耗了这个事件,返回false 代表没有消耗这个事件,把事件会推给他的父View,一级一级的向上传递。 同时ViewGroup的这个方法的内部会调用 onInterceptTouchEvent方法判断是否拦截事件。

public boolean onInterceptTouchEvent(MotionEvent event)

这个方法的作用,是否拦截该事件自己处理, 是ViewGroup 内部的方法。View里面没有,因为View 已经是最底层的了,不需要判断是否拦截了。默认就已经拦截了事件了,只需要做处理或者不处理的操作就行了,如果我们调用了这个事件,那么就不会向下面的子View传递这个事件,自己处理~

public boolean onTouchEvent(MotionEvent event)

这个就是最后的处理 点击屏幕事件的方法,每个View,或者ViewGroup内部中都有一个这个方法,如果ViewGroup调用了onInterceptTouchEvent 并且拦截了事件那么 ViewGroup就会调用自己的onTouchEvent ,否则如果没拦截那么就会向下传递这个事件给子View,View再做处理,在onTouchEvent 的返回值中,我们返回true,代表这我们已经消耗了这个事件,处理掉了这个事件,返回false,代表我们处理不了这个事件,然后上一级拿到这个事件之后就会调用自己的onTouchEvent。 一直这么判断下去。

这是大概的理解方式,还没有到代码阶段,我在一本书中看到了一个伪代码表现的很有意思:

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

这个伪代码非常形象的表示了事件分发的判断方式!我在调用 View的时候发现里面有一个方法setTouchListener 如果设置了这个onTouchListener(这个是View内部的一个接口里面有一个回调方法onTouch),那么我们会判断这个接口内部方法的onTouch,在dispatchTouchEvent 里面会先判断这个接口是否为空,如果不为空就调用onTouch方法,否则才会开始判断并且调用onTouchEvent 这个方法。

这里再重点提一下如果子View的onTouchEvent 返回了false ,那么其父View  接下来会判断这个返回值,如果是false 那么代表子View 没有消耗这个事件,父View就会自己调用属于自己的onTouchEvent 如果自己的也不处理这个事件那么他就会和子View 一样,返回给属于他的父View!

下面是本人在书中得到的一些注意点(这个自己写和抄写一个样子我就丢上来了,理论就是理论就像太阳从东边升起一样):

(1) View事件是指同一个时间序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down 事件开始,中间含有数不清的move事件,最后以up事件结束。

(2)正常情况下,一个事件序列只能被一个View拦截且消耗。一旦某个元素拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强制传递给其它View处理。

(3)某个View一旦决定拦截,那么这一个事件序列都只能由它处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问他是否要拦截了。

(4)某个View一旦开始处理事件,那么它不消耗ACTION_DOWN 事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中的剩下事件就不会再交给它处理了。(这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,二者是类似的道理。当然如果以上理解了这段比喻可以不用看,以免看完更抽象反而蒙了。)

(5)如果View不消耗除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的clickable属性默认是true,而且TextView的clickable属性默认是false。

(9)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的的onTouchEvent就返回true。

(10)onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件,。

(11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisllowIntercept方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

下一片我会单独开放一下View事件分发源码解读。都放在一起太长了,当然我也可以编辑进来~

你可能感兴趣的:(自主文章记录)