3_View的事件体系

3.1.1 Veiw的基础知识

什么是View:Android 开发中,Activity承担可视化的功能,同时Android系统提供了很多基础控件,View是Android中所有控件的基类,不管是基础的Button和TextView还是复杂的RelativeLayout和RecycleView,它们共同的基类都是View,View是一种界面层的控件的一种抽象,它代表了一个控件。除了View还有ViewGroup,ViewGroup内有许多控件,即一组View,ViewGroup也继承View,这就意味着View可以是单个控件也可以是多个控件组成的一组控件。

3.1.2 View 的位置参数

View的位置主要由它四个顶点决定,分别对应View的四个属性:left、top、right、bottom,left左上角横坐标,top左上角纵坐标,right是右下角横坐标,bottom是右下角横坐标。这些坐标是相对于它的父容器来说的,因此它是一种相对坐标。

如何得到这四个参数:

Left = getLeft(); Top = getTop(); right = getRight(); Bottom = getBottom; 从android3.0开始,View增加了额外的几个参数:x、y、translationX、translationY,其中x,y是View左上角的坐标,translationX和tanslationY是左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标。

x = left + translationX; y = top = translationY;

View在平移的过程,left和top表示是原始左上角的位置信息。其值不会发生改变。

3.1.4 VelocityTracker、GestureGetector和Scroller

1. VelocityTracker 速度追踪,用于追踪手指在屏幕滑动的速度。包括在水平方向和竖直方向。首先在View的OnTouchEvent事件中追踪。

//追踪当前点击事件的速度 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); //这里的速度指的是手指在一定的时间内滑过的像素数 float xVelocity = velocityTracker.getXVelocity(); float yVelocity = velocityTracker.getYVelocity();

在android坐标系中,手指逆着坐标系正方向滑行,速度为负值,当不需要使用的时候可以调用clear方法来重置和回收内存。

velocityTracker.clear();

velocityTracker.recycle();

2 GestureDetector,手势检测,用于辅助检测用户的单击,双击,滑动,长按等事件。根据需要可以实现onDoubleTabListener监听双击行为

首先创建一个GestureDetector对象并实现OnGestrueListener接口,接着,接管目标View或目标界面的onTouchEvent方法 在onTouchEvent方法中添加如下实现:

boolean b = mGestureDetector.onTouchEvent(event); returen b;

3 Scroller 弹性滑动对象,用于实现View的弹性滑动,Scroller本身无法让View本身进行弹性滑动,它需要和View的computeScroll方法进行配合使用才能共同完成这个功能

3.2 View的滑动,通过三种方式实现View的滑动:

1 View本身提供的scrollTo/scrollBy方法来实现滑动

2 通过动画来实现View的滑动效果

3 改变View的LayoutPararms使得View重新布局从而实现滑动。

3.2.1 使用scrollTo/scrollBy, scrollTo是相对当前参数的绝对滑动,scrollBy是基于当前位置的相对滑动。实际scrollBy也调用了scrollTo方法。同时,scrollBy和scrollTo只能改变View内容的位置不能改变View在布局中的位置
3.2.2通过动画可以实现View的平移,主要是操作ViewtranslationX和translationY属性,采用属性动画时,为了兼容android3.0需要采用开源动画库nineoldandroids。View动画(非属性动画)是对View的影像做操作,并不能真的改变view的位置参数,如果希望动画后的状态得已保存还必须将fillAfter属性设置为true。属性动画并不会存在上述问题。
3.2.3 改变布局参数,即改变LayoutPararms。

ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); marginLayoutParams.leftMargin += 100; mView.requestLayout(); 或者 //mView.setLayoutParams(marginLayoutParams);

3.2.4 各种滑动方式的对比
1 scrollTo/scroBy是View提供的原生的方法,其作用专门用于滑动的,可以方便的实现滑动效果而不影响内部元素的单击事件。缺点是只能滑动view的内容,不能滑动view本身。
2 通过动画实现View的滑动,在android3.0以上使用属性动画无明显缺点,使用非属性动画或者在3.0以下使用属性动画,均不能改变view本身的属性,如果不影响用户交互的情况下,非属性动画是合适的,否则不太合适。
3 改变布局方式实现滑动,无明显缺点,操作稍微复杂。

3.3 弹性滑动有一个共同的思想,将一次大的滑动分成若干个小的滑动并在一个时间段内完成。

