View事件
View的几个参数
- left:相对于父容器的左上角纵坐标
- top:相对于父容器的左上角横坐标
- right:相对于父容器的右上角横坐标
- bottom:相对于父容器的右上角纵坐标
- x:相对于父容器左上角横坐标
- y:相对于父容器左上角纵坐标
- translationX:相对于父容器左上角横坐标偏移量
- translationY:相对于父容器左上角纵坐标偏移量
参数的意义: 在非属性变换中
几个重要的类:
- MotionEvent
- TouchSlop:系统所能识别出的被认为是最小的滑动距离
ViewConfiguration.get(Context).getScaledTouchSlop()
- VelocityTracker:追踪手指的滑动速度
- GestureDetector
View的滑动
- View的scrollTo/scrollBy
- 只能改变View内容的位置,而不能改变view在父容器中的位置
- 使用动画
- 传统的View动画【补间动画】,如translate(后续再论)
- 属性动画
- 通过改变view的LayoutParams参数实现动画
- 通过延时策略实现
弹性滑动
补间动画 和 属性动画
补间动画的缺陷
- 只能作用于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()
,然后整个的逻辑流程就在这里处理。
首先会判断要不要对这个事件进行拦截,即是否触发
onInterceptTouchEvent()
,被拦截下来的事件将不会传递给子view,由自己消费事件。onInterceptTouchEvent
在决定拦截后,只会被调用一次。也就是说,viewGroup决定拦截事件后,那么系统就会默认地把后续的事件传递给他处理。
如果不拦截就给子view传递,会遍历所有的子view。它会首先判断这个view是否有资格接受这个事件,标准是:1、事件点击发生在子元素的区域上;2、子元素没有动画事件 。最后就调用子元素的
dispatchTouchvent()
方法。如果返回true,则调出对子元素的轮询;反之,返回false,则继续下一个子元素【返回值取决于onTouchEvent
】。- 在没有任何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在一开始就不消耗这个事件,那么后续的事件也不会传递给他,而是由消费了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的方法是:
- 构造TouchDelegate实例delegate,参数为需要修改作用范围的控件view1和增大后的rect。
- 在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()方法使用。