mLeft mScrollX translationX 傻傻分不清楚
一开始看到这个标题内心是血崩的 ^ ^
先抛出几个锅: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 }
也就是说你不重新在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.