View虽然不属于四大组件,但它的作用堪比四大组件,甚至比Receiver和Provider的重要性都大,在Android开发中,Activity承担这可视化的功能,同时Android系统提供了很多基础控件,常见的有Button、Textview、CheckBox等。
View的基础知识
什么是View
View是一种界面层的控件的一种抽象,代表了一个控件。View是所有控件的基类。ViewGroup是控件组,内部包含了许多控件,也继承了View,这就意味着View本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系形成了View树的结构。
View位置的参数
View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角的纵坐标。需要注意的是,这些坐标都是相对于View的父容器来说的,因为它是一种相对坐标。
获取:getLeft(),getRight(), getTop(), getBottom();
在Android中,X轴和Y轴的正方向分别为右和下。那么就可以得出以下关系:
width = right - left
height = bottom - top
从Android3.0开始新增x, y, translationX, translationY,x,y表示view相对于父容器的左上角坐标,translationX, translationY表示view相对于父容器的偏移量,默认值为0
MotionEvent和TouchSlop
1.MotionEvent和TouchSlop
在手指接触屏幕后会产生一系列的事件,典型的事件类型有如下几种:
ACTION_DOWN:手指刚触碰到屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指在屏幕上松开的瞬间
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,如下:
点击屏幕后离开松开,事件序列为DOWN->UP
点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->…->MOVE->UP
同时我们可以通过MotionEvent对象我们可以得到点击事件发生的X坐标和Y坐标。为此,系统提供了两组方法:getX/getY和getRawX/getRawY,区别很简单,前者返回的是相对于当前View左上角的X和Y坐标,而后者返回的相对于手机屏幕左上角的X和Y坐标。
2.TouchSlop
是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作,因为滑动的距离太短了,系统就不认为它是滑动,这是一个常量,跟设备有关。可以通过以下方式获取:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
VelocityTracker追踪手指滑动过程中的速度
在onTouchEvent中追踪点击事件的速度
VelocityTrackervelocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000),1000ms内划过的像素
velocityTracker.getXVelocity();水平速度
velocityTracker.getXVelocity();竖直速度
velocityTracker.clear();
velocityTracker.recycle();
5 GestureDetector 手势检测 检测单击、双击、长按、滑动
onSingleTapUp(单击)、onFailing(快速滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击)
弹性滑动:
Scroller 弹性滑动对象 有过渡效果的滑动,以下是固定写法
Scroller scroller = new Scroller(context);
private void smoothScrollTo(int x, int y){
int detal = x – getScrollX();
scroller.startScroll(getScrollX(), 0, detal, 0, 1000);
invalidate();
}
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrentX(), mScroller.getCurrentY());
postInvalidate();
}
}
View的滑动
三种实现:
- scrollTo/scrollBy
- 使用动画
- 改变布局参数
ScrollTo/ScrollBy
我们需要获取View里的两个属性mScrollX和mScrollY,在滑动过程中,mScrollX的值总是等于View的左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总等于View上边缘和View内容上边缘在竖直方向的距离。View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘
scrollTo/scrollBy只能改变View内容的位置而不能改变View在布局中的位置。
如果从左往右滑动,那么mScrollX为负值,反之为正值,如果从上往下滑动,那么mScrollY为负值,反之为正值。
使用动画
使用动画我们能够让一个View进行平移,而平移就是一个滑动。主要操作View的translationX和translationY属性
View动画不是真正的改变View的位置,是影像不是真的View
View动画:
向右下角平移100
View动画是对View的影像做操作,并没有真正改变View的位置参
数,动画完成后结果会消失,设置fillAfter属性为true会保留动画后的状态,移动后的View无法触发onClick事件
属性动画 View从原位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
属性动画后动画结果不会消失,并且在平移动画之后,能够触发onClick事件
改变布局参数
改变LayoutParams,通过设置margin参数即可达到移动的效果
三种对比
scrollTo/scrollBy: 适合对View内容的滑动
动画: 适合没有交互的View和复杂的动画效果
改变布局参数: 适合有交互的View
弹性滑动
知道了View的滑动,我们还需要知道如何实现View的弹性滑动,比较生硬的滑过去,这种方式的用户体验实在太差了,因此我们要实现渐近式滑动。如何实现弹性滑动呢,有个共同思想:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成,常见的弹性滑动具体实现方式有很多,比如通过Scroller、Handler#postDelayed,以及Thread#Sleep等等。
使用Scroller
当构造一个Scroller对象并调用它的startScroll方法时,Scroller内部其实什么也没做,只是传递了几个参数并保存了,而invalidate方法会导致View重绘,draw方法又会调用computeScroll方法,而在computeScroll中通过scrollTo方法让View滑动,接着又调用postInvalidate方法再次重绘,直到滑动过程结束。
而在computeScroll方法中,通过computeScrollOffset判断当前是否重绘结束,通过时间的流逝来计算当前View的位置坐标。如果经过的时间小于总时间,继续重绘,直到时间达到动画的总时间,重绘结束,同时滑动完成。
View的重绘距滑动起始会有一个时间间隔,通过这个时间间隔就能得出当前View的滑动位置,然后通过scrollTo方法完成View的滑动,这样每次重绘都会让View小幅度滑动,多次就成了弹性滑动。
使用动画
动画本身就是一个渐进的过程 通过动画的每一帧的到来获取动画完成的比例,然后根据这个比例计算View应该滑动的距离。
演示策略
核心思想是通过发送一系列延时消息从而达到渐进式的效果
通过Handler或postDelayed,或者在线程中通过while和sleep来不断的发送消息,在消息中进行View的滑动。
View的分发机制
点击事件的传递规则
首先我们要明白这里分析对象就是MotionEvent即点击事件,所谓的点击事件就是对MotionEvent事件的分发过程,当一个MotionEvent产生以后,系统要把这个事件传递给一个具体的View,这个过程就是分发机制。
View的分发机制主要是由三个函数决定:
dispatchTouchEvent:分发
onInterceptTouchEvent:拦截
onTouchEvent:消耗
一个触摸事件,如果事件坐标处于ViewGroup所“管辖范围”,首先调用的是该ViewGroup的dispatchTouchEvent函数,dispatchTouchEvent函数内部调用onInterceptTouchEvent函数,用于判断是否拦截该事件,如果拦截,则调用ViewGroup的onTouchEvent。否则调用子View的dispatchTouchEvent函数,可以参考图下
注意,上述图中,只是描述事件从ViewGroup往下传递过程,没有考虑子View的onTouchEvent的返回值,即没有考虑事件从子View往上回传的过程。后面再介绍事件回传的过程。ViewGroup是否拦截事件,是通过onTnterceptTouchEvent返回值来确定,当返回true时,表示拦截该事件,那么该系列事件全部传递给ViewGroup的onTouchEvent,如果返回false,则表示不拦截该系列事件,该系列事件全部交给子View来处理。为什么我们说是“该系列事件”,而不是说“该事件”呢?注意,View的事件体系中,从down->move->…->move->up。这一个过程为同一个事件系列,如果在onInterceptTouchEvent中返回false,那么所有的事件都不会再交给ViewGroup的的onTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}
else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件的来源
我们知道,我们直接通过onTouchEvent里面的形参就可以拿到事件对象,可是事件对象时从哪里产生的?又是经历过哪些曲折的道路才到达目的地的?
首先,Activity拿到事件对象,Activity把事件对象传递给PhoneWindow,PhoneWindow再传递给DecorView,DecorView通过遍历再传递到我们的ViewGroup。那么Activity又是从哪里得到事件对象的呢?这里面就涉及的比较底层了,感兴趣的童鞋参考任玉刚的《 Android中MotionEvent的来源和ViewRootImpl 》这篇文章。
onTouch,onClick,onTouchEvent优先级
还有一个当View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调,OnTouchListener优先于onTouchEvent。在onTouchEvent中,如果设置了OnClickListener,那么它的OnClick方法会被调用,可以看出我们平时常用的OnClickListener优先级最低,onTouch>onClick.
事件的回传机制
我们知道,在ViewGroup中,事件是dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent。由onInterceptTouchEvent决定是否将事件传递给子View。如果传递给子View,但是子View并不想处理这个系列的事件(子View的onTouchEvent返回false),该怎么处理这个系列事件呢?
看到,当子View的onTouchEvent返回的是false,那么该系列的事件会回到ViewGroup的onTouchEvent。注意,down事件先到达子View的onTouchEvent,如果子View不消耗,则down事件及其后续的事件会传到ViewGroup的onTouchEvent。而ViewGroup的onTouchEvent也是一样,如果ViewGroup不处理该系列事件,又会继续回传到ViewGroup的父View的onTouchEvent。
我们以上讨论的点击位置都是子View所处的区域,即如下如所示。
如果点击不是子View所处的区域,事件的传递会是怎么样的呢?我们看看日志信息:
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup onTouchEvent
可以看到,子View并没有调用任何函数。这很容易理解,因为压根就跟子View没有半毛钱关系,要是点击任意区域子View都会有事件传递过去那才奇怪呢!因此,可以看出,ViewGroup在传递触摸事件时,会遍历子View,判断触摸点是否在各个子View中,如果在,则触发调用相关函数。如果点击的位置没有子View,那么不管onIntercepTouchEvent返回的是什么,ViewGroup的onTouchEvent都会执行!
总结
同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。一般是以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
如果View只消耗down事件,而不消耗其他事件,那么其他事件不会回传给ViewGroup,而是默默的消逝掉。我们知道,一旦消耗down时间,接下来的该系列所有的事件都会交给这个View,因此,如果不处理down以外的事件,这些事件就会被“遗弃”。
如果ViewGroup决定拦截,那么这个系列事件都只能由它处理,并且onInterceptTouchEvent不会再被调用。
某个View,在onTouchEvent中,如果针对最开始的down事件都返回false,那么接下来的事件系列都不会交给这个View。
ViewGroup默认不拦截事件,即onInterceptTouchEvent默认返回false。
View的onTouchEvent默认返回false,即不消耗事件。
View没有onInterceptTouchEvent方法。
事件分发的过程源码解释
1.Activity对点击事件的分发过程
触摸事件最先到达Activity,所以首先会在Activity中分发,由Activity的dispatchTouchEvent进行事件派发,具体是由Activity内的Window来完成的,Window会将事件传递给decor view(setContentView设置的View的父容器)。
首先事件交给Activity所附属的Widow进行分发,如果返回true整个事件就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会调用。
2.Window是如何将事件传递给ViewGroup
Window是个抽象类,Window的superDispatchTouchEvent方法也是个抽象方法,PhoneWindow是Window的唯一实现类。
PhoneWindow直接将事件传给了DecorView,DecorView是setContentView所设置View的父容器,所以事件肯定会传递给setContentView的View,也叫顶级View。
3.顶级View对事件的分发过程
对于根ViewGroup来说,点击事件首先会传给它,此时调用它的dispatchTouchEvent方法,然后执行onInterceptTouchEvent方法,如果该方法返回true,表示它要拦截该事件,然后事件就会交给ViewGroup处理,然后它的onTouchEvent方法就会被调用,如果返回false表示它不拦截当前事件,这时事件就会传递给它的子元素,接着调用子元素的dispatchTouchEvent方法,如此反复直到事件被处理。
当ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值,并指向子元素,即当ViewGroup不拦截事件并交给子元素处理时mFirstTouchTarget != null。
当ViewGroup拦截时,mFirstTouchTarget就为空,当后续事件到来时actionMasked != MotionEvent.ACTION_DOWN并且mFirstTouchTarget== null,所以该条件为false,所以ViewGroup的onInterceptTouchEvent不会再被调用,并且同一事件序列中的其它事件都将交给它来处理。
特殊情况 : 标记位FLAG_DISALLOW_INTERCEPT,子View可以通过设置requestDisallowInterceptTouchEvent方法,使得ViewGroup无法拦截除了ACTION_DOWN以外的其它事件。如果是ACTION_DOWN事件,这个标记位就会被重置。
上面代码说明了ViewGroup会在ACTION_DOWN事件到来之前重置标记位的状态。当ViewGroup不再拦截事件的时候,事件就会下发至它的子View处理。源码如下所示
遍历ViewGroup所有子元素,判断子元素是否能够接收点击事件并且点击事件是否落在子元素的区域内。如果都满足,那么事件就交给它来处理,在dispatchTransformedTouchEvent方法中有这样一段代码。
如果子元素不为空,直接调用它的dispatchTouchEvent方法,如果返回true,那么mFirstTouchEvent会被赋值并且跳出循环。如果返回false,ViewGroup会把事件分发给下一个元素,
如果遍历完所有的元素后时间都没有被处理,那么有两种情况,一是ViewGroup没有子元素,二是子元素处理了点击事件,但是dispatchTouchEvent返回了false,一般是因为在TouchEvent中返回了false,这两种情况下ViewGroup会自己处理点击事件。
这里dispatchTransformedTouchEvent方法中第三个参数为null,由之前的代码分析,它会调用super.dispatchTouchEvent方法,即交给View处理,接着看View的处理。
4.View对点击事件的处理过程。
![在这里插入图片描述](https://img-blog.csdnimg.cn/4345ed61daea4f4d9727942f2316af96.png
首先判断有没有OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,所以OnTouchListener优先级高于onTouchEvent。
接下来分析onTouchEvent,这里面主要分三个过程 : View处于不可用状态,View设置有TouchDelegate,View处于可用状态。
a. View不可用
View在不可用状态下依然会消耗点击事件
b. View设置了TouchDelegate
TouchDelegate可以让某个控件处理比它实际占用空间更大的触摸消息。具体可以参考官方文档 : TouchDelegate,具体使用 : TouchDelegate的使用。
c. View可用
整个过程分为ACTION_UP,ACTION_DOWN,ACTION_CANCEL,ACTION_MOVE四个状态,我们就最有代表性的ACTION_UP来分析
只要View的CLICKABLE或LONG_CLICKABLE为true,就会消耗这个事件,并返回true,然后触发performClick方法,如果View设置了onClick事件,那么onClick就会被调用。
可点击的View的CLICKABLE为true,不可点击的为false
View的LONG_CLICKABLE默认为false
setClickable和setLongClickable可以分别改变其属性。
setOnClickListener和setOnLongClickListener可以分别将CLICKABLE和LONG_CLICKABLE改为true。
View的滑动冲突
常见的滑动冲突场景
如何拦截
根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。几种处理方式:
外部拦截法。点击事情都是先经过父容器的拦截处理,如果父容器需要次事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。
内部拦截法。父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗点,否则就交由父容器进行处理。
外部拦截
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: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
内部拦截
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父View的onInterceptTouchEvent(…)伪代码
public boolean onInterceptTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
retuen false;
}else{
retuen true;
}
}
内部拦截法过程说明,父类在ACTION_DOWN时不拦截,子类在ACTION_DOWN时拦截,这时mFirstTouchTarget!=null, disallowIntercept = true,这意味着父类的onInterceptTouchEvent(…)不会再被执行,并且一个事件序列只有一个View来处理,则所有的后续ACTION_MOVE都会传到子View,当在子View中判断到某个事件应该由父View处理,只需重置disallowIntercept=false即可,即调用函数requestDisallowInterceptTouchEvent(false),这时事件就到父View的onTouchEvent(…)处理的(因为onInterceptionTouchEvent在非ACTION_DOWN时都返回true).如果父类没有在设置requestDisallowInterceptTouchEvent(true)的话,这个事件就会一直都在父View中做处理了.(注:为个人理解,若有不对,望其指出)