在这里先说一下结论,我们调用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分析也就结束了。