3.3.1使用Scroller,这里的滑动是指View内容的滑动而非View本身位置的滑动,仅仅调用Scroller是无法让View滑动的,因为它内部并没有做滑动相关的事。因为invalidate方法,invalidate方法会导致view的重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在view中是一个空实现,需要我们自己去实现。正是这个computeScroll这个方法,View才能实现弹性滑动。

View重绘后会在draw方法调用computerScroll方法,computeScroll方法中又会向Scroller获取当前scrollX和scrollY,然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法第二次重绘,这一次重绘和上次重绘一样,都是通过scrollTo滑到新的位置,如此反复,调用Scroller的computeScrollOffet当返回false时整个滑动结束。

3.3.2 通过动画,动画本身是一种渐进的过程,通过动画实现的滑动,天然的具有弹性效果,让一个view在100ms内移动100个像素:

ObjectAnimator.ofFloat(tartgetView,"translationX",0,100).setDuration(100).start();
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDouration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @Override
    public void onAnimationUpdate(ValueAnimator animator){
        float fraction = animator.getAnimatedFraction();
        mButton1.scrollTo(startX + (int)(deltax * fraction),0);
    }
})

在上述代码中动画本质上没有作用到任何对象上,只是在一千毫秒内完成动画的过程,我们就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。

###3.3.3使用延时策略,它的核心思想是通过发送一系列延时消息从而达到一种渐进式效果,具体可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法

3.4 View的事件分发机制

3.4.1 点击事件的传递规则

点击事件分析的对象就是MotionEvent, 所谓点击事件的分发其实是对MotionEvent事件的分发过程,即当一MotionEvent产生以后,系统需要把这个事件传递给一个具体的View,而这个过程就是分发过程,分发过程由三个很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatTouchEvent方法的影响。

public boolean onInterceptTouchEvent (MotionEvent ev)

用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouEvent(MotionEvent ev)

在dispatchTouchEvent方法中调用,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OntouchListener中的onTouch方法会被调用,如果返回false,onTouchEvent方法会被调用,返回true的话onTouchEvent方法不会被调用,因此OnTouchListener优先级高于onTouchEvent方法,在onTouchEvent方法中,如果当前View设置了OnClicklistener,那么它的onClick方法会被调用,因此OnClickListener优先级最低,处于事件传递的尾端。

事件产生后的传递过程:

Activity-->Window-->View,事件总是先传给Activity,activity在传递给window,最后window再传给顶级View。如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,如果所有的元素都不处理这个事件,那么这个事件最终会传递给activity处理,即activity的onTouchEvent会被调用。

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

ViewGroup默认不拦截任何事件,onInterCepTouchEvent方法默认返回false。

View没有onInterceptTouchEvent方法,一旦事件传递给它,那么它的onTouchEvnet方法一定会被调用。

View的onTouchEvent默认都会消耗事件,除非该View是不可点击的(clickable和longClickable同时为false)。View的longClickablse属性都默认为false

View的enable属性不影响onTouchEvent默认返回值,只跟clickable和longClickable有关。

事件传过程是由外向内的,事件总是先传递给父元素,再由父元素分发给子元素,但是通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN除外

3.4.2 事件分发的源码分析

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

Window是个抽象类,Window的superDispatchTouchEvent也是抽象方法,Window的实现类是PhoneWindow,看源码知道,PhoneWindow会将事件直接传递给DecorView,可以通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式获取activity所设置的顶级View,设置的View的父容器就是DecorView,所以最终事件会传递给View。

ViewGroup在两种情况下会判断是否要拦截当前事件:

事件类型为ACTION_DOWN或者mFirstTouchTarget != null ,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,也就是VeiwGoup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget != null成立,反之不成立。不成立的话,那么当ACTION_DOWN和ACTION_UP到来时事件类型为ACTION_DOWN或者mFirstTouchTarget != null这个条件不成立将导致ViewGroup的onInterceptTouchEvent不会被再调用。并且同一序列的其他事件都会默认交给它处理。

*FLAG_DISALLOW_INTERCEP这个标记位是通过requestDisallowIntercepTouchEvent方法来设置的,一般用于子元素中,一旦设置后,ViewGroup将无法拦截除ACTION_DOWN以外的其他事件,ViewGroup在分发事件时,如果是ACTION_DOWN,将导致子View中设置的这个FLAG_DISALLOW_INTERCEPT标记位无效。因此,当面对ACTION_DOWN事件时,ViewGrop总是会调用自己的onInterceptTouchEvent。

