简析Scroller

起初认识Srcoller还是在ViewPager的源码中,当看到ViewPager源码了解它是如何滑动的时候,发现了Scroller的作用。Scroller是一个辅助类,根据x、y坐标,还有滑动的时间,得到滑动到某个时间的坐标,这样可以使滑动的过程显的更加平滑,而不会出现一段段位移的效果。
如何使用,源码中也列出了几句话

举例 下面是开始一个滑动的代码

private Scroller mScroller = new Scroller(context);
... ...
public void zoomIn() {
	//Revert any animation currently in progress
	mScroller.forceFinished(true);
	// Start scrolling by providing a starting point and
	// the distance to travel
	mScroller.startScroll(0, 0, 100, 0);
	// Invalidate to request a redraw
	invalidate();
}

举例  计算当前位移的坐标

if (mScroller.computeScrollOffset()) {
	// Get current x and y positions
	int currX = mScroller.getCurrX();
	int currY = mScroller.getCurrY();
	... ...
}

开始滚动的方法 

    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

startX startY 代表开始滚动的一个点的坐标(使用滚动坐标系)
dx 水平滚动的距离
dy 垂直滚动的距离
startX 和 dx 为正代表向左 为负代表向右
startY 和 dy 为正代表向上 为负代表向下
duration 滚动需要的时长

 

实时拿到控件移动的位置,并且移动的姿势都在computeScrollOffset方法中封装好了

    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

无论是滚动模式 还是 快速滑动模式  这里都是要得到mCurrX 和 mCurrY的值,View通过    int currX = mScroller.getCurrX()   int currY = mScroller.getCurrY() 就能得到当前View的内容应该滚动到的位置。
拿到了滚动的位置,但是Scroller又不承担滚动的责任,那么View是如何滚动起来的呢?上面源码的举例说明提到了,开始滚动调用startScroll方法,都是初始化的操作。接着startScroll执行完又调用了invalidate方法,猜测invalidate方法很有可能跟View 的滚动有关系。继续查看invalidate的源码:

说明

     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.

绘制整个View,如果View是可见的那么以后会调用onDraw方法。这个方法一定是要运行在UI线程的,如果是从非UI线程调用那么应该用postInvalidate方法。

View调用invalidate方法
 --->invalidate(true)
 --->invalidateInternal
 --->mParent.invalidateChildinvalidateChild(this, damage)   调用父容器的invalidateChild方法,作用是通过do while循环不断将向上层遍历,最终拿到最外层的parent

            ... ...
			do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) {
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }
				...	...
                parent = parent.invalidateChildInParent(location, dirty);
				...	...
            } while (parent != null);		

由于最顶层的View是DecorView,而通过invalidateChildInParent拿到的parent就是ViewRootImpl;非顶层通过invalidateChildInParent拿到的parent就是ViewGroup。如果parent是ViewGroup,那么就直接返回parent,如果parent是ViewRootImpl,操作如下

 --->invalidateChildInParent  如果parent是ViewRootImpl那么它调用这个方法如下

	public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
		... ...
        invalidateRectOnScreen(dirty);
        return null;
    }
	void checkThread() {
			if (mThread != Thread.currentThread()) {
				throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
			}
	}

由于创建VieRootImpl的时候mThread就是UI线程,因此这里的checkThread会判断当前是否是UI线程,如果不是那么会抛出异常,这也就是说明了Invalidate方法必须在UI线程中调用。

--->invalidateRectOnScreen  
--->scheduleTraversals  
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 发送消息要执行mTraversalRunnable的run方法的内容
--->doTraversal
--->performTraversals
--->performDraw
--->draw(fullRedrawNeeded)
--->drawSoftware

        final Canvas canvas;
		... ...
        canvas = mSurface.lockCanvas(dirty);
		... ... 
		canvas.translate(-xoff, -yoff);
			if (mTranslator != null) {
				mTranslator.translateCanvas(canvas);
			}
			... ...
			mView.draw(canvas);
			... ...

这里的mView.draw(canvas)就是DecorView的draw方法,DecorView本质上也是一个View,那么我们看View的draw方法

--->draw(Canvas canvas)

    draw 的流程 
    1. 绘制背景 
    2.保存画布的图层为了渐隐效果做准备(如果需要)
    3. 绘制内容 
    4. 绘制 children 
    5. 如果有需要,绘制渐隐(fading) 效果 
    6. 绘制装饰物 (scrollbars)

--->dispatchDraw   我们直接看这个方法,它的作用是绘制children,当然肯定是ViewGroup调用它
--->drawChild
--->child.draw(canvas, this, drawingTime)  这里又调用了View的draw方法,不过参数有所不同,该方法是ViewGroup调用         drawChild方法后执行的,意在使每个child都被绘制出来。制的view专门负责描述视图和硬件加速方面的类型。

		... ...
		int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }
		... ...
		if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }
        ... ...

