Scroller的调用过程以及View的重绘

背景

这是一个滑动帮助类,并不可以使View真正的滑动,而是根据时间的流逝,获取插值器中的数据,传递给我们,让我们去配合scrollTo/scrollBy去让view产生缓慢滑动,产生动画的效果,其实是和属性动画同一个原理。下面是官方文档对于这个类所给的解释:

This class encapsulates scrolling. You can use scrollers (Scroller or OverScroller) to collect the data you need to produce a scrolling animation—for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view. It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

一.scroller的绘制过程:

  1. 调用public void startScroll(int startX, int startY, int dx, int dy)
    该方法为scroll做一些准备工作.
    比如设置了移动的起始坐标,滑动的距离和方向以及持续时间等.
    该方法并不是真正的滑动scroll的开始,感觉叫prepareScroll()更贴切些.

  2. 调用invalidate()或者postInvalidate()使View(ViewGroup)树重绘
    重绘会调用View的draw()方法
    draw()一共有六步:

  3. 绘制背景

  4. 保存画布

  5. 调用onDraw()绘制内容

  6. 去调用dispatchDraw()绘制子View

  7. If necessary, draw the fading edges and restore layers

  8. Draw decorations (scrollbars for instance)

其中最重要的是第三步和第四步,重绘分两种情况:
2.1 . ViewGroup的重绘
在完成第三步onDraw()以后,进入第四步ViewGroup重写了
父类View的dispatchDraw()绘制子View,于是这样继续调用:
dispatchDraw()-->drawChild()-->child.computeScroll();

2.2 .View的重绘

当View调用invalidate()方法时,会导致整个View树进行从上至下的一次重绘.比如从最外层的Layout到里层的Layout,直到每个子View.在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及onDraw()方法。
当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.看到这里就明白了:当这个子View的父容器重绘时,也会调用上面提到的线路:onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
于是子View(比如此处举例的ButtonSubClass类)中重写的computeScroll()方法就会被调用到.

3.** View树的重绘会调用到View中的computeScroll()方法**

4.** 在computeScroll()方法中,在View的源码中可以看到public void computeScroll(){}是一个空方法. 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy()来实现移动.该方法才是实现移动的核心.**
4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成
注意:该方法是Scroller中的方法而不是View中的

       public boolean computeScrollOffset(){
         Call this when you want to know the new location.
         If it returns true,the animation is not yet finished.  
         loc will be altered to provide the new location.
     }
   

返回true时表示还移动还没有完成.
4.2 若动画没有结束,则调用:scrollTo(By)();使其滑动scrolling

5.再次调用invalidate()
调用invalidate()方法那么又会重绘View树.
从而跳转到第3步,如此循环,直到computeScrollOffset返回false

二.onMeasure、onLayout、draw 关系

onMeasure()方法
onMeasure(int widthMeasureSpec,int heightMeasureSpec)
1、调用时间:当控件的父元素放置该控件时,用于告诉父元素该控件需要的大小。
2、传入参数:widthMeasureSpec,heightMeasureSpec。这两个传入参数由高32位和低16位组成,高32位保存的值叫specMode,可以通过MeasureSpec.getMode()获取;低16位为specSize可以由MeasureSpec.getSize()获取。这两个值是由ViewGroup中的layout_width,layout_height和padding以及View自身的layout_margin共同决定。权值weight也是尤其需要考虑的因素,有它的存在情况可能会稍微复杂点。

specMode可以取三个值:MeasureSpec.EXACTLY ,MeasureSpec.AT_MOST,MeasureSpec.UNSPECIFIED;specMode与layout_的对应关系如下:

match_parent - MeasureSpec.EXACTLY:当layout_为match_parent或者为某一具体值的时候specMode为EXACTLY代表精确的值;

wrap_content - MeasureSpec.AT_MOST:表示能获得的最大尺寸;
当无法确定尺寸的时候则是 MeasureSpec.UNSPECIFIED,这时候specSize会为最小值(即0);

3、可以在onMeasure()中来计算控件的尺寸,然后根据setMeasuredDimension(mWidth,mHeight);方法来告诉父控件此控件需要的尺寸,onMeasure()方法中必须调用此方法。