第一点:onInterceptTouchEvent不是每次事件都会被调用的。如果想提前处理所有的事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用。

第二点:FLAG_DISALLOW_INTERCEPT标记位可以解决滑动冲突。

ViewGroup不拦截事件时,首先会遍历ViewGroup的所有子元素,判断子元素是否能够接收到事件。是否能够接收事件主要由两点判断:

子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。

mFirstTouchTarget真正的赋值是在addTouchTarget内部完成的,如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:

第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件。

3.5 View的滑动冲突:在界面中只要内外两层同时可以滑动,,这个时候就会产生滑动冲突

3.5.1 常见的滑动冲突

1 外部滑动方向和内部滑动方向不一致;

2 外部滑动方向和内部滑动方向一致;

3 上面两种情况的嵌套;

3.5.2 滑动冲突的处理规则

image

场景1:当用户左右滑动时,需要让外部拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件,水平滑动和竖直滑动可以依据滑动路径和水平方向所形成的夹角也可以依据水平方向和竖直方向的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。这里我们可以通过水平和竖直方法的距离差来判断,比如竖直方向滑动的距离大就判断为竖直滑动,否则判断为水平滑动

场景2 无法通过滑动的角度、距离差和速度差来做判断,一般这个时候可业务状态来判断,比如当处于某种状态需要外部View响应用户的滑动,而处于另一种状态则需要内部View来响应用户的滑动,依据这种业务状态也能得出相应的处理规则

image

场景3来说同场景2一样无法通过角度、距离差、速度差来判断,同样还是只能从业务上找到突破点。

3.5.3 滑动冲突的解决方式

1 外部拦截

所谓外部拦截是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截反之不拦截,这样可以解决滑动冲突的问题。这种方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。

public boolean oInterceptTouchEvent(MotionEvent event){
    boolean intercept = false;
    int x = (int)event.getX();
    int y = (int)event.getY();
    case MotionEvent.ACTION_DOWN:
        interceptd = false;
    break;
    
    case MotionEvemt.ACTION_MOVE:
        if(父容器需要当前点击事件){
            interceptd = true;
        } else{
            interceptd = false;
        }
    break;
    
    case MotionEvent.ACTION_UP:
        interceptd = false;
    break;
    default;
    break;
    mLasXIntercept = x;
    mLastYIntecept = y;
    return intercepted;
}

在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION-DOWN事件,,因为一旦拦截了ACTION_DOWN,那么后续的ACTION-MOVE和ACTION_UP事件都会直接交由父容器处理,无法再将事件传递给子元素;其次是ACTION_MOVE事件,这个事件可以根据需要决定是否需要拦截,如果父容器需要拦截就返回true,最后是ACTION_UP事件,这里必须返回false,因为ACTION_UP事件本身没有太多意义。(如果事件交由子元素处理,这时父容器在ACTION_UP中返回true,就会导致子元素无法接收到ACTION_UP事件,子元素的onClick事件就无法触发。)

2 内部拦截法 内部拦截法是指父容器不拦截任何事件,所有的事件都交由子元素,如果子元素需要拦截事件就直接处理,否则交由父容器处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,需要重写子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event){
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getAction()){
       case MotionEvent.ACTION_DOWN:
        parent.requestDisallowInterceptTouchEvent(true);
    break;
    
    case MotionEvemt.ACTION_MOVE:
        int deltax = x - mLastX;
        int deltay = y - mLastY;
        if(父容器需要此类点击事件{
        parent.requestDisallowInterceptTouchEvent(false);
        }
    break;
    
    case MotionEvent.ACTION_UP:
    break;
    default;
    break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispathcTouchEvent(event);
}

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面大的条件即可,其他不需要改动而且也不能改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他的事件。

考虑一种情况,如果此时用户正在水平滑动,但是在水平滑动停止之前如果用户再迅速进行竖直滑动,就会导致界面在水平方向无法滑动到终点从而处于一种中间状态。因此,但水平方向正在滑动时,下一个序列的点击事件仍然交给父容器处理,这样水平方向就不会停留在中间状态了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值