这里我们看到了computeScroll方法,这个方法是要求子类自己去实现的,然后我们看到canvas被移动了,而移动的距离跟mScrollX和mScrollY有关系。

而mScrollX和mScrollY就是关键所在,在这之前看两个方法:scrollTo 和 scrollBy

    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

顾名思义 这两个方法正是View滚动的源头。根据源码我们看到要使View滚动就得改变mScrollX和mScrollY的值,然后重绘,显示View滚动后的位置。所以说,View滚动跟canvas移动了有关,移动了多少跟mScrollX和mScrollY有关,mScrollX、mScrollY又通过调用了scrollTo或者scrollBy方法得到,这两个方法的参数x、y就影响了移动距离的数值。如果想移动一下传一对x、y就行,如果想按某个规则移动就得依靠Scroller,通过Scroller实时将一组组x、y传递过来,那么View就会是平滑的进行一段移动。因此View的draw方法中留了一个computeScroll方法让我们重写,使View当前的mScrollX和mScrollY跟Scroller的mCurrX和mCurrY做了映射,然后重绘,根据Scroller之前讲的移动规则,实时返回移动后的位置,computeScroll拿到赋值给mScrollX和mScrollY,重绘调用draw方法的时候cavas就会移动mScrollX和mScrollY,然后调用ViewCompat.postInvalidateOnAnimation(this),使得这一个过程达到平滑移动的效果。

 

拓:是如何做到所有View都调用了onDraw方法呢?

我们看到上面提到了调用DecorView的draw方法,就是调用View的draw方法,draw分为六个步骤,在第三个步骤中,如果本身是不透明的就调用onDraw方法,如下:

	public void draw(Canvas canvas) {
		... ...
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }
		... ...
	}

dirtyOpaque 代表是否是不透明的效果(是否是实心控件),然后调用onDraw(canvas)方法。

遍历所有子View进行绘制,根据字面意思应该在dispatchDraw方法中进行:

	protected void dispatchDraw(Canvas canvas) {
		... ...
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
		... ...
	}

遍历了当前ViewGroup(顶层即DecorView)所有子View然后调用drawChild方法:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

drawChild绘制了ViewGroup中的一个View,并且这个方法可以获得正确的画布状态,正确状态包括裁剪,移动坐标的转化以便child移动原点在0,0处,还有一些动作转变。
看源码,实际上这些操作都在child的draw(canvas,this,drawingTime)方法中实现了。进入View的draw(canvas,this,drawingTime)

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ... ...
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }
	... ...
}

这里的draw方法本身就是DecorView的child发起的,先判断是否有缓存,如果有缓存证明绘制过一次,则会跳过绘制它自己直接调用dispatchDraw。一般ViewGroup都会跳过绘制自己直接绘制child,即
mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW 是为ture,但是如果你想ViewGroup也要进行onDraw方法的话,需要为这个ViewGroup设置背景或者setWillNotDraw(false)。
所以如果我们自定义ViewGroup想让该ViewGroup执行onDraw方法,那么我们可以为这个ViewGroup设置一个背景或者setWillNotDraw(false),该ViewGroup就走draw(canvas)方法,即那六个步骤,顺带
自己也要执行onDraw(canvas)方法。无论哪种方法DecorView的child此时也会绘制他们自己的child,这样遍历下去,所有的子View(View树)都会得到绘制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Scroller 是一个用于实现平滑滚动效果的工具类。它可以用于在 Android 应用中实现滑动的动画效果,如平滑滚动到指定位置或者平滑滚动到顶部。 使用 Android Scroller 需要以下步骤: 1. 创建一个 Scroller 实例:使用 `new Scroller(context)` 创建一个 Scroller 对象。 2. 在 View 的 `computeScroll()` 方法中更新滚动位置:在需要实现滑动效果的 View 类里重写 `computeScroll()` 方法,然后在该方法中调用 `scroller.computeScrollOffset()` 获取当前的滚动位置,并根据需要更新 View 的位置。 3. 处理触摸事件:在触摸事件的回调方法中调用 Scroller 的 `startScroll()` 方法来启动滚动效果。可以根据触摸事件的不同情况调用不同的方法,如 `startScroll(int startX, int startY, int dx, int dy)` 或者 `startScroll(int startX, int startY, int dx, int dy, int duration)` 来指定滚动的起点、偏移量和持续时间。 4. 在 View 的 `invalidate()` 方法中不断重绘:在 `computeScroll()` 方法中更新了 View 的位置后,需要在 View 的 `invalidate()` 方法中调用,以便触发 View 的重新绘制。 需要注意的是,尽管 Android Scroller 提供了平滑滚动的功能,但它仅仅是一个工具类,实际的滚动效果实现还需要结合其他相关的 API 和组件来完成,如使用 `ViewGroup.LayoutParams` 来设置 View 的位置和大小,或者使用 `ViewPropertyAnimator` 实现更复杂的动画效果。 希望这个回答对你有帮助!如果有更多问题,请继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值