一开始看到这个标题内心是血崩的 ^ ^

先抛出几个锅:view的getLeft返回什么? getScrollX返回什么? getX返回什么? getTranslationX返回什么?

onLayout大家都知道吧

 1 /**
 2      * Called from layout when this view should
 3      * assign a size and position to each of its children.
 4      *
 5      * Derived classes with children should override
 6      * this method and call layout on each of
 7      * their children.
 8      * @param changed This is a new size or position for this view
 9      * @param left Left position, relative to parent
10      * @param top Top position, relative to parent
11      * @param right Right position, relative to parent
12      * @param bottom Bottom position, relative to parent
13      */
14     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
15     }

说了这个参数left (top right bottom) 是相对于父view的左间距, 那么这个left哪里来呢? 自然就要去看看layout:

 1 public void layout(int l, int t, int r, int b) {
 2         // blahblah...
 3 
 4         boolean changed = isLayoutModeOptical(mParent) ?
 5                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
 6 
 7         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
 8             onLayout(changed, l, t, r, b);
 9             // blahblah...
10     }

其中setOpticalFrame最终也会调用setFrame:

 1 protected boolean setFrame(int left, int top, int right, int bottom) {
 2         // blahblah...
 3 
 4         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
 5             changed = true;
 6 
 7             // Remember our drawn bit
 8             int drawn = mPrivateFlags & PFLAG_DRAWN;
 9 
10             int oldWidth = mRight - mLeft;
11             int oldHeight = mBottom - mTop;
12             int newWidth = right - left;
13             int newHeight = bottom - top;
14             boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
15 
16             // Invalidate our old position
17             invalidate(sizeChanged);
18 
19             mLeft = left;
20             mTop = top;
21             mRight = right;
22             mBottom = bottom;
23             mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
24         	
25         	// blahblah...
26 
27     }

看到没有,将这个left传给了mLeft, 那mLeft是什么?

1 /**
2      * Left position of this view relative to its parent.
3      *
4      * @return The left edge of this view, in pixels.
5      */
6     @ViewDebug.CapturedViewProperty
7     public final int getLeft() {
8         return mLeft;
9     }
getLeft返回的是本view与父view左边界的间距, 也就是layout过程中确定的父子view的相对位置, 也就是显示布局边界看到的边界

也就是说你不重新在layout过程中改变想对于parent的位置,那么mLeft是不会变的


Scroller大家都用过吧, 用来控制滑动的, 简单给个sample好了:

 1 public void smoothScrollBy(int dx, int dy) {  
 2         mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);  
 3         invalidate();
 4     }  
 5       
 6     @Override  
 7     public void computeScroll() {  
 8       
 9         if (mScroller.computeScrollOffset()) {  
10           
11             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
12               
13             postInvalidate();  
14         }  
15         super.computeScroll();  
16     }

基本上Scroller就是一个计时器, 根据走过的时间,计算出当前滑的坐标,然后调用scrollTo才是真正让view显示出滑动的效果, scrollTo本身没有动画效果, 所以本质上scroller就是一个润滑油, 使invalidate更加缓和,从而达到滑的视觉效果

首先要明确一点,手机屏幕的坐标,默认x向右增长,因此我们做动画的时候,增加translationX什么的都会往右移动, 但是当你增加mScrollX时,内容是向左移,为什么等下说

那么scrollTo跟mScrollX是什么关系呢?到底滑的是什么?

 1 public void scrollTo(int x, int y) {
 2         if (mScrollX != x || mScrollY != y) {
 3             int oldX = mScrollX;
 4             int oldY = mScrollY;
 5             mScrollX = x;
 6             mScrollY = y;
 7             invalidateParentCaches();
 8             onScrollChanged(mScrollX, mScrollY, oldX, oldY);
 9             if (!awakenScrollBars()) {
10                 postInvalidateOnAnimation();
11             }
12         }
13     }

很简单嘛,只不过就是将x设置给了mScrollX,然后调用postInvalidateOnAnimation(这个方法会把本view加入viewRootImpl的一个缓存中,然后在适当的时候viewRootImpl会调用这个缓存中所有view的invalidate, 这又会引起viewRootImpl的scheduleTraversals引起重绘, 关于invalidate和requestLayout以后有空再详细分析好了)
那么这样,每scrollTo一次就可以draw一次啦, 那再去看看draw(我们看ViewGroup.drawChild()调用的draw会看的更清楚一点, 原因等会儿说)到底干了什么让view滑了起来:

 1 /**
 2      * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 3      *
 4      * This is where the View specializes rendering behavior based on layer type,
 5      * and hardware acceleration.
 6      */
 7     boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
 8         // blahblah... 
 9 
