Android中所有控件的基类。
View的位置主要由它的四个顶点来决定的,分别对应View的四个属性:left:左上角横坐标,top:左上角纵坐标,right:右下角横坐标,bottom:右下角纵坐标
获取这四个参数通过getLeft,getTop,getRight,getBottom得到,在3.0版本后,新增了几个参数:x,y:自身View的坐标;和translationX,translationY:相对于父容器的偏移量
通过MotionEvent对象,我们可以得到点击事件发生的x、y坐标。为此,系统提供了两个组方法:getX、getY(相对于当前View)和getRawX、getRawY(相对于手机屏幕)
TouchSlop是系统所能识别出的最小滑动距离,它是一个常量,和设备有关,不同设备的值不同。通过ViewConfiguration.get(getContext()).getScaledTouchSlop();获得。
作用:处理滑动时,可利用这个常量做一些过滤,比如滑动距离小于此值,我们就可以认为未达到滑动距离的临界值。
Ø VelocityTracker:速度追踪
用于追踪手指在滑动过程中的速度。它的使用过程很简单,首先,在View的onTouchEvent方法中追踪当前单击事件的速度:
通过以下方法获得当前的滑动速度:
要注意的是,要获得速度之前必须要计算,这里的速度是指手指滑动的像素数,1000表示时间间隔为1秒,取值:左负右正。
速度 = (终点位置 - 起点位置) / 时间段
使用完毕后需调用clear和recycle将其重置并回收。
当然,在ViewDragHelper中重写onViewReleased 方法直接获得了速度:
public void onViewReleased(ViewreleasedChild, float xvel, float yvel);
*ViewDragHelper解决了android中手势处理过于复杂的问题
本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View。
Ø GestureDetector:手势检测
用于辅助检测用户的单击、滑动、长按、双击等行为。
首先,创建GestureDetector对象并实现OnGestureListener接口(OnDoubleTapListener:监听双击)
接着,接管目标View的onTouchEvent方法,代码如下:
做完了上面两步,就可以有选择性的实现OnGestureListener和OnDoubleTapListener的方法了,两个接口的方法介绍:
方法名 |
描述 |
所属接口 |
onDown |
手指触摸屏幕的一瞬间: ACTION_DOWN |
OnGestureListener |
onShowPress |
手指轻轻触摸屏幕尚未松开或 拖动的状态 |
OnGestureListener |
onSingleTapUp |
手指松开,伴随着1个ACTION_UP而触发,这是单击行为 |
OnGestureListener |
onScroll |
手指按下屏幕并拖动,拖动行为 |
OnGestureListener |
onLongPress |
长久的按着屏幕不放,即长按 |
OnGestureListener |
onFling |
快速滑动后松开 |
OnGestureListener |
onSingleTapConfirmed |
严格的单击行为,注意与onSingleTapUp不同,如果触发了此方法,那么不可能在紧跟着另一个单击行为,即这只能是单击,不是双击中的一次单击 |
OnDoubleTapListener |
onDoubleTap |
双击,由两次连续的单击组成不可能和onSingleTapConfirmed方法共存 |
OnDoubleTapListener |
onDoubleTapEvent |
表示发生了双击行为,在双击的期间,ACTION_DOWN/MOVE/UP都会触发此回调 |
OnDoubleTapListener |
建议的GestureDetector使用场景:监听双击行为时可使用
Ø Scroller:弹性滑动
用于实现View的弹性滑动,我们都知道,scrollTo和scrollBy方法滑动时,其过程是瞬间完成的,体验不是很好,这个就可以使用Scroller可以实现过渡效果的滑动(在一定时间间隔内完成的)它本身无法让View弹性滑动,需要和View的computeScrllo方法配合才能完成,如下:
三种方式实现滑动:
Ø 第一种:通过View本身提供的scrollTo和scrollBy方法实现滑动
² scrollBy实际也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则是基于所传递参数的绝对滑动。
Ø 第二种:通过动画给View施加平移效果来实现。
² 主要操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画(属性动画为了兼容3.0以下,使用开源动画库:nineoldandroids)需要注意一点:View动画是对View影像做操作,并不是真正的改变View位置参数,如果希望动画后的状态得以保留,我们还必须将fillAfter设置为true。
Ø 第三种:通过改变View的LayoutParams使得View重新布局从而实现
² 如果想将一个Button向右平移100px,只需通过Button.getLayoutParams()得到对象,并且设置其leftMargin属性增加100px即可,最后别忘记button.requestLayout();或者直接setLayoutParams(params对象);
这是一种很灵活的做法。
针对上面的分析,得出结论:
l scrollTo和scrollBy:操作简单,适合对View内容的滑动
l 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
l 改变布局参数:操作稍微复杂,适用于有交互的View
所谓点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统将事件传递给一个具体的View,这个过程就是分发。点击事件的分发过程由三个重要的方法共同完成:
l dispatchTouchEvent
² 用来进行事件分发,如果事件能够传递给当前View,那此方法一定被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent影响,表示是否消耗当前事件。
l onInterceptTouchEvent
² 在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件。如果拦截,调用onTouchEvent方法
l onTouchEvent
² 在dispatchTouchEvent方法中调用,用来处理点击事件。
点击事件传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回的是true表示要拦截,接着就自然调它的onTouchEvent;
如果返回false,就表示不拦截并传递给它的子元素并调用子元素的dispatchTouchEvent,如此反复直到事件被最终处理。
这其中有一点要注意: 如果View设置了事件监听:OnTouchListener,那么其中的onTouch方法优先级比上述的第三个重要方法:onTouchEvent要高!就是说:onTouch返回true,那onTouchEvent方法将不会被调用了!
而我们平时经常设置的OnClickListener是在onTouchEvent中的,由此可见,我们平时用的优先级是最低的!即处于事件传递的尾端。
如果一个View不消耗这个事件,那么就不会再传递给它了,并且没有其他子View消耗,那这个事件最终会消失。
ViewGroup默认不拦截任何事件;View没有onInterceptTouchEvent方法,View的onTouchEvent事件默认返回true,消耗。
事件传递过程是由外向内的,即事件总是先传递给父元素。不过可以通
getParent().requestDisallowInterceptTouchEvent(true);////不让父拦截
在子元素中干预父元素的事件分发过程,但是ACTIVITY_DOWN事件除外。
View的滑动冲突:只要内外层同时可以滑动,就会产生冲突。例如ViewPager中嵌套横向ListView?
介绍View的三大流程之前必须要了解一些基本概念。ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是由ViewRoot来完成的。当Activity被创建后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将这个实现类与DecorView关联起来。
View的绘制流程是从ViewRoot的performTraversals方法开始的,他经过measure、layout和draw三个过程才能最终将一个View绘制出来。
关于DecorView和“测量方式”的介绍请看上次总结的文档:
主要是指measure(测量)、layout(布局)、draw(绘制)这三大流程
Ø Measure过程要分情况来看,如果是一个原始的View,通过measure方法就完成了测量过程;若是ViewGroup,除了完成自己的测量外,还要遍历去调用子元素的measure方法
² View的measure方法是final的,不能被重写,在方法中会调用onMeasure方法,它里面有两个重要的方法:setMeasureDimension()和getDeaultSize(),
直接继承View的自定义控件需要重写onMeasure并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。为什么呢?通过查看:getDeaultSize的源码就明白了(自己看吧)。
² ViewGroup和View不同的是:他是抽象类,没有重写onMeasure方法,但是他提供了一个measureChhildren方法会在遍历子View的时候调用,并调用其中measureChild
² 这里有个问题!:
我们想在Activity启动的时候获取某个View的宽高,不要觉得直接getXXX就可以的!事实上在OnCreate、Start、Resume中均无法正确的得到。因为View的measure过程和Activity的生命周期不是同步执行的,因此无法保证get之前测量过程完毕,那怎么办呢?!我们可以通过下面四种方式:
l Activity中重写onWindowFocusChanged()
含义为View初始化完毕了,需要注意的是,Activity得到焦点和失去焦点均会调用此方法,所以可能会被拼频繁调用。代码如下:
l view . post(runnable)
通过post可以将一个runnable投地到消息队列的尾部,然后等待Looper调用的时候,View也已经初始化好了:
l ViewTreeObserver
使用ViewTreeObserver的众多回调也可完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树发生改变时,onGlobalLayout方法将会被调用,伴随着View树的改变,此方法会被调用多次:
l view . measure(int w,int h)
通过手动对view进行测量来得到宽高,这种方式比较复杂,需要根据View的LayoutParams来分情况处理。
match_parent:无法测量
具体数值(dp/px):
wrap_content:理论上支持的最大值
Ø Layout:作用是ViewGroup用来确定子元素的位置,ViewGroup位置被确定后会在onLayout中遍历调用子元素的layout方法,比measure简单的多,只要记住layout确定view本身位置,而onLayout确定子View位置。
Layout中通过setFrame方法设置四个顶点位置
这里也有个问题!?:View的测量宽高和最终宽高有何区别?
也就是getMeasureWidth和getWidth的区别?
其实,测量宽高形成于measure过程,最终宽高形成于layout过程。
Ø Draw:这个过程就比较简单了,作用是将View绘制到屏幕上,绘制过程遵循如下几步:
² 绘制背景background . draw(canvas)
² 绘制自己(onDraw)
² 绘制children(dispatchDraw)
² 绘制装饰(onDrawScrollBars)
View的绘制过程的传递是通过dispatchDraw实现的!它会遍历子view的draw方法。
关于自定义View可大致分为四类:
Ø 继承View重写onDraw方法
Ø 继承ViewGroup派生特殊的layout
Ø 继承特定的View(比如TextView)
Ø 继承特定的ViewGroup(比如LinearLayout)
自定义view须知
1. 让View支持wrap_content
2. 如果有必要,让你的View支持padding
3. 尽量不要在View中使用Handler,没必要
4. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
5. View带有滑动嵌套情形时,妥善处理滑动冲突