Android 浅谈scrollTo和scrollBy源码

来源:互联网 发布:乐乎青年公寓官网 编辑:程序博客网 时间:2024/06/05 02:26

一.写在前面

在View的几种移动方法中我相信Scorller+scrollTo或者scrollBy是大家比较接受.我们再使用的时候总是会碰到一些奇怪的问题,可以得出以下几点:

  • scrollTo和scrollBy只是移动自己的内容.
    也就是如果ViewGroup设置scrollTo或者scrollBy的话,只有它的子View会有位移效果.如果是TextView设置scrollTo或者scrollBy的话只会让它内部的文字发生位移.

  • scrollBy还是调用的scrollTo,但scrollBy的起始坐标是相对于上次结束时的mScrollX和mScrollY.scroolTo的起始坐标是相对于父布局的左上角,之后起始坐标是不会变的.如下代码,Button:scroolto的内容只能发生一次位移.Button:scroolby的内容可以多次位移.

Button scroolto,scroolby; scroolto = findViewById(R.id.scroolto); scroolby = findViewById(R.id.scroolby);item.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                scroolto.scrollTo(-100,-100);                scroolby.scrollBy(-100,-100);            }        });
  • 当ViewGroup设置scrollTo或者scrollBy的时候,它的子View发生位移但是子View的getX()和getY()是不会发生变化的.子View的位于屏幕中的位置会发生变化,如下代码:
        //获取位于屏幕中的位置        int[] location = new int[2];        scroolto.getLocationOnScreen(location);        Log.e("WANG","ScreenX"+location[0]);        Log.e("WANG","ScreenY"+location[1]);        //获取位于父布局中的位置        Log.e("WANG","ViewX"+scroolto.getX());        Log.e("WANG","ViewY"+scroolto.getY());
  • 最后一点就是Android中View视图是没有边界的,Canvas是没有边界的.

这些就是小编大概的了解了,可能不太全面,希望读者们能指点一二.接下来我们会有个疑问,为什么再使用scrollTo(100,100)或者scrollBy(100,100)的时候,我们的内容是往负方向移动的呢?设置成-100,-100的时候是往正方向移动的呢,大家可以去看一下Android坐标系?

这里呢我们要知道,控件的x和y坐标的增值是正值的话就是往正方向移动,负值就是往负方向移动.这是一般的规则.然后scrollTo和scrollBy却是例外.我们之后去看下源码了,这里呢会涉及到View的绘制过程,也就是从Activity的setContentView()到View出现在手机屏幕中的过程.这里就大概说一下,下一篇会详细的介绍.

