View基础知识
什么是View
Android中的控件主要分为容器控件和普通控件,它们都继承View父类,容器控件中可以容纳多个控件(容器控件与普通控件)。这种关系最终形成View树的结构
View的位置参数
View的位置主要由4个顶点来决定,分别是:top,left,right,bottom。其中top是控件左端横坐标,right是控件右端横坐标,top是控件顶部纵坐标,bottom是控件底部纵坐标。可以通过getter方法获得
那么,控件的宽高就为
Width = right – left;Height = bottom – top
从Android3.0开始,View增加了额外的参数:x,y,translationX和tranlationY,这四个参数都是相对于父容器的坐标;x,y表示View的左上角坐标,translationX与translationY是表示View相对于父容器的偏移量(属性动画平移);两者的换算关系:
X = left + translationX;Y = top + translationY;
注意:在View平移过程中,top和left是原始左上角的位置信息,其值并不会改变,改变的是x,y,translationX和tranlationY
MotionEvent和TouchSlop
在手机触摸屏幕后产生的一系列事件,典型的事件类型:
ACTION_DOWN, ACTION_MOVE, ACTION_UP
例如:点击事件, DOWN-> UP
滑动事件:DOWN->MOVE->UP
通过MotionEvent对象提供的方法我们可以获取到事件发生的坐标位置:getX/getY和getRowX/getRowY;前者获取的坐标是相对于View自身,后者坐标的是相对于手机屏幕左上角;
TouchSlop是系统所能识别出的被认为是滑动的最小距离,如果用户在屏幕滑动的两点之间距离小于这个常量,那么系统就认为是在进行滑动操作。这是一个常量,与设备有关,不同设备上可能不同
ViewConfiguration.get(this).getScaledTouchSlop()
意义:在处理滑动时可以把该值作为临界值
VelocityTracker、GestureDetector和Scroller
VelocityTracker用于追踪手指在滑动过程中的速度(包括水平与垂直)
// 获取对象
VelocityTracker tracker = VelocityTrakcher.obtain();
// 添加事件
tracker.addMovement(event);
//计算速度
tracker.computeCurrentVelocity(times);
int xVelocity = (int) tracker.getXVelocity();
int yVelocity = (int) tracker.getYVelocity();
// 不用时重置并回收
tracker.clear();
tracker.recycle();
GestureDetector作用主要是进行手势的设定与检测,可辅助检测用户的单击,滑动,双击等行为
Scroller主要用于实现View的弹性滑动
View的滑动
实现View滑动的三种方式:
1.使用ScrollTo/ScrollBy
ScrollBy是基于当前位置的相对滑动
ScrollTo是基于参数的绝对滑动
ScrollBy内部其实也是调用的scrollTo方法
scrollTo(mScrollX + x,mScrollY + y);
mScrollX表示View左边缘到View内容左边缘的距离
mScrollY表示View上边缘到View内容上边缘的距离
注意:两方法改变的是View内容位置无法改变View在布局中的位置
(也就是说无法scroll到其他控件的区域中)
SrcollTo/By是瞬间完成,可以配合Scroller或Handler实现弹性滑动
利用Handler即每次延时发送一个消息调用SrcollTo/By方法
利用Scroller实现弹性滑动原理:
当我们构造一个Scroller对象并调用startScroll方法时,其实Scroller内部什么事也没做(只是保存了传递的参数)
那么View是如何实现滑动呢
调用startScroll后接着调用invalidate方法,该方法会导致View重绘,在重绘时会调用computeScroll方法,该方法由我们覆写并调用computeScrollOffset。computeScrollOffset方法会根据插值器类型以及时间的流逝计算当前的scrollX与scrollY,接着我们就可以通过Scroller对象getter方法获取这两个值并调用scrollTo方法;调用scrollTo方法后再次调用invalidate方法触发下一次滑动直至滑动结束
computeScrollOffset的返回值是boolean值,true表示滑动未结束
总结:Scroller本身不能实现View的滑动,需要配合computeScroll方法才能完成弹性滑动效果,通过不断的让View重绘,而每次重绘距离起始时间会有一个时间间隔,通过这个时间间隔获得View当前需要滑动的位置,然后通过scrollTo进行滑动
2.使用动画
动画可以分为:View动画(帧动画,补间动画)与属性动画
两种动画的区别本质在于:后者能够改变控件的位置
原因分析:之前有提到translationX/Y,这两个属性是在android3.0新增的,而属性动画只支持android3.0+,低版本需要使用nineoldandroids库进行兼容。translationX/Y是表示控件相对于父容器的偏移位置(类似margin,两者相互独立)。属性动画即通过这两个值来改变控件位置的,也就是说设置translationX/Y是能够改变控件位置的,但是不会改变控件LayoutParams中的margin属性,改变的是控件本身android:translationX/Y属性。
margin属性会改变控件的顶点坐标(lrtb),而translationX/Y属性是不会改变LayoutParams的,改变的是x与y。magin属性是属于父容器的属性,而translationX/Y是属于控件本身的(理解)。
那么对于属性动画的复位,我们可以直接用view. setTranslationX(0);
可以通过以下几种方式获取控件位置:
view.getLocationInWindow(pos);//控件在其父窗口中的坐标位置
view.getLocationOnScreen(pos);//控件在其整个屏幕上的坐标位置
view.getLocalVisibleRect();
view.getGlobalVisibleRect();
注:通过View动画 + updateLayoutParams的方式也可实现改变位置
Android3.0以下通过兼容库实现的属性动画本质还是View动画
3.改变布局参数:适合于有交互的View
layoutParams=(RelativeLayout.LayoutParams) v.getLayoutParams();
View的事件分发机制
三个核心方法:
dispatchTouchEvent(MotionEvent event)
onInterceptTouchEvent(MotionEvent event)
onTouchEvent(MotionEvent event)
三者关系(伪代码):
public void diapatchTouchEvent(Motion event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = true;
}else{
if(haveChild)
consume = child. dispatchTouchEvent(event);
}
return consume;
}
事件的传递:事件的捕获过程与事件的冒泡过程
捕获(传递)过程:Activity -> Window -> View(过程中被拦截将终止)
冒泡(处理)过程:View -> Activity(过程中被消费将终止)
当一个点击事件产生后,对于一个ViewGroup来说首先会调用dispatchTouchEvent方法,如果这个ViewGroup的onInteceptTouchEvent方法返回true表示要拦截当前事件,那么该事件会交给这个ViewGroup处理,它的onTouchEvent方法会被调用,如果onInterceptTouchEvent方法返回false表示不拦截事件,这时事件会继续传递给子元素,子元素的dispatchTouchEvent被调用,如此反复直至事件最终被处理
当一个View需要处理事件时,并且它设置了onTouchListener,那么onTouch方法会先被调用,如果onTouch方法返回false会继续调用onTouchEvent方法,否则onTouchEvent方法将不会被调用,也就是说onTouch优先级比onTouchEvent高。我们常见的onClick优先级是最低的,onClick会在onTouchEvent中处理;
在事件传递与处理的过程中,如果某个View的onTouchEvent方法返回false,那么事件会传递给父容器的onTouchEvent;如果最后所有View的onTouchEvent方法均返回false(所有元素都不处理该事件),那么该事件最终会传递给Activity,即Activity的onTouchEvent方法被调用(一层一层向上抛的过程,类似冒泡)
关于事件传递机制的一些总结:
1. 同一个事件序列是指从手指触摸屏幕开始到手指离开屏幕,即从down开始,中间包含n个move,最后以up结束
2. 正常情况下一个事件序列只能够被一个View拦截且消耗。因为一旦某个元素拦截了某事件,那么同一个事件序列的后续事件都会直接交由它处理,因此同一个事件序列的事件不能分别由两个View处理。(特殊手段:通过调用其他View的onTouchEvent方法)
3. 当View决定拦截事件后,那么同一事件序列的后续事件都会由它处理,不会再调用onInterceptTouchEvent询问是否拦截
4.当View一旦开始处理事件(即执行onTouchEvent方法),若它不消耗ACTION_DOWN事件,那么同一事件序列的后续事件都不会再交由它处理,并且将该DOWN事件重新交由父容器处理
5.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent方法不会被调用,并且该View能够持续收到后续事件,最终消失的点击事件由Activity处理
6. ViewGroup默认不拦截任何事件
7. View没有onInterceptTouchEvent方法,不具有拦截功能,一旦事件传递给它,它的onTouchEvent方法会被调用
8. View的onTouchEvent方法默认返回true(除非它是不可点击的,clickable与longClickable均为false)。View的longClickable默认都为false,clickable视情况而定,如Button的clickable默认为true,而TextView的clickable默认为false
注: setOnClickListener时会自动设置clickable为true
setOnLongClickListener时会自动设置longClickable为true
9. View的enable属性不影响onTouchEvent方法的默认返回值
10. OnClick发生的前提的View可点击并且收到DOWN与UP事件
11.通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父容器的事件分发过程(除ACTION_DOWN事件外)
12.在View消耗ACTION_DOWN事件后,父容器仍然可拦截后续事件
注意:上述提及的可拦截事件的View均指的是ViewGroup
源码解析:
Activity#dispatchTouchEvent 事件分发源头
Window# superDispatchTouchEvent事件传递给DecorView(抽象)
- Window可控制顶级View的外观和行为策略
- Window的唯一实现类是PhoneWindow
PhoneWindow # superDispatchTouchEvent事件传递DecorView(实现)
- DecorView是PhoneWindow的内部类,代表顶级View
- DecorView继承自FrameLayout,会调用其dispatchTouchEvent
ViewGroup#dispatchTouchEvent ViewGroup的事件分发
- ACTION_DOWN事件会导致状态重置
- (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)避免多次执行拦截方法
-遍历子View将事件向下分发
- dispatchTransformedTouchEvent调用子View的事件分发
若找到接收事件的子View会跳出循环并赋值mFirstTouchTarget
-如果ViewGroup没有子元素或者没有任何子元素处理事件将调用View的事件分发super.dispatchTouchEvent(event);
View的滑动冲突
在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。
主要有三个场景:
场景1:内外两层滑动方向不一致
(如:ViewPager嵌套Fragment,Fragment存在ListView,ViewPager 内部已进行了滑动冲突的处理)
场景2:内外两层滑动方向一致
(如:ViewPager与SlideMenu同时存在)
场景3:上述两种场景的嵌套
(如:网易云音乐首页界面)
滑动冲突的处理规则
原理:主要利用事件分发机制
分析:针对场景1情况,我们可以通过某些滑动信息来决定将事件交给谁处理,如:水平滑动距离与垂直滑动距离的比较、水平速度与竖直速度或者通过路径与水平/垂直方向的角度大小;针对场景2,只能根据业务需求或者自定义控件的效果去决定由谁处理事件;而场景3是场景1与场景2的嵌套,我们只需要分别处理好中间层与外层,以及中间层与内层两个滑动冲突即可。
方式:外部拦截法,内部拦截法
外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理该事件则将其拦截(符合事件分发机制)
外部拦截法伪代码如下:
public void boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTIO_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要拦截当前事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
针对不同的滑动冲突,只需修改if条件即可。在DOWN事件时必须返回false,因为一旦返回true,后续事件直接交由父容器处理,那么事件根本无法传递给子元素;在MOVE事件中进行具体判断决定是否拦截事件;UP事件中必须返回false,主要2个原因:1.UP事件本身没有太大意义,其作为事件序列的最后一个事件必定会传递给父容器2.若UP事件返回true那么子元素将处理不了click事件
内部拦截法是指将父容器是否拦截事件交由子元素决定,这种方式与事件分发机制不一致,需配合requestDisallowInterceptTouchEvent
内部拦截法伪代码如下:
// 子元素
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 MotionEvent.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;
}
// 父容器
public boolean onInterceptTouch(MotionEvent event){
int action = event.getAction();
if( action == MotionEvent.ACTION_DOWN ){
return false;
}else{
return true; // 默认拦截除ACTION_DOWN以外所有事件
}
}
首先,父容器默认拦截除DOWN事件以外其他事件。父容器具体是否要拦截事件由子元素决定,子元素的dispatchTouchEvent方法中,DOWN事件中默认不允许父容器拦截,MOVE事件中根据具体条件决定父容器是否拦截事件,UP事件无须关注