4、值得注意的是:
1)specSize和传入setMeasuredDimension()方法中的值的单位都是px(dp*density就是px)。2)match_parent并不是填充整个父容器,而是在不覆盖已经加入父容器的控件的情况下填充父容器。

onLayout()方法
onLayout(boolean changed, int left, int top,int right,int bottom);
父容器的onLayout()调用子类的onLayout()来确定子view在viewGroup中的位置,如:onLayout(10,10,100,100)表示子容器在父容器中(10,10)位置显示,长、宽都是90。结合onMeasure()方法使用可以确定子view的布局。

onDraw()方法
onDraw(Canvas canvas)
自定义view的关键方法,用于绘制界面,可以重写此方法以绘制自定义View。

onMeasure 属于View的方法,用来测量自己和内容的来确定宽度和高度 ,view的measure方法体中会调用onMeasure。
onLayout属于ViewGroup的方法,用来为当前ViewGroup的子元素分配位置和大小 View的layout方法体中会调用onLayout。
onMeasure和onLayout, onMeasure在onLayout之前调用。
设置background后,会重新调用onMeasure和onLayout,onMeasure测量子VIEW大小后调用LAYOUT布局 所以初始化的时候会多次调用onlayout方法

实例:

ublic class MultiViewGroup extends ViewGroup {  
  
     private VelocityTracker mVelocityTracker; // 用于判断甩动手势  
    private static final int SNAP_VELOCITY = 600; // X轴速度基值,大于该值时进行切换  
    private Scroller mScroller;// 滑动控制  
    private int mCurScreen; // 当前页面为第几屏  
    private int mDefaultScreen = 0;  
    private float mLastMotionX;// 记住上次触摸屏的位置  
    private int deltaX;  
  
    private OnViewChangeListener mOnViewChangeListener;  
  
    public MultiViewGroup(Context context) {  
        this(context, null);  
    }  
  
    public MultiViewGroup(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        init(getContext());  
    }  
  
    private void init(Context context) {  
        mScroller = new Scroller(context);  
        mCurScreen = mDefaultScreen;  
    }  
  
    @Override  
    public void computeScroll() {  
        if (mScroller.computeScrollOffset()) {// 会更新Scroller中的当前x,y位置  
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
            postInvalidate();  
        }  
    }  
  
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        int width = MeasureSpec.getSize(widthMeasureSpec);  
        int count = getChildCount();  
        for (int i = 0; i < count; i++) {  
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);  
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);  
        }  
        scrollTo(mCurScreen * width, 0);// 移动到第一页位置  
    }  
  
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        int margeLeft = 0;  
        int size = getChildCount();  
        for (int i = 0; i < size; i++) {  
            View view = getChildAt(i);  
            if (view.getVisibility() != View.GONE) {  
                int childWidth = view.getMeasuredWidth();  
                // 将内部子孩子横排排列  
                view.layout(margeLeft, 0, margeLeft + childWidth,  
                        view.getMeasuredHeight());  
                margeLeft += childWidth;  
            }  
        }  
    }  
  
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        int action = event.getAction();  
        float x = event.getX();  
        switch (action) {  
        case MotionEvent.ACTION_DOWN:  
            obtainVelocityTracker(event);  
            if (!mScroller.isFinished()) {  
                mScroller.abortAnimation();  
            }  
            mLastMotionX = x;  
            break;  
        case MotionEvent.ACTION_MOVE:  
            deltaX = (int) (mLastMotionX - x);  
            if (canMoveDis(deltaX)) {  
                obtainVelocityTracker(event);  
                mLastMotionX = x;  
                // 正向或者负向移动,屏幕跟随手指移动  
                scrollBy(deltaX, 0);  
            }  
            break;  
        case MotionEvent.ACTION_UP:  
        case MotionEvent.ACTION_CANCEL:  
            // 当手指离开屏幕时,记录下mVelocityTracker的记录,并取得X轴滑动速度  
            obtainVelocityTracker(event);  
            mVelocityTracker.computeCurrentVelocity(1000);  
            float velocityX = mVelocityTracker.getXVelocity();  
            // 当X轴滑动速度大于SNAP_VELOCITY  
            // velocityX为正值说明手指向右滑动,为负值说明手指向左滑动  
            if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {  
                // Fling enough to move left  
                snapToScreen(mCurScreen - 1);  
            } else if (velocityX < -SNAP_VELOCITY  
                    && mCurScreen < getChildCount() - 1) {  
                // Fling enough to move right  
                snapToScreen(mCurScreen + 1);  
            } else {  
                snapToDestination();  
            }  
            releaseVelocityTracker();  
            break;  
        }  
        // super.onTouchEvent(event);  
        return true;// 这里一定要返回true,不然只接受down  
    }  
  
    /** 
     * 边界检测 
     *  
     * @param deltaX 
     * @return 
     */  
    private boolean canMoveDis(int deltaX) {  
        int scrollX = getScrollX();  
        // deltaX<0说明手指向右划  
        if (deltaX < 0) {  
            if (scrollX <= 0) {  
                return false;  
            } else if (deltaX + scrollX < 0) {  
                scrollTo(0, 0);  
                return false;  
            }  
        }  
        // deltaX>0说明手指向左划  
        int leftX = (getChildCount() - 1) * getWidth();  
        if (deltaX > 0) {  
            if (scrollX >= leftX) {  
                return false;  
            } else if (scrollX + deltaX > leftX) {  
                scrollTo(leftX, 0);  
                return false;  
            }  
        }  
        return true;  
    }  
  
    /** 
     * 使屏幕移动到第whichScreen+1屏 
     *  
     * @param whichScreen 
     */  
    public void snapToScreen(int whichScreen) {  
        int scrollX = getScrollX();  
        if (scrollX != (whichScreen * getWidth())) {  
            int delta = whichScreen * getWidth() - scrollX;  
            mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 2);  
            mCurScreen = whichScreen;  
            invalidate();  
            if (mOnViewChangeListener != null) {  
                mOnViewChangeListener.OnViewChange(mCurScreen);  
            }  
        }  
    }  
  
    /** 
     * 当不需要滑动时,会调用该方法 
     */  
    private void snapToDestination() {  
        int screenWidth = getWidth();  
        int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;  
        snapToScreen(whichScreen);  
    }  
  
    private void obtainVelocityTracker(MotionEvent event) {  
        if (mVelocityTracker == null) {  
            mVelocityTracker = VelocityTracker.obtain();  
        }  
        mVelocityTracker.addMovement(event);  
    }  
  
    private void releaseVelocityTracker() {  
        if (mVelocityTracker != null) {  
            mVelocityTracker.recycle();  
            mVelocityTracker = null;  
        }  
    }  
  
    public void SetOnViewChangeListener(OnViewChangeListener listener) {  
        mOnViewChangeListener = listener;  
    }  
  
    public interface OnViewChangeListener {  
        public void OnViewChange(int page);  
    }  
}  
总结:

