一个滑屏Demo,所包含的知识点:
Android相关:
How to create a ViewGroup
Usage of Scroller
OnTouchEvent & onInterceptTouchEvent
Android无关:
判断是否换页
判断下页是否有效
细节问题(滑屏实现)
How to create a ViewGroup
一般要重载两个函数,而且是必须的:
onMeasure:超类的该方法,只会计算该View本身的默认大小,并不会计算其child的大小,如果不重载该方法,子类大小全为0,不会显示出来。
一般的方法为:for(i=0; i<getChildCount(); i++)
getChildAt(i).measure();
onLayout:该方法为抽象方法,说明ViewGroup并没有默认规定子视图的排列方式,必须其子类来定义其子视图在父视图里面的排列方式。
正确的重载了这两个方法,就可以正确的显示一个ViewGroup了。
Additional:
ViewGroup默认是不会回调onDraw()的。因为ViewGroup是个子视图的容器,本身并没有什么好画的,所以默认它是willNotDraw(),即它是不会执行draw命令的,也就不会回调onDraw函数。如果ViewGroup有背景色,即setBackground后,那么他将会执行onDraw回调。
Usage of Scroller
Scroller是个Helper类,仅仅是为了方便实现滑屏,没有这个类,照样可以实现滑屏,只是滑屏每个时刻的参数需要自己实现。
Scroller有两个重要方法:startScroll和computeScrollOffset
startScroll乍一看貌似掉了这个函数就能实现View的滑屏了,但是仔细想想,这个类的构造函数或者startScroll方法都没有传入要滑动的View,那Scroller怎么知道要滑哪个View呢。于是一开始我直接在ViewGroup内部构造了一个Scroller,然后startScroll->invalidate(),然后Nothing happens。之后看了Scroller代码实现后,才发现Scroller只是帮你实现了View从A点到B点滑动过程中所有的XY坐标。startScroll表示开始计算坐标,然后用computeScrollOffset获取计算出的坐标,在滑屏动画过程中,随着时间的流逝,每次获得的坐标不同,这一连串坐标连起来,便形成了动画,那么何时调用computeScrollOffset获取坐标呢?android View架构中已经对滑屏进行了支持,每次draw的过程中都会回调computeScroll。computeScroll回调用来通知View更新自己的滑屏偏移位置,即mScrollX,mScrollY。默认是回调为空,即对偏移位置不做调整。接下来对mScrollX,mScrollY进行简单介绍,android View架构本身支持View的滑动,于是有了mScrollX,mScrollY这两个变量,使用ScrollTo和ScrollBy方法对View进行滑动,默认mScrollX,mScrollY为0,表示在draw的时候,该View不进行任何偏移。该在哪就在哪。如果使用了ScrollBy和ScrollTo那么mScrollX,mScrollY将会发生改变,在下一次draw的时候该View就会进行相应偏移。ScrollBy和ScrollTo不会产生动画,即时生效,简而言之方法只要执行两个操作,对mScrollX,mScrollY进行赋值,调用invaildate()。
于是事件驱动发生滑屏动画实现大致为这样
//某事件触发该方法
Void Scroll(){
//横向滑动,startX为起始位置,dx为偏移量,1000表示整个过程耗时1000毫秒
mScroller.startScroll(startX,0, dx, 0,1000);
//触发重绘
invalidate();
}
//View的回调
Void computeScroll(){
如果if返回true表示动画还没结束,获取偏移量
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
对invalidate的一些说明,invalidate是个异步调用过程,即调用invalidate不会立即进行重绘,它只是发送一个重绘请求,所以在onDraw里面调用invalidate不会造成onDraw的无限递归调用.只是会在UI线程的消息池里面不断产生重绘请求,不断产生重绘请求+每次View的属性参数不同,就造成了动画效果
OnTouchEvent & onInterceptTouchEvent
要进行手势滑屏,那么就必须重载OnTouchEvent,在按下的时候记录位置,更新状态,在Move的时候,ScrollBy与上次距离的偏移量。在Up的时候,判断是否换页,然后就走startScroll的流程
onInterceptTouchEvent对于一个最基本的手势滑屏应该说是不必要的,重载该方法的主要意义在于:当手指按在其子视图上面时截获其触屏消息,并转发到父视图的OnTouchEvent。但是对于完整的手势滑屏应该说是不可或缺的,因为你不能让用户点在空白区域时可以滑动,而点在子视图上面却无法滑动,特别是当父视图的子视图相当多时(如桌面),onInterceptTouchEvent就显得相当必要了。
判断是否换页
目前有两种算法:一种是根据按下去的位置和手指离开屏幕位置的差值来决定是否可以换页,另外一种是更主流的使用速度追踪器(VelocityTracker)来根据滑动时的速度和距离来判断。相对来说,第二种算法会有更好的用户体验。而第一种方法更加基本,对于描述滑屏更加容易理解。
判断下页是否有效
这个非常简单,每一个子视图都是一个Page,所以当要滑过去的那个视图,不在父视图的子节点范围内,就是个无效页
private boolean isVaildScreen(int which){
intcount = getChildCount();
if(which> count-1 || which < 0){
returnfalse;
}
returntrue;
}
细节问题(滑屏实现)
完成了android相关的三个技术点之后,基本就可以搭出手势滑屏的基本雏形。但是这只是个半成品。在细节方面还有很多地方需要完善的:
比如在每次动画滑屏前都需要进行前一次动画的取消。
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
在onTouchEvent的Move事件:需要避免微距的滑动,因为这样会让界面看上去会发飘一样。可以增加这次坐标和上次坐标的差值比较,如果大于一个阀值那么才进行滑动
If(Math.abs(x-mLastX) >ViewConfiguration.getTouchSlop())
还有一个关键点,就是手势滑和动画滑的配合,计算startX和dx