Android View视图系统分析和Scroller和OverScroller分析

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的绘制原理,如果不清楚的话能够看一下这个的博客,我认为解说的非常到位
        当然,假设你不是非常理解也不是非常致命的问题。以下我会做一些解释。
    (一)非常核心的一点,首先我们要知道不论什么的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类中   

  1. /** 
  2.      * The offset, in pixels, by which the content of this view is scrolled 
  3.      * horizontally. 
  4.      * {@hide} 
  5.      */  
  6.     protected int mScrollX;   //该视图内容相当于视图起始坐标Layout的偏移量   , X轴 方向  
  7.     /** 
  8.      * The offset, in pixels, by which the content of this view is scrolled 
  9.      * vertically. 
  10.      * {@hide} 
  11.      */  
  12.     protected int mScrollY;   //该视图内容相当于视图起始坐标Layout的偏移量   , Y轴方向  
  13.   
  14.     /** 
  15.      * Return the scrolled left position of this view. This is the left edge of 
  16.      * the displayed part of your view. You do not need to draw any pixels 
  17.      * farther left, since those are outside of the frame of your view on 
  18.      * screen. 
  19.      * 
  20.      * @return The left edge of the displayed part of your view, in pixels. 
  21.      */  
  22.     public final int getScrollX() {  
  23.         return mScrollX;  
  24.     }  
  25.   
  26.     /** 
  27.      * Return the scrolled top position of this view. This is the top edge of 
  28.      * the displayed part of your view. You do not need to draw any pixels above 
  29.      * it, since those are outside of the frame of your view on screen. 
  30.      * 
  31.      * @return The top edge of the displayed part of your view, in pixels. 
  32.      */  
  33.     public final int getScrollY() {  
  34.         return mScrollY;  
  35.     }  

     那么,究竟什么叫做视图起始坐标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类中

  1. /** 
  2.  * Set the scrolled position of your view. This will cause a call to 
  3.  * {@link #onScrollChanged(int, int, int, int)} and the view will be 
  4.  * invalidated. 
  5.  * @param x the x position to scroll to 
  6.  * @param y the y position to scroll to 
  7.  */  
  8. public void scrollTo(int x, int y) {  
  9.     //偏移位置发生了改变  
  10.     if (mScrollX != x || mScrollY != y) {  
  11.         int oldX = mScrollX;  
  12.         int oldY = mScrollY;  
  13.         mScrollX = x;  //赋新值,保存当前廉价量  
  14.         mScrollY = y;  
  15.         //回调onScrollChanged方法  
  16.         onScrollChanged(mScrollX, mScrollY, oldX, oldY);  
  17.         if (!awakenScrollBars()) {  
  18.             invalidate();  //一般都引起重绘  
  19.         }  
  20.     }  
  21. }  

 

     public void scrollBy(int x, int y)    

            说明:在当前视图内容在Layout布局中继续偏移(x , y)个单位。

        方法原型为: View.java类中

  1. /** 
  2.    * Move the scrolled position of your view. This will cause a call to 
  3.    * {@link #onScrollChanged(int, int, int, int)} and the view will be 
  4.    * invalidated. 
  5.    * @param x the amount of pixels to scroll by horizontally 
  6.    * @param y the amount of pixels to scroll by vertically 
  7.    */  
  8.   // 看出原因了吧 。

     mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位  

  9.   public void scrollBy(int x, int y) {  
  10.       scrollTo(mScrollX + x, mScrollY + y);  
  11.   }  
      上面仅仅是用TextView来让大家充分理解内容的偏移。一般来讲这样做事实上并没有什么意义。那么ScrollTo和ScrollBy的强大之处在哪呢。

      举个样例来说,大家能够想象一下一个宽和高都是match_parent的ViewPager,假设说他当中有3页,那么我们非常明显受手机屏幕的限制,我们仅仅能看到第1页,那么假设我们对自己的ViewPager调用scrollBy。让他延X轴反向滚动屏幕的宽度,那么请问我们能够看到第几页。这个问题和代码都非常easy,这里我就不细说了。

大家自己想一想。



      无论了,说了这么多,我仅仅能默认大家对与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类中      

  1. /** 
  2.      * Called by a parent to request that a child update its values for mScrollX 
  3.      * and mScrollY if necessary. This will typically be done if the child is 
  4.      * animating a scroll using a {@link android.widget.Scroller Scroller} 
  5.      * object. 
  6.      */由父视图调用用来请求子视图依据偏移值 mScrollX,mScrollY又一次绘制  
  7.     public void computeScroll() { //空方法 。自己定义ViewGroup必须实现方法体  
  8.           
  9.     }  

          为了实现偏移控制,一般自己定义View/ViewGroup都须要重载该方法 。

 

     其调用过程位于View绘制流程draw()过程中,例如以下:

  1. @Override  
  2. protected void dispatchDraw(Canvas canvas){  
  3.     ...  
  4.       
  5.     for (int i = 0; i < count; i++) {  
  6.         final View child = children[getChildDrawingOrder(count, i)];  
  7.         if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  8.             more |= drawChild(canvas, child, drawingTime);  
  9.         }  
  10.     }  
  11. }  
  12. protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
  13.     ...  
  14.     child.computeScroll();  
  15.     ...  
  16. }  
     既然如今我们知道了:
      一、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(<span style="color: rgb(51, 51, 51); font-family: 宋体;font-size:18px; line-height: 30px; text-indent: 28px;">startX ,<span style="color: rgb(51, 51, 51); font-family: 宋体;font-size:18px; line-height: 30px; text-indent: 28px;">startY,dx,dy,duration</span></span>);
@Override
    public void computeScroll() {

        // 先推断mScroller滚动是否完毕
        if (mScroller.computeScrollOffset()) {
            // 这里调用View的scrollTo()完毕实际的滚动
            scrollTo( mScroller.getCurrX(), mScroller .getCurrY());
            // 必须调用该方法,否则不一定能看到滚动效果
            invalidate();
        }
        super.computeScroll();
    }

那么到了这里,基本上我们对于Scroller的基本使用应该就不是问题了。

  今天就到这里吧。下次再写个高于高级使用续篇吧。



原文地址:https://www.cnblogs.com/zsychanpin/p/6945371.html