二.源码分析

 /**     * 为你的View设置滚动的位置,这里会调用{@link #onScrollChanged(int, int, int, int)} 随后这个View将调用失效.     * @param x the x position to scroll to     * @param y the y position to scroll to     */public void scrollTo(int x, int y) {//这里是不是就可以解释为什么scrollTo只能调用一次.        if (mScrollX != x || mScrollY != y) {            int oldX = mScrollX;            int oldY = mScrollY;            mScrollX = x;            mScrollY = y;            invalidateParentCaches();            //我们可以监听到的方法            onScrollChanged(mScrollX, mScrollY, oldX, oldY);            //这里有个判断的条件,也就是postInvalidateOnAnimation这个方法无论scrollbars是否开始绘制都会执行.            if (!awakenScrollBars()) {                postInvalidateOnAnimation();            }        }    }

这里面的awakenScrollBars()方法呢是去唤醒ScrollBar的绘制.任何View都是有ScrollBar的.scrollBar执行绘制流程的时候该方法会返回true,这样的话将不会执行scrollTo里面的postInvalidateOnAnimation方法,如何返回fasle则相反.再awakenScrollBars()里面也会调用到postInvalidateOnAnimation这个方法.我们来看下源码.

 /**     * <p>     * 触发scrollbars去绘制.当调用这个方法的时候将开启一个延迟动画去隐藏scrollbars,如果有个子类提供了滑动的动画,那么这个延迟的时间要和子类动画执行的实行进行对比.     * </p>     *     * <p>     * 只有在scrollbars可用的时候才会开启动画.调用 {@link #isHorizontalScrollBarEnabled()} 和     * {@link #isVerticalScrollBarEnabled()}.     当动画执行的时候该方法会返回true, 其他情况将返回false. 如何动画执行将调用到invalidate()方法区重绘.     * @see #scrollBy(int, int)     * @see #scrollTo(int, int)     * @see #isHorizontalScrollBarEnabled()     * @see #isVerticalScrollBarEnabled()     * @see #setHorizontalScrollBarEnabled(boolean)     * @see #setVerticalScrollBarEnabled(boolean)     */    protected boolean awakenScrollBars(int startDelay, boolean invalidate) {        final ScrollabilityCache scrollCache = mScrollCache;        if (scrollCache == null || !scrollCache.fadeScrollBars) {            return false;        }        if (scrollCache.scrollBar == null) {        //初始化了绘制scrollBar所需的一些参数,这里没初始化的时候再draw(canvas方法里面将不会开启绘制流程)            scrollCache.scrollBar = new ScrollBarDrawable();            scrollCache.scrollBar.setState(getDrawableState());            scrollCache.scrollBar.setCallback(this);        }        if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) {        //这里就是调用重新绘制的方法.invalidate的值为true.            if (invalidate) {                // Invalidate to show the scrollbars                postInvalidateOnAnimation();            }            if (scrollCache.state == ScrollabilityCache.OFF) {                // FIXME: this is copied from WindowManagerService.                // We should get this value from the system when it                // is possible to do so.                final int KEY_REPEAT_FIRST_DELAY = 750;                startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);            }            //开启动画            long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;            scrollCache.fadeStartTime = fadeStartTime;            scrollCache.state = ScrollabilityCache.ON;            // Schedule our fader to run, unscheduling any old ones first            if (mAttachInfo != null) {                mAttachInfo.mHandler.removeCallbacks(scrollCache);                mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);            }            return true;        }        return false;    }

一切的结果都将调用到postInvalidateOnAnimation()那我们就跟着去看下该方法.

    public void postInvalidateOnAnimation() {        // We try only with the AttachInfo because there's no point in invalidating        // if we are not attached to our window        final AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);        }    }

attachInfo再View跟window连接上的时候就开始赋值,这里是不为null的,然后将调用mViewRootImpl里面的方法.我们先来看下ViewRootImpl类.

/** * 在试图层级的顶层, 实现了View和Windowmanage质检需要的协议,内部实现的细节要去看这个类{@link WindowManagerGlobal}. */@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})public final class ViewRootImpl implements ViewParent,        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {

对于该类的定义我们再下节详细讲解.该类呢实现了ViewPaent的接口,ViewParent的方法都在该类中得到重写.那就去看下dispatchInvalidateOnAnimation(this)方法.

 public void dispatchInvalidateOnAnimation(View view) {        mInvalidateOnAnimationRunnable.addView(view);    }

这里面呢mInvalidateOnAnimationRunnable是一个实现了Runable接口的内部类.这里呢将会调用View的invalidate()方法,这个方法我们在写自定义View的时候都用过吧,调用之后讲会通知View进行draw.才InvalidateOnAnimationRunnable的addView中将会调用到postIfNeededLocked()方法.

 private void postIfNeededLocked() { //mPosted初始值是false,这里保证该方法只被执行一次.再run方法执行之后会被重新赋值成false.            if (!mPosted) {            //这里将执行mInvalidateOnAnimationRunnable这个实现类.                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);                mPosted = true;            }        }

mInvalidateOnAnimationRunnable执行的时候将会调用自己的run方法里面将遍历存储View的集合,挨个的调用自己的invalidate()方法然后将存储的View对象释放掉.

for (int i = 0; i < viewCount; i++) {                mTempViews[i].invalidate();                mTempViews[i] = null;            }

下面是mInvalidateOnAnimationRunnable的源码:

final class InvalidateOnAnimationRunnable implements Runnable {        private boolean mPosted;        private final ArrayList<View> mViews = new ArrayList<View>();        private final ArrayList<AttachInfo.InvalidateInfo> mViewRects =                new ArrayList<AttachInfo.InvalidateInfo>();        private View[] mTempViews;        private AttachInfo.InvalidateInfo[] mTempViewRects;        public void addView(View view) {            synchronized (this) {                mViews.add(view);                postIfNeededLocked();            }        }        public void addViewRect(AttachInfo.InvalidateInfo info) {            synchronized (this) {                mViewRects.add(info);                postIfNeededLocked();            }        }        public void removeView(View view) {            synchronized (this) {                mViews.remove(view);                for (int i = mViewRects.size(); i-- > 0; ) {                    AttachInfo.InvalidateInfo info = mViewRects.get(i);                    if (info.target == view) {                        mViewRects.remove(i);                        info.recycle();                    }                }                if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {                    mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);                    mPosted = false;                }            }        }        @Override        public void run() {            final int viewCount;            final int viewRectCount;            synchronized (this) {                mPosted = false;                viewCount = mViews.size();                if (viewCount != 0) {                    mTempViews = mViews.toArray(mTempViews != null                            ? mTempViews : new View[viewCount]);                    mViews.clear();                }                viewRectCount = mViewRects.size();                if (viewRectCount != 0) {                    mTempViewRects = mViewRects.toArray(mTempViewRects != null                            ? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]);                    mViewRects.clear();                }            }             //这里将调用View的绘制步骤.            for (int i = 0; i < viewCount; i++) {                mTempViews[i].invalidate();                mTempViews[i] = null;            }           //这里并不会执行.            for (int i = 0; i < viewRectCount; i++) {                final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];                info.target.invalidate(info.left, info.top, info.right, info.bottom);                info.recycle();            }        }        private void postIfNeededLocked() {            if (!mPosted) {                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);                mPosted = true;            }        }    }

最后还是调用到了View的invalidate()方法,那就去看下这个方法吧.

 /**     * 使整个试图无效,如果试图是可见的,     * {@link #onDraw(android.graphics.Canvas)}之后将会调用这个方法     */ public void invalidate() {        invalidate(true);    }public void invalidate(boolean invalidateCache) {        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);    }    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,            boolean fullInvalidate) {     ................省略代码..........            // 将矩形的信息给父类.            //damage:损坏,脏.            final AttachInfo ai = mAttachInfo;            final ViewParent p = mParent;            if (p != null && ai != null && l < r && t < b) {                final Rect damage = ai.mTmpInvalRect;                damage.set(l, t, r, b);                p.invalidateChild(this, damage);            }         ................省略代码..........        }    }

这里又调用到了ViewParent.invalidateChild方法,因为ViewParent是一个接口,也就是调用了ViewRootlmpl类重写的invalidateChild方法.我们跟一下源码.

 @Override    public void invalidateChild(View child, Rect dirty) {        invalidateChildInParent(null, dirty);    }    @Override    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {        ..........        invalidateRectOnScreen(dirty);        return null;    }    private void invalidateRectOnScreen(Rect dirty) {        .......        if (!mWillDrawSoon && (intersected || mIsAnimating)) {        //这个是启动View绘制三部曲的入口,measure,layout,draw.        //牵扯到View的绘制源码.            scheduleTraversals();        }    }

当调用到scheduleTraversals()方法的时候将会调用到View绘制的入口,也就是从布局的measure,layout,draw方法最开始被调用的地方,这里其实我们现在不必要去了解,我们只需要去看下源码就好了,最终肯定会调用到View的draw(canvas)方法,canvas对象是在scheduleTraversals()这一系列方法中创建然后传给View的.我们直接看下View的draw方法.

 public void draw(Canvas canvas) {        final int privateFlags = mPrivateFlags;        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;        /*         * Draw traversal performs several drawing steps which must be executed         * in the appropriate order:         *         *      1. Draw the background         *      2. If necessary, save the canvas' layers to prepare for fading         *      3. Draw view's content         *      4. Draw children         *      5. If necessary, draw the fading edges and restore layers         *      6. Draw decorations (scrollbars for instance)         */        // Step 1, draw the background, if needed        int saveCount;        if (!dirtyOpaque) {            drawBackground(canvas);        }        // skip step 2 & 5 if possible (common case)        final int viewFlags = mViewFlags;        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;        if (!verticalEdges && !horizontalEdges) {            // Step 3, draw the content            //这个是自己实现然后实现自己的绘制逻辑            if (!dirtyOpaque) onDraw(canvas);            //这个一般是ViewGroup才会重写的方法.            // Step 4, draw the children            dispatchDraw(canvas);           ...................            // Step 6, draw decorations (foreground, scrollbars)            //这里面绘制了scrollBar            onDrawForeground(canvas);            // we're done...            return;        }        .............    }

draw方法里面可以看出很多都是空的方法需要子类自己去实现绘制逻辑,只有那个onDrawForeground(canvas)绘制前景,这里面才用到了mScrollX和mScrollY变量.跟着走源码:

public void onDrawForeground(Canvas canvas) {        onDrawScrollIndicators(canvas);        onDrawScrollBars(canvas);        ........        }

接着看源码:

 protected final void onDrawScrollBars(Canvas canvas) {        // scrollbars are drawn only when the animation is running        //再scrollTo方法里面已经给ScrollabilityCache赋值了.        final ScrollabilityCache cache = mScrollCache;        if (cache != null) {            int state = cache.state;            if (state == ScrollabilityCache.OFF) {                return;            }            boolean invalidate = false;           .......                if (drawHorizontalScrollBar) {                    scrollBar.setParameters(computeHorizontalScrollRange(),                            computeHorizontalScrollOffset(),                            computeHorizontalScrollExtent(), false);                    final Rect bounds = cache.mScrollBarBounds;                    getHorizontalScrollBarBounds(bounds, null);                    onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,                            bounds.right, bounds.bottom);                    if (invalidate) {                    //看到这个方法我们就发现了希望.                        invalidate(bounds);                    }                }                if (drawVerticalScrollBar) {                    scrollBar.setParameters(computeVerticalScrollRange(),                            computeVerticalScrollOffset(),                            computeVerticalScrollExtent(), true);                    final Rect bounds = cache.mScrollBarBounds;                    getVerticalScrollBarBounds(bounds, null);                    onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,                            bounds.right, bounds.bottom);                    if (invalidate) {                        invalidate(bounds);                    }                }            }        }    }

最会我们发现onDrawScrollBars里再次调用了invalidate方法,不过参数是一个矩形的对象.继续跟源码:

 public void invalidate(int l, int t, int r, int b) {        final int scrollX = mScrollX;        final int scrollY = mScrollY;        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);    }

这里我们能大致的看到矩形的left 是 l - scrollx等等,这就是为什么再scrollto或者scrollBy里面设置正直的话就是往反方向走,设置父值得话就是往正确的方向.我们继续跟着源码走的话可以看到这里又重新的调用了ViewRootlmpl类的invalidateChild方法,又重复了之前的绘制过程.好吧源码勉强就看到这把,其实遗留了很多的问题小编也是无奈的,Android源码太强大!

结语

这里面就简单的跟着scrollTo的源码走了一遍,还有很多的问题没有解决.这里就算是给大家指出一条看源码的思路吧.随后还会继续更新这里面没有解决的几个问题,什么问题呢我想跟着走一遍源码的时候你自己就会发现了.

欢迎大家的纠正~
感觉可以就给个赞支持一下吧~

欢迎关注我的掘金
欢迎关注我的简书
欢迎关注我的CSDN