View事件

View的几个参数

  • left:相对于父容器的左上角纵坐标
  • top:相对于父容器的左上角横坐标
  • right:相对于父容器的右上角横坐标
  • bottom:相对于父容器的右上角纵坐标
  • x:相对于父容器左上角横坐标
  • y:相对于父容器左上角纵坐标
  • translationX:相对于父容器左上角横坐标偏移量
  • translationY:相对于父容器左上角纵坐标偏移量

参数的意义: 在非属性变换中

几个重要的类:

  • MotionEvent
  • TouchSlop:系统所能识别出的被认为是最小的滑动距离ViewConfiguration.get(Context).getScaledTouchSlop()
  • VelocityTracker:追踪手指的滑动速度
  • GestureDetector

View的滑动

  1. View的scrollTo/scrollBy
    • 只能改变View内容的位置,而不能改变view在父容器中的位置
  2. 使用动画
    • 传统的View动画【补间动画】,如translate(后续再论)
    • 属性动画
  3. 通过改变view的LayoutParams参数实现动画
  4. 通过延时策略实现弹性滑动

补间动画 和 属性动画

补间动画的缺陷

  • 只能作用于View
  • 只有固定的几种操作,无法扩展
  • 只是改变了view的显示效果,view没有真正的移动。即view的在原始位置上还是存在的。

属性动画

  • 核心类
    • ValueAnimator:对值的平滑过渡
    • ObjectAnimator:实际的对熟悉进行动画操作(内部的工作机制是通过寻找特定属性的get和set方法,然后通过方法不断地对值进行改变,从而实现动画效果的
    • AnimatorSet:组合动画
    • AnimatorListener:动画监听器
    • TypeEvaluator:告诉动画系统如何从初始值过度到结束值,是ValueAnimator实现的基础(可重写)
    • TimeInterpolator:控制动画的变化速率(可自己继承重写,实现自己对速率的控制
    • ViewPropertyAnimator:3.1引入,专门针对view的,简化view动画的操作。通过view.animate()获取
  • xml实现
    • 对应代码中的ValueAnimator
    • 对应代码中的ObjectAnimator
    • 对应代码中的AnimatorSet
  • 优势
    • 解决了补间动画没有真正移动view的问题

View的事件分发机制

view的事件分发遵循如下的顺序:Activity->Window(PhoneWindow)->ViewGroup->View。

Activity的事件处理

Activity接收到事件后,会由它的dispatchTouchEvent()来对事件进行转发。

它主要做了两个事情:

  • 将事件派发给Window处理,通过Window传递给ViewGroup(Window类控制着顶级的View)。这个过程的具体实现是在PhoneWindow,在PhoneWindow的superDispatchTouchEvent会直接调用DecorView的superDispatchTouchEvent,从而将事件传递给ViewGroup。
  • 在没有任何view消费这个事件时,调用自己的onTouchEvent把事件消费掉。

我们可以在自己的代码中获取到DecorView:

getWindow().getDecorView();

ViewGroup的事件处理

点击事件被传到ViewGroup后,会调用ViewGroup的dispatchTouchEvent(),然后整个的逻辑流程就在这里处理。

  1. 首先会判断要不要对这个事件进行拦截,即是否触发onInterceptTouchEvent(),被拦截下来的事件将不会传递给子view,由自己消费事件。

    • onInterceptTouchEvent在决定拦截后,只会被调用一次。也就是说,viewGroup决定拦截事件后,那么系统就会默认地把后续的事件传递给他处理。
  2. 如果不拦截就给子view传递,会遍历所有的子view。它会首先判断这个view是否有资格接受这个事件,标准是:1、事件点击发生在子元素的区域上;2、子元素没有动画事件 。最后就调用子元素的dispatchTouchvent()方法。如果返回true,则调出对子元素的轮询;反之,返回false,则继续下一个子元素【返回值取决于onTouchEvent】。

  3. 在没有任何view处理事件的情况下,ViewGroup将处理这个事件。即事件向上层转移,这里就是将childView传null值过去了,代码如下:
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
     // No touch targets so treat this as an ordinary view.
     handled = dispatchTransformedTouchEvent(ev, canceled, null,
     TouchTarget.ALL_POINTER_IDS);
    }
    
    • 如果子view在一开始就不消耗这个事件,那么后续的事件也不会传递给他,而是由消费了DOWN时间的view处理。这个可以扩展理解下,就是一旦下层不处理,那么上层在开始处理后,所有的事件将都由上层处理,如图: 事件上传的逻辑

View的事件处理

dispatchTouchvent()方法里,首先会判断有没有OnTouchListener,如果有了,则直接触发这个方法消费调点击事件;否则触发OnTouchEvent,进入到onTouchEvent中。

onTouchEvent里会处理各种事件,在ACTION_UP事件里会调用onClickListener

总结

  • Activity的onTouchEvent一般情况下返回的是false;ViewGroup的onInterceptTouchEvent默认返回值是false;onTouchEvent是false。
  • 如果View只消费了down事件,那么后续的事件就会消失,不会被上层接收。最后这些事件都传递给了Activity来处理。
  • View的enable属性不影响OnTouchEvent的默认返回值
  • requestDisallowInterceptTouchEvent事件总是从外向内传递的,最后分发给子View,但是子view可以通过上面的方法干预父层的分发逻辑,但是ACTION_DOWN事件除外(因为Down事件不收这个标记所控制)。

TouchDelegate

一个很牛逼的类,在不改变view的视觉的情况下可以扩大View点击的范围。重新实现了view的onTouchEvent。在View的onTouchEvent也是优先处理这个类的onTouchEvent

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event){
        return true;
    }
}

使用TouchDelegate的方法是:

  1. 构造TouchDelegate实例delegate,参数为需要修改作用范围的控件view1和增大后的rect。
  2. 在view1的祖先控件view2上设定delegate。

核心代码:

Rect rc = new Rect();
child.getHitRect(rc);
rc.bottom += 150;
parent.setTouchDelegate(new TouchDelegate(rc, child));

滑动冲突

简单而言就是页面存在内外多层滑动,导致事件冲突。

  • 外部拦截法:即在冲突的外层将事件拦截到,由父层来决定这次事件该怎么处理。这个比较简单,在onInterceptTouchEvent中把逻辑处理掉就可以了。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;

    switch (event.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;
}
  • 内部拦截法:就是父层不拦截任何事件,都传递给子元素,如果子元素愿意消费则直接消费掉,否则交给父层,子层需要配合使用onInterceptTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
        mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (父层需要当前的事件) {
                mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
        case MotionEvent.ACTION_UP: {
            break;
            }
        default:
            break;
    }

    return super.dispatchTouchEvent(event);
}

父层对应的修改:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

getMeasuredWidth/getMeasuredHeight 和 getwidth/getHeight

  • getWidth()获得的宽度是View在设定好布局后整个View的宽度。getwidth返回的是右边坐标减轻坐标减去左边坐标,这要在布局之后才能确定它们的坐标,也就是说在布局后才能调用getwidth来获取。
  • getMeasuredWidth()是对View上的内容进行测量后得到的View内容占据的宽度。一般结合onMeasured()方法使用。

results matching ""

    No results matching ""