Android View视图系统分析和Scroller和OverScroller分析
View 视图分析
首先,我们知道,在Android中所有的视图资源(不管是Layout还是View),最终的父类都是View类。各式各样的Layout只是对ViewGroup的一中特别的实现,各种View也只是View的特别实现。而ViewGroup也是对于View的一种实现。所以说所有的View元素在根本上都是一样的。当然这并不等于说View == ViewGroup,就好比只有ViewGroup才能够addView。
当然这里我们的核心并不是要讲述View与ViewGroup的区别。但是为什么还是要提到所有的View子元素都是一种View呢。接下来我们首先需要分析一下Android 中的View视图系统。
假设在这里我们每一个人都清楚了View的绘制原理,如果不清楚的话可以看一下这个的博客,我觉得讲解的很到位
http://blog.csdn.net/qinjuning
当然,如果你不是很理解也不是很致命的问题,下面我会做一些解释。
(一)很核心的一点,首先我们要知道任何的View要显示在我们的屏幕上面都要经过 Measure 和 Layout 2个过程
(二)我们需要充分理解到Canvas的含义
假设你对于上面我提到的2点有一定程度的理解话,接下来的内容应该就很好理解了。
做过JAVA绘图编程的人都知道Canvas(画布)的概念,这里,我就不买关子了,我们只需要知道我们所有的视图元素都会在我们的Canvas上呈现。
接下来,我先重点分析一下Android中的视图坐标。
首先,我们需要知道的事在Android或者是JAVA中Canvas是没有边界的,为什么说是没有边界的呢,在我之前的一篇博文中自定义特效VIew第一弹之竖直TextView,我给大家做了一个竖直TextView,在那片博文里面我详细的阐述了Canvas的概念,如果不理解的话可以转回去看一看。或者大家可以这样理解,反证法,如果我们的Canvas是有边界的,那么当我们绘制元素时越界了怎么办,报错?程序终止运行?所以说,我们的Canvas是没有边界的。这样理解下来我们就明白了Canvas是没有边界的,也就是说我们放置我们的View子元素是不会有任何的限制的。同理,这也就解释了我们队Canvas采取translate(平移)、clipRect(剪切)一系列变换的意义所在。那么Canvas没有边界,是不是就意味着我们的View资源一定都能如我们所愿吗。
其实不然,Canvas是没有边界的所以我们可以任意的放置我们的View子元素(注意这个放置的含义),但是他们一定都会被我们看到吗,其实不然,这就涉及到了我们的Layout坐标了。Canvas是没有边界的,但是我们的Layout区域确实有边界的,如果,我们希望看到我们的View子元素,那么他就必须在我们的视图坐标(可视区域),但是在此处还要注意Layout区域并不一定都可见。
下面我用一张更直观的图标来和大家解释一下吧。
开启你手机之中的开发者选项中的显示布局边界就可以看到这个效果图了。我相信在你第一眼看到这张图片的时候你就可以瞬间的对于Layout坐标(视图区域)有了一个认识了。没错,你所看到的那些蓝角红线组成的长方形区域就是我们的布局区域,那么他们的框定是源自于哪里呢。这里还记得我上面强调过的
一)很核心的一点,首先我们要知道任何的View要显示在我们的屏幕上面都要经过 Measure 和 Layout 2个过程
是的,没错你猜对了,他们源自于Layout(Layout框定了他们的显示位置和大小)。
这里就解释了我们为什么网上可以通过
LastCallMessageContainer.layout(left, top, left+ LastCallMessageContainer.getWidth(), top+ LastCallMessageContainer.getHeight());
这种方式来实现跟随手指移动的View,但是这种方式好不好呢,答案很明显,并不好,有机会的话给打击分享一下实现方式吧。
在这里,我还要继续啰嗦一下,大家必须要有的概念,是不是我们的Layout区域都一定可见呢,答案是否定的,所以说这里我们还会有第三个区域(可视区域可视坐标),那么什么是可视区域,可视坐标呢,其实说白了就是你的手机屏幕坐标,所以说我们希望见到我们的视图元素,必须满足:
一,虽然Canvas是没有边界的,但是Layout区域是有边界的,所以我们的视图元素需要在我们的Layout区域中
二,Layout区域必须在可视区域内(手机屏幕)。
scrollTo()和scrollBy()分析
好的,我们继续回归正题,假定在这里每一个人对于Canvas和Layout都有了足够的认识。
那么问题来了,我们可以想象一下:
一、我们部分子元素的Layout区域在可视区域之外,我们想看见他怎么办?
二、虽然我们能够看到子元素,但是有一些我们不想看到他或者想看见其他看不见的又怎么办呢?
是不是觉得很绕,但是没办法就是这么绕,对于第一个问题,大家可以想象一下,如果让你来设计ViewPager,你觉得你会怎么设计。对于第二个问题一般就会用的很多的,比如说拖拽View效果。
那么,现在很明显了,我们希望做到以上两点,谁可以帮助我们 ,何以解忧 ,唯有ScrollTo和ScrollBy
那么要说ScrollTo和ScrollBy,我们必须先说的事mScrollX和mScrollY
在View.java中提供了了如下两个变量以及相应的属性方法去读取滚动值 ,如下: View.java类中
-
-
-
-
-
- protected int mScrollX;
-
-
-
-
-
- protected int mScrollY;
-
-
-
-
-
-
-
-
-
- public final int getScrollX() {
- return mScrollX;
- }
-
-
-
-
-
-
-
-
- public final int getScrollY() {
- return mScrollY;
- }
那么,到底什么叫做视图起始坐标Layout的偏移量呢,那么我们返回去,就最上面那个图片你觉得我们的TextView的mScrollX 和mScrollY 为多少呢,答案是0,看一下你能不能有所理解。简而言之就是说TextView的text(文本内容)在Layout布局中的坐标为(0,0),TextView的text(内容文本)偏移量也为0.
通过上下这两张图片,我相信大家对与mScrollX 和mScrollY有一个初步的认识了吧,代码我就不贴了,太简单了。Hello Word后面的图案是TextView的背景。
这时,我相信大家对与视图起始坐标Layout的偏移量应该是有一定的认识了,这里顺带要提一下的内容偏移量对背景无效。比如我们上图TextView的背景就没有一起移动。
public void scrollTo(int x, int y)
说明:在当前视图内容偏移至(x , y)坐标处,即位于Layout区域(x , y)坐标处。
方法原型为: View.java类中
-
-
-
-
-
-
-
- public void scrollTo(int x, int y) {
-
- if (mScrollX != x || mScrollY != y) {
- int oldX = mScrollX;
- int oldY = mScrollY;
- mScrollX = x;
- mScrollY = y;
-
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- if (!awakenScrollBars()) {
- invalidate();
- }
- }
- }
public void scrollBy(int x, int y)
说明:在当前视图内容在Layout布局中继续偏移(x , y)个单位。
方法原型为: View.java类中
-
-
-
-
-
-
-
-
- public void scrollBy(int x, int y) {
- scrollTo(mScrollX + x, mScrollY + y);
- }
上面只是用TextView来让大家充分理解内容的偏移。一般来讲这样做其实并没有什么意义。那么ScrollTo和ScrollBy的强大之处在哪呢。
举个例子来说,大家可以想象一下一个宽和高都是match_parent的ViewPager,如果说他其中有3页,那么我们很明显受手机屏幕的限制,我们只能看到第1页,那么如果我们对自己的ViewPager调用scrollBy,让他延X轴反向滚动屏幕的宽度,那么请问我们可以看到第几页。这个问题和代码都很简单,这里我就不细说了。大家自己想一想。
不管了,说了这么多,我只能默认大家对与mScrollX 和mScrollY和ScrollTo和ScrollBy有了足够的认识了。
Scroller 和OverScroller分析
我们知道想把一个View的内容偏移至指定坐标(x,y)处,利用scrollTo()方法直接调用就OK了,但我们不能忽视的是,该方法本身来的的副作用:非常迅速的将View/ViewGroup偏移至目标点,而没有对这个偏移过程有任何控制,对用户而言可能是不太友好的。于是,基于这种偏移控制,Scroller类被设计出来了,该类的主要作用是为偏移过程制定一定的控制流程(后面我们会知道的更多),从而使偏移更流畅,更完美。
可能上面说的比较悬乎,道理也没有讲透。下面我就根据特定情景帮助大家分析下:
情景: 从上海如何到武汉?
普通的人可能会想,so easy : 飞机、轮船、11路公交车...
文艺的人可能会想, 小 case : 时空忍术(火影的招数)、翻个筋斗(孙大圣的招数)...
不管怎么样,我们想出来的套路可能有两种:
1、有个时间控制过程才能抵达(缓慢的前进) ----- 对应于Scroller的作用
假设做火车,这个过程可能包括: 火车速率,花费周期等;
2、瞬间移动(超神太快了,都眩晕了,用户体验不太好) ------ 对应于scrollTo()的作用
在这里,我必须第一时间强调一下:Scrollers并不是控制View进行滚动,包括内容或者是位置,实际上,Scrollers只是一个控件移动轨迹的辅助计算类,如果你想滚,他能帮你计算什么时间应该滚到什么位置,但是滚不滚,全靠你自觉~所以说,滚动位置由Scrollers计算出来了,我们在什么时候滚呢?滚多少呢?这时候,就要View的一个回调函数computeScroll()出马了。
我们看看View里面的computeScroll()做了些什么
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
public void computeScroll() {
}
空的,意思很明显,自己添加。
computeScroll()方法介绍
为了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。
computeScroll()方法原型如下,该方法位于ViewGroup.java类中
-
-
-
-
-
- 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
- public void computeScroll() {
-
- }
为了实现偏移控制,一般自定义View/ViewGroup都需要重载该方法 。
其调用过程位于View绘制流程draw()过程中,如下:
- @Override
- protected void dispatchDraw(Canvas canvas){
- ...
-
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- ...
- child.computeScroll();
- ...
- }
既然现在我们知道了:
一、Scroller只是一个滚动计算辅助类。并不能实现滚动要求。
二、我们的滚动要在computeScroll中自己去写。
那么,我们应该怎么开启滚动呢。下面我们就来系统的介绍一下我们的Scroller 和 OverScroller。
其实说到这里,有的同学可能比较迷惑,OverScroller和Scroller有什么区别呢?事实上,这两个类都属于Scrollers,Scroller出现的比较早,在API1就有了,OverScroller是在API9才添加上的,出现的比较晚,所以功能比较完善,Over的意思就是超出,即OverScroller提供了对超出滑动边界的情况的处理,这两个类80%的API是一致的,OverScroller比Scroller添加了一下几个方法
☞ isOverScrolled()
☞ springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
☞ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)
☞ notifyHorizontalEdgeReached(int startX, int finalX, int overX)
☞ notifyVerticalEdgeReached(int startY, int finalY, int overY)
从名字也能看出来,都是对Over功能的支持,其他的API都一样,所以介绍通用API的时候,并不区分OverScroller和Scroller。
下面简单介绍一下常用的API。
☞ startScroll(int startX, int startY, int dx, int dy)
☞ startScroll(int startX, int startY, int dx, int dy,int duration)
核心用来开启滚动的方法:
参数
startX 滚动起始值
startY 滚动起始值
dx 水平方向滑动的距离,正值会使滚动向左滚动
dy 垂直方向滑动的距离,正值会使滚动向上滚动
duration 滚动持续时间,以毫秒计。缺省值250ms作为持续时间。
☞ computeScrollOffset() 这个函数很核心,表面上来看他只是用来判断我们的滚动是否结束
if (mScroller.computeScrollOffset())
看起来似乎没有什么价值,起始不然
//根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public boolean computeScrollOffset() {
if (mFinished) { //已经完成了本次动画控制,直接返回为false
return false;
}
.......
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
.......
}
这就是为什么我们称Scroller为滚动的助手类,原来如此,我们在startScroll中设好初始值 、滚动距离、滚动时间。那么
computeScrollOffset会帮我们计算出在特定的时间内应该滚动在什么地方,到时候我们只要通过getCurrX()和getCurrY()得到就可以了
☞ getCurrX() 这个就是获取当前滑动的坐标值,因为Scrollers只是一个辅助计算类,所以如果我们想获取滑动时的时时坐标,就可以通过这个方法获得,然后在computeScroll()里面调用
☞ getFinalX() 这个是用来获取最终滑动停止时的坐标
☞ isFinished() 用来判断当前滚动是否结束
那么 ,到了这里我们就一目了然,Scroller的基本使用方法不外乎:
mScroller.startScroll(startX ,startY,dx,dy,duration);
@Override
public void computeScroll() {
// 先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
// 这里调用View的scrollTo()完成实际的滚动
scrollTo( mScroller.getCurrX(), mScroller .getCurrY());
// 必须调用该方法,否则不一定能看到滚动效果
invalidate();
}
super.computeScroll();
}
那么到了这里,基本上我们对于Scroller的基本使用应该就不是问题了。
今天就到这里吧,下次再写个高于高级使用续篇吧。