Scroller执行流程里面的三个核心方法

  mScroller.startScroll()
  mScroller.computeScrollOffset()
  view.computeScroll()
  1. mScroller.startScroll()中为滑动做了一些初始化准备.
    比如:起始坐标,滑动的距离和方向以及持续时间(有默认值)等.
    其实除了这些,在该方法内还做了些其他事情:
    比较重要的一点是设置了动画开始时间.

  2. computeScrollOffset()方法主要是根据当前已经消逝的时间
    来计算当前的坐标点并且保存在mCurrX和mCurrY值中。
    因为在mScroller.startScroll()中设置了动画时间,那么在computeScrollOffset()方法中依据已经消逝的时间就很容易得到当前时刻应该所处的位置并将其保存在变量mCurrX和mCurrY中。除此之外该方法还可判断动画是否已经结束。

@Override
    public void computeScroll() {
       super.computeScroll();
       if (mScroller.computeScrollOffset()) {
           scrollTo(mScroller.getCurrX(), 0);
           invalidate();
       }
    }

先执行mScroller.computeScrollOffset()判断了滑动是否结束
2.1 返回false,滑动已经结束.
2.2 返回true,滑动还没有结束.
并且在该方法内部也计算了最新的坐标值mCurrX和mCurrY.
就是说在当前时刻应该滑动到哪里了.
既然computeScrollOffset()如此贴心,盛情难却啊!
于是我们就覆写View的computeScroll()方法,
调用scrollTo(By)滑动到那里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容