10         int sx = 0;
11         int sy = 0;
12         if (!drawingWithRenderNode) {
13             computeScroll();
14             sx = mScrollX;
15             sy = mScrollY;
16         }
17 
18         final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
19         final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
20 		
21 		// blahblah...
22         
23         if (offsetForScroll) {
24             canvas.translate(mLeft - sx, mTop - sy);
25         } else {
26             if (!drawingWithRenderNode) {
27                 canvas.translate(mLeft, mTop);
28             }
29 
30         // blahblah...
31 
32         float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
33         if (transformToApply != null
34                 || alpha < 1
35                 || !hasIdentityMatrix()
36                 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
37             if (transformToApply != null || !childHasIdentityMatrix) {
38                 int transX = 0;
39                 int transY = 0;
40 
41                 if (offsetForScroll) {
42                     transX = -sx;
43                     transY = -sy;
44                 }
45 
46                 if (transformToApply != null) {
47                     if (concatMatrix) {
48                         if (drawingWithRenderNode) {
49                             renderNode.setAnimationMatrix(transformToApply.getMatrix());
50                         } else {
51                             // Undo the scroll translation, apply the transformation matrix,
52                             // then redo the scroll translate to get the correct result.
53                             canvas.translate(-transX, -transY);
54                             canvas.concat(transformToApply.getMatrix());
55                             canvas.translate(transX, transY);
56                         }
57                         
58         // blahblah...
59 
60         if (!drawingWithDrawingCache) {
61             if (drawingWithRenderNode) {
62                 mPrivateFlags &= ~PFLAG_DIRTY_MASK;
63                 ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
64             } else {
65                 // Fast path for layouts with no backgrounds
66                 if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
67                     mPrivateFlags &= ~PFLAG_DIRTY_MASK;
68                     dispatchDraw(canvas);
69                 } else {
70                     draw(canvas);
71                 }
72             }
73         } 
74 
75         // blahblah...
76     }

draw的代码非常长,里面的逻辑也很复杂,说实话我看不懂,但是我们可以从第24、51行发现一些蛛丝马迹:

24.将整个画布平移, mLeft刚才解释了,再减去负的mScrollX,对,负的!自己画一下吧,这就是真正的偏移量!

51.人家注释都说了,undo 和 redo scroll translation是通过canvas平移来实现变换的,而平移的量就是12行拿到的mScrollX !

至此,我有了一个大胆的猜想:scroll的操作是通过移动view的canvas来实现的,跟layout没有任何关系,由于子view的内容全部画在parent的canvas上,当parent的canvas整体平移时, 子view也会跟着移动,并且不需要重绘
要论证上述猜想, 我们换个思路,看看TouchEvent是怎么分发的吧:

我们知道,在一个view收到一个TouchEvent的时候,却getX是得到这个event在本view中的偏移,那么可想而知从parent分发这个event的时候,必定要对x进行操作,以保证子view中能等到正确的偏移:

 1 @Override
 2     public boolean dispatchTouchEvent(MotionEvent ev) {
 3         // blahblah...
 4         if (newTouchTarget == null && childrenCount != 0) {
 5             final float x = ev.getX(actionIndex);
 6                         final float y = ev.getY(actionIndex);
 7                         // Find a child that can receive the event.
 8                         // Scan children from front to back.
 9                         final ArrayList<View> preorderedList = buildOrderedChildList();
10                         final boolean customOrder = preorderedList == null
11                                 && isChildrenDrawingOrderEnabled();
12                         final View[] children = mChildren;
13                         for (int i = childrenCount - 1; i >= 0; i--) {
14                             final int childIndex = customOrder
15                                     ? getChildDrawingOrder(childrenCount, i) : i;
16                             final View child = (preorderedList == null)
17                                     ? children[childIndex] : preorderedList.get(childIndex);
18 
19                             // If there is a view that has accessibility focus we want it
20                             // to get the event first and if not handled we will perform a
21                             // normal dispatch. We may do a double iteration but this is
22                             // safer given the timeframe.
23                             if (childWithAccessibilityFocus != null) {
24                                 if (childWithAccessibilityFocus != child) {
25                                     continue;
26                                 }
27                                 childWithAccessibilityFocus = null;
28                                 i = childrenCount - 1;
29                             }
30 
31                             if (!canViewReceivePointerEvents(child)
32                                     || !isTransformedTouchPointInView(x, y, child, null)) {
33                                 ev.setTargetAccessibilityFocus(false);
34                                 continue;
35                             }
36 
37                             // blahblah...
38 
39             // Dispatch to touch targets.
40             if (mFirstTouchTarget == null) {
41                 // No touch targets so treat this as an ordinary view.
42                 handled = dispatchTransformedTouchEvent(ev, canceled, null,
43                         TouchTarget.ALL_POINTER_IDS);
44             } else {
45                 // Dispatch to touch targets, excluding the new touch target if we already
46                 // dispatched to it.  Cancel touch targets if necessary.
47                 TouchTarget predecessor = null;
48                 TouchTarget target = mFirstTouchTarget;
49                 while (target != null) {
50                     final TouchTarget next = target.next;
51                     if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
52                         handled = true;
53                     } else {
54                         final boolean cancelChild = resetCancelNextUpFlag(target.child)
55                                 || intercepted;
56                         if (dispatchTransformedTouchEvent(ev, cancelChild,
57                                 target.child, target.pointerIdBits)) {
58                             handled = true;
59                         }
60                         // blahblah...
61     }
62 }

先看32行:

 1 /**
 2      * Returns true if a child view contains the specified point when transformed
 3      * into its coordinate space.
 4      * Child must not be null.
 5      * @hide
 6      */
 7     protected boolean isTransformedTouchPointInView(float x, float y, View child,
 8             PointF outLocalPoint) {
 9         final float[] point = getTempPoint();
10         point[0] = x;
11         point[1] = y;
12         transformPointToViewLocal(point, child);
13         final boolean isInView = child.pointInView(point[0], point[1]);
14         if (isInView && outLocalPoint != null) {
15             outLocalPoint.set(point[0], point[1]);
16         }
17         return isInView;
18     }
19 
20 
21 
22     public void transformPointToViewLocal(float[] point, View child) {
23         point[0] += mScrollX - child.mLeft;
24         point[1] += mScrollY - child.mTop;
25 
26         if (!child.hasIdentityMatrix()) {
27             child.getInverseMatrix().mapPoints(point);
28         }
29     }

看到木有,point[0] = x + mScrollX - child.mLeft, 自己去画个图吧,这就是event在child里的坐标,不光跟child.mLeft有关,还跟parent的mScrollX有关
还不信?在来看看dispatchTransformedTouchEvent(dispatchTouchEvent 56行):

 1 /**
 2      * Transforms a motion event into the coordinate space of a particular child view,
 3      * filters out irrelevant pointer ids, and overrides its action if necessary.
 4      * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 5      */
 6     private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
 7             View child, int desiredPointerIdBits) {
 8         final boolean handled;
 9 
10         // Canceling motions is a special case.  We don't need to perform any transformations
11         // or filtering.  The important part is the action, not the contents.
12         final int oldAction = event.getAction();
13         if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
14             event.setAction(MotionEvent.ACTION_CANCEL);
15             if (child == null) {
16                 handled = super.dispatchTouchEvent(event);
17             } else {
18                 handled = child.dispatchTouchEvent(event);
19             }
20             event.setAction(oldAction);
21             return handled;
22         }
23 
24         // Calculate the number of pointers to deliver.
25         final int oldPointerIdBits = event.getPointerIdBits();
26         final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
27 
28         // If for some reason we ended up in an inconsistent state where it looks like we
29         // might produce a motion event with no pointers in it, then drop the event.
30         if (newPointerIdBits == 0) {
31             return false;
32         }
33 
34         // If the number of pointers is the same and we don't need to perform any fancy
35         // irreversible transformations, then we can reuse the motion event for this
36         // dispatch as long as we are careful to revert any changes we make.
37         // Otherwise we need to make a copy.
38         final MotionEvent transformedEvent;
39         if (newPointerIdBits == oldPointerIdBits) {
40             if (child == null || child.hasIdentityMatrix()) {
41                 if (child == null) {
42                     handled = super.dispatchTouchEvent(event);
43                 } else {
44                     final float offsetX = mScrollX - child.mLeft;
45                     final float offsetY = mScrollY - child.mTop;
46                     event.offsetLocation(offsetX, offsetY);
47 
48                     handled = child.dispatchTouchEvent(event);
49 
50                     event.offsetLocation(-offsetX, -offsetY);
51                 }
52                 return handled;
53             }
54             transformedEvent = MotionEvent.obtain(event);
55         } else {
56             transformedEvent = event.split(newPointerIdBits);
57         }
58 
59         // Perform any necessary transformations and dispatch.
60         if (child == null) {
61             handled = super.dispatchTouchEvent(transformedEvent);
62         } else {
63             final float offsetX = mScrollX - child.mLeft;
64             final float offsetY = mScrollY - child.mTop;
65             transformedEvent.offsetLocation(offsetX, offsetY);
66             if (! child.hasIdentityMatrix()) {
67                 transformedEvent.transform(child.getInverseMatrix());
68             }
69 
70             handled = child.dispatchTouchEvent(transformedEvent);
71         }
72 
73         // Done.
74         transformedEvent.recycle();
75         return handled;
76     }
77 
78 
79 
80 
81 	/**
82      * Adjust this event's location.
83      * @param deltaX Amount to add to the current X coordinate of the event.
84      * @param deltaY Amount to add to the current Y coordinate of the event.
85      */
86     public final void offsetLocation(float deltaX, float deltaY) {
87         if (deltaX != 0.0f || deltaY != 0.0f) {
88             nativeOffsetLocation(mNativePtr, deltaX, deltaY);
89         }
90     }

看看46、65行吧, 我就不解释了


getX又是什么鬼!

 1 /**
 2      * The visual x position of this view, in pixels. This is equivalent to the
 3      * {@link #setTranslationX(float) translationX} property plus the current
 4      * {@link #getLeft() left} property.
 5      *
 6      * @return The visual x position of this view, in pixels.
 7      */
 8     @ViewDebug.ExportedProperty(category = "drawing")
 9     public float getX() {
10         return mLeft + getTranslationX();
11     }

这么看来, getX返回的是view的虚拟x坐标,所谓虚拟就是原本相对于parent的layout再加上translation的偏移

看来跟mScrollX没什么关系嘛, 那translationX到底是什么?

 1 /**
 2      * Sets the horizontal location of this view relative to its {@link #getLeft() left} position.
 3      * This effectively positions the object post-layout, in addition to wherever the object's
 4      * layout placed it.
 5      *
 6      * @param translationX The horizontal position of this view relative to its left position,
 7      * in pixels.
 8      *
 9      * @attr ref android.R.styleable#View_translationX
10      */
11     public void setTranslationX(float translationX) {
12         if (translationX != getTranslationX()) {
13             invalidateViewProperty(true, false);
14             mRenderNode.setTranslationX(translationX);
15             invalidateViewProperty(false, true);
16 
17             invalidateParentIfNeededAndWasQuickRejected();
18             notifySubtreeAccessibilityStateChangedIfNeeded();
19         }
20     }
21 
22 
23     /**
24      * The horizontal location of this view relative to its {@link #getLeft() left} position.
25      * This position is post-layout, in addition to wherever the object's
26      * layout placed it.
27      *
28      * @return The horizontal position of this view relative to its left position, in pixels.
29      */
30     @ViewDebug.ExportedProperty(category = "drawing")
31     public float getTranslationX() {
32         return mRenderNode.getTranslationX();
33     }

讲道理, 看不清renderNode到底是什么东西, 有很多native的东西,貌似跟硬件有关?并且在draw方法里也看到了很多在drawingWithRenderNode条件下有一些不同的操作, 这里就留一个疑问吧

大胆猜测, 是一套单独直接与底层接触的动画重绘机制,跟canvas没有太大的关系

总结

getLeft() 就是你layout完跟parent之间的距离
scroll 本质上是移动这个view的画布(的坐标系)并重新invalidate(画到canvas上的东西是不会变的,即使移动了坐标系,只能invalidate重新画),那么里面的子view的坐标系也会动,但是它们的相对位置是一样的
getX() 原始在parent里的位置加上属性动画translation的偏移

RenderNode:

A display list records a series of graphics related operations and can replay
them later. Display lists are usually built by recording operations on a
{@link DisplayListCanvas}. Replaying the operations from a display list avoids 
executing application code on every frame, and is thus much more efficient.