Android scrollTo滑动原理分析

本文深入解析了Android中scrollTo方法的工作原理,揭示了其如何通过调整画布位置实现视图内容的滚动,以及为何传入正值会导致内容向左或向上移动。通过分析scrollTo方法的调用序列,我们了解到它最终会触发View的重绘过程,同时解释了为何某些情况下,如没有背景的View,onDraw方法不会被调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里先说一下结论,我们调用scrollTo使View内容发生移动的原因是:view的画布发生了移动;即scrollTo的调用最终会调用:

canvas.translate(-mScrollX, -mScrollY)

这也同时解释了为什么我们对scrollTo方法传入正值,view的内容却往左/上移动。在分析之前,我们先给出一个scrollTo方法的使用例子:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="10dp"
    tools:context=".MainActivity">

    <com.example.apiexcute2.MyLinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />

    </com.example.apiexcute2.MyLinearLayout>


    <Button
        android:id="@+id/button"
        android:onClick="click"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />

</LinearLayout>

这里的MyLinearLayout直接继承了LinearLayout,并没有修改LinearLayout的逻辑代码,使MyLinearLayout滑动代码

    public void click(View v){
        myLinearLayout.scrollTo(10,10);
    }

运行结果分析:

当我们点击按钮时,这两个TextView向左上移动了10px,这是正常的。但是在这个过程中,我发现MYLinearLayout的onDraw方法没有调用。然后百度了一下这个问题,网上说是只有设置了MyLinearLayout的背景,它的onDraw方法才会调用,测试了一下确实是这样。那么接下来,我们就带着这个问题来分析一下scrollTo方法的方法调用序列。

我们先来看一下scrollTo方法:

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();
            }
        }
    }

从这里我们看到scrollTo方法,最终会去调用postInvalidateOnAnimation方法,

/**
     * <p>Cause an invalidate to happen on the next animation time step, typically the
     * next display frame.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @see #invalidate()
     */
    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);
        }
    }

对于这个方法,从注释和它的名字,我们可以看出它应该是请求Vieiw绘制的,而且这个方法可以从非UI线程调用。在这个方法里又调用了dispatchInvalidateOnAnimation,我们继续跟进:

 public void dispatchInvalidateOnAnimation(View view) {
        mInvalidateOnAnimationRunnable.addView(view);
    }
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);//将要刷新的view方法到mViews
                postIfNeededLocked();
            }
        }
        ......
        @Override
        public void run() {
            final int viewCount;
            final int viewRectCount;
            synchronized (this) {
                mPosted = false;
                //将mViews中的对象放到mTempViews中
                viewCount = mViews.size();
                if (viewCount != 0) {
                    mTempViews = mViews.toArray(mTempViews != null
                            ? mTempViews : new View[viewCount]);
                    mViews.clear();
                }

                ......
            }
            //关键代码
            for (int i = 0; i < viewCount; i++) {
                mTempViews[i].invalidate();
                mTempViews[i] = null;
            }
            ......
        }
    }

从这里我们可以看到dispatchInvalidateOnAnimation(View view),这个方法最终会将view传给mViews,并且会调用View的invalidate方法(invalidate绘制流程)。我们知道invalidate方法最终会引起View树的绘制(View绘制原理)。而View树在绘制时,会调用View的draw(Canvas,ViewGroup,long)方法

/**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
         * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
         * HW accelerated, it can't handle drawing RenderNodes.
         */
        boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;
        ......

        // Sets the flag as early as possible to allow draw() implementations
        // to call invalidate() successfully when doing animations
        mPrivateFlags |= PFLAG_DRAWN;
        ......

        RenderNode renderNode = null;
        Bitmap cache = null;
        int layerType = getLayerType(); // TODO: signify cache state with just 'cache' local
        if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
             if (layerType != LAYER_TYPE_NONE) {
                 // If not drawing with RenderNode, treat HW layers as SW
                 layerType = LAYER_TYPE_SOFTWARE;
                 buildDrawingCache(true);
            }
            cache = getDrawingCache(true);
        }

        if (drawingWithRenderNode) {
            // Delay getting the display list until animation-driven alpha values are
            // set up and possibly passed on to the view
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.isValid()) {
                // Uncommon, but possible. If a view is removed from the hierarchy during the call
                // to getDisplayList(), the display list will be marked invalid and we should not
                // try to use it again.
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }
        ......
        mRecreateDisplayList = false;

        return more;
    }

首先,我们会看到canvas.isHardwareAccelerated。这个方法的返回值为true,然后设置drawingWithRenderNode的值,这里它的值也为true,这个变量猜测是决定此次绘制是否启用硬件加速的(对于硬件加速可以看下这个链接上的文章)。因为drawingWithRenderNode的值为true,所以接下来会去调用updateDisplayListIfDirty方法。

public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
        if (!canHaveDisplayList()) {
            // can't populate RenderNode, don't try
            return renderNode;
        }

        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.isValid()
                || (mRecreateDisplayList)) {
            ......
            // If we got here, we're recreating it. Mark it as such to ensure that
            // we copy in child display lists into ours in drawChild()
            mRecreateDisplayList = true;

            int width = mRight - mLeft;
            int height = mBottom - mTop;
            int layerType = getLayerType();

            final DisplayListCanvas canvas = renderNode.start(width, height);

            try {
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    ......
                } else {
                    computeScroll();

                    canvas.translate(-mScrollX, -mScrollY);
                    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                        drawAutofilledHighlight(canvas);
                        ......
                    } else {
                        draw(canvas);
                    }
                }
            } finally {
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
        } else {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }
        return renderNode;
    }

在这里,我们就可以看到canvas.translate方法了,它传入的参数就是scrollTo方法中设置mScrollX与mScrollY,但是在调用这个方法之前有一个判断,即判断此次绘制是否是软件绘制。这里我们发现MyLinearLayout的layerType类型是LAYER_TYPE_NONE,所以不会进行软件绘制。接着往下看,

if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
     dispatchDraw(canvas);
     drawAutofilledHighlight(canvas);
     ......
} 

我们发现在绘制view时,会检查view的标志位是否是PFLAG_SKIP_DRAW,如果是,则标志此次绘制不用自身的内容,即不用调用onDraw方法。因为MyLinearLayout只是进行了滑动,所以这个判断为true。

同时,我们也发现了滑动时,MyLinearLayout的onDraw方法不调用的原因:“Fast path for layouts with no backgrounds”。也就是说没有背景的View会走这个快速执行路径,直接调用dispatchDraw(Canvas),而不是去调用draw(Canvas)方法绘制。

到此我们的scrollTo分析也就结束了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值