安卓开发艺术笔记 | View的事件体系(View的分发,滑动冲突的解决)

目录

一.View的基本概念

1.什么是View

2.View的位置参数

3.MotionEvent和TouchSlop

4.VelocityTracker,GestureDetector

二.View的滑动

1.View的ScrollTo和ScrollBy

2.动画

3.LayoutParams

三.View的弹性滑动

1.Scroller

2.动画

3.延时策略

四.View的分发

1.本质

2.传递顺序

3.三个主要方法

五.View的滑动冲突

1.滑动冲突类型

2.滑动冲突解决思路

3.两种解决方式

 

一.View的基本概念

1.什么是View

View是所有控件的基类,是一种界面层的控件的抽象

ViewGroup,控件组

 

2.View的位置参数

四个属性:top,left,right,bottom(都为相对父布局的坐标)

3.0后新加的参数:x,y,trannslationX,translationY。x=left+translationX

 

3.MotionEvent和TouchSlop

(1)MotionEvent

手指接触屏幕后产生的事件,

Action_DOWN--手指刚接触屏幕

Action_MOVE--手指移动

Action_UP--手指抬起

例如:

点击屏幕后离开 事件序列为DOWN->UP

点击屏幕滑动后离开 事件序列为DOWN->MOVE->...->MOVE->UP

通过MotionEvent对象的getX/getY和getRawX/getRawY可以得到点击事件发生的相对父布局的坐标和相对屏幕左上角的坐标

 

(2)TouchSlop

系统能识别的最小的滑动距离

ViewConfiguration.get(getContext()).getScaledTouchSlop()得到

 

4.VelocityTracker,GestureDetector

(1).VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。

使用过程:首先在view的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();//实例化一个VelocityTracker 对象 velocityTracker.addMovement(event);//添加追踪事件

接着在ACTION_UP事件中获取当前的速度。注意这里计算的是1000ms时间间隔移动的像素值,假设像素是100,即速度是每秒100像素。另外,手指逆着坐标系的正方向滑动,所产生的速度为负值,顺着正反向滑动,所产生的速度为正值。

velocityTracker .computeCurrentVelocity(1000);//获取速度前先计算速度,这里计算的是在1000ms内 

float xVelocity = velocityTracker .getXVelocity();//得到的是1000ms内手指在水平方向从左向右滑过的像素数,即水平速度 
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms内手指在水平方向从上向下滑过的像素数,垂直速度

最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

velocityTracker.clear(); velocityTracker.recycle()

 

(2).GestureDetector:

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为

创建一个GestureDetecor对象并实现OnGestureListener接口,根据需要实现单击等方法:

GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象 
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象

接着,接管目标view的onTouchEvent方法,在待监听view的onTouchEvent方法中添加如下实现:

boolean consume = mGestureDetector.onTouchEvent(event); 
return consume;

然后,就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法了。

 

二.View的滑动

三种实现方法

1.通过View本身提供的scrollTo/scrollBy方法

两者区别:scrollBy是内部调用了scrollTo的,它是基于当前位置的相对滑动;而scrollTo是绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法,由于View初始位置是不变只会出现一次View滚动的效果而不是多次。

注意:两者都只能对view内容进行滑动,而不能使view本身滑动。

 

2.通过动画给View施加平移效果:

主要通过改变View的translationX和translationY参数来实现。可用view动画,也可以采用属性动画,如果使用属性动画的话,为了能够兼容3.0以下版本,需要采用开源动画库nineoldandroids。

注意View动画的View移动只是位置移动,并不能真正的改变view的位置,而属性动画可以。

 

3.通过改变View的LayoutParams使得View重新布局:比如将一个View向右移动100像素,向右,只需要把它的marginLeft参数增大即可,代码见下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams(); 
params.leftMargin += 100; 
btn.requestLayout();// 请求重新对View进行measure、layout

 

三种方式对比:

  • scrollTo/scrollBy:操作简单,适合对view内容滑动。非平滑
  • 动画:操作简单,主要适用于没有交互的view和实现复杂的动画效果
  • 改变LayoutParams:操作稍微复杂,适用于有交互的view。非平滑

 

三.弹性滑动

1.使用Scroller

Scoller本身无法让View弹性滑动,它需要和View的computerScroller方法配合使用。

Scroller惯用代码:

Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象 
private void smoothScrollTo(int dstX, int dstY) { 
  int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离 
  int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离 
  int deltaX = dstX - scrollX;//x方向滑动的位移量 
  int deltaY = dstY - scrollY;//y方向滑动的位移量 
  scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动 
  invalidate(); //刷新界面 
} 
@Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记 
public void computeScroll() { 
  if (scroller.computeScrollOffset()) {
    scrollTo(scroller.getCurrX(), scroller.getCurY()); 
    postInvalidate();//通过不断的重绘不断的调用computeScroll方法 
   }
}

其中StartScroll进行的是赋值操作,没有进行滑动

具体过程:

在MotionEvent.ACTION_UP事件触发时调用startScroll方法->

通过startScroll传入参数->

invalidate请求重绘->

在View.draw方法中调用ComputeScroll方法->

注:在computeScroll方法中按照传入的滑动距离和时间进行百分比的分配,再调用scrollTo来分段滑动实现弹性滚动。

 

2.通过动画:动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动。方法是:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms内使得View从原始位置向右平移100像素

3.使用延时策略:通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过handler.post,也可使用线程的sleep方法。

对弹性滑动完成总时间有精确要求的使用场景下,使用延时策略是一个不太合适的选择。

 

 

四.View事件分发机制

1.事件分发本质:

MotionEvent事件的分发过程。即当一个MotionEvent产生了以后,系统需要将这个点击事件传递到一个具体的View上。

 

2.点击事件的传递顺序:

Activity(Window) -> ViewGroup -> View

 

3.三个主要方法:

dispatchTouchEvent:进行事件的分发(传递)。返回值是 boolean 类型,受当前onTouchEvent和下级view的dispatchTouchEvent影响

onInterceptTouchEvent:对事件进行拦截。该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup的onTouchEvent,在ViewGroup中处理事件,而不接着分发给View。且只调用一次,所以后面的事件都会交给ViewGroup处理。

onTouchEvent:进行事件处理。

 

整个流程伪代码实现

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

大致传递规则:

ViewGroup本身是否拦截,如果拦截则交由ViewGroup的onTouchEvent来处理,

否则交由子控件的dispatchTouchEvent来处理。

安卓开发艺术笔记 | View的事件体系(View的分发,滑动冲突的解决)_第1张图片

 

注:

如果View实现了onTouchListener,则其中onTouch会对事件产生影响,如果返回true,则不执行onTouchEvent,否则执行。

得到一个优先级:

onTouchListener>onTouchEvent>onClickListenr

 

 

五.View的滑动冲突

1.滑动冲突的类型:

类型一:外部滑动方向和内部滑动方向不一致

类型二:外部滑动方向和内部滑动方向一致

类型三:以上两种场景的嵌套

 

2.滑动冲突的解决思路:

类型一:由于滑动方向不一致,可以根据滑动的水平方向和竖直方向的距离差来判断

类型二:根据具体的业务逻辑,判断滑动应该交由哪个部份处理

类型三:由于更加复杂,所以一样要从业务逻辑出发找解决办法

 

3.滑动冲突的解决方式:

(1)外部拦截法

点击事件都先通过父容器的拦截处理,所以在父容器的onInterceptTouchEvent方法中对滑动进行判断,从而决定是否拦截。

方法:重写父容器的onInterceptTouchEvent(),以下是伪代码

注意:ViewGroup比较特殊,一旦他要拦截事件,则后续事件都会交给他来处理。

//重写父容器的拦截方法
public boolean onInterceptTouchEvent (MotionEvent event){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
      //对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE:
      //对于ACTION_MOVE事件根据需要决定是否拦截
         if (父容器需要当前事件) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
      case MotionEvent.ACTION_UP://对于ACTION_UP事件必须返回false,一旦拦截子View的onClick事件将不会触发
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }

 

(2)内部拦截法:

父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。

方法:需要配合requestDisallowInterceptTouchEvent方法。以下是子View的dispatchTouchEvent方法的伪代码:

//重写子View的方法
public boolean dispatchTouchEvent ( MotionEvent event ) {
  int x = (int) event.getX();
  int y = (int) event.getY();

  switch (event.getAction) {
      case MotionEvent.ACTION_DOWN:
          //为true表示禁止父容器拦截
         parent.requestDisallowInterceptTouchEvent(true);
         break;
      case MotionEvent.ACTION_MOVE:
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要此类点击事件) {
             //父容器可以拦截,所以需要父容器默认拦截除了DOWN以外的事件。
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
      case MotionEvent.ACTION_UP:
         break;
      default :
         break;        
 }

  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}
//父View重写 
@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }

    }

除子容器需要做处理外,父容器也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子容器调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父View需要重写onInterceptTouchEvent方法:

注意:

①FLAG_DISALLOW_INTERCEPT标记位将会干预父元素除了DOWN以外事件的分发。

②父容器不能拦截DOWN的原因:由于父容器一起但拦截了该事件,后面的事件全部都拦截了,不会传递给子View,内部拦截法失效。

 

流程:

ACTION_DOWN->父view:onInterceptTouchEvent=false->

子view:dispatchTouchEvent

(先设置了标志位,不允许父容器拦截事件)

ACTION_MOVE->父容器(不拦截)->子View:dispatchTouchEvent

(如果交给父容器处理事件,则改变标志位,让父容器接下来可以拦截,配合父容器中重写的onInterceptTouchEvent,对后面的事件都拦截,所以后面的事件都交给了父View)

ACTION_MOVE->父View:onInterceptTouchEvent中拦截,执行onTouchEvent

 

 

你可能感兴趣的:(安卓开发)