Android源码角度分析View的scrollBy()和scrollTo()的参数正负问题
为什么要写这篇博客?以前在使用View的scrollBy()或者scrollTo()的时候,发现它们的参数在正的时候是反方向移动,负的时候是正方向移动。于是就google了下,发现好多博客都要么是转摘、要么是直接抄袭然后美起名曰原创,更恶劣的是这些博文由于是转摘抄袭的关系,竟然都说View在scrollBy()或者scrollTo()的时候,它们的直角坐标系是相反的,这明显是一个错误的观念。
好了,废话不多说进入正题。
Android设备平面直角坐标系
在做分析之前,首先要建立起Android设备屏幕的平面直角坐标系概念。在Android手机中,屏幕的直角坐标轴概念简单来说:
屏幕左上角为直角坐标系的原点(0,0)从原点出发向左为X轴负方向,向右为X轴正方向从原点出发向上为Y轴负方向,向下为Y轴正方向
上述概念可通过如下图总结:
在Android中,我们通常说View在屏幕上的坐标,其实就是view的左上的坐标。调用View的invalidate()方法会促使View重绘。
View的scrollBy()和scrollTo()
在分析scrollBy()和scrollTo()之前,先上一段源码片段:01.
/**
02.
* Set the scrolled position of your view. This will cause a call to
03.
* {@link #onScrollChanged(int, int, int, int)} and the view will be
04.
* invalidated.
05.
* @param x the x position to scroll to
06.
* @param y the y position to scroll to
07.
*/
08.
public
void
scrollTo(
int
x,
int
y) {
09.
if
(mScrollX != x || mScrollY != y) {
10.
int
oldX = mScrollX;
11.
int
oldY = mScrollY;
12.
mScrollX = x;
13.
mScrollY = y;
14.
invalidateParentCaches();
15.
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
16.
if
(!awakenScrollBars()) {
17.
invalidate(
true
);
18.
}
19.
}
20.
}
21.
22.
/**
23.
* Move the scrolled position of your view. This will cause a call to
24.
* {@link #onScrollChanged(int, int, int, int)} and the view will be
25.
* invalidated.
26.
* @param x the amount of pixels to scroll by horizontally
27.
* @param y the amount of pixels to scroll by vertically
28.
*/
29.
public
void
scrollBy(
int
x,
int
y) {
30.
scrollTo(mScrollX + x, mScrollY + y);
31.
}
scrollBy()和scrollTo()的滚动不同点
scrollTo(x, y):通过invalidate使view直接滚动到参数x和y所标定的坐标scrollBy(x, y):通过相对于当前坐标的滚动。从上面代码中,很容以就能看出scrollBy()的方法体只有调用scrollTo()方法的一行代码,scrollBy()方法先对属性mScollX加上参数x和属性mScrollY加上参数y,然后将上述结果作为参数传入调用方法scrollTo()scrollBy()和scrollTo()的参数正负影响滚动问题
scrollBy()和scrollTo()在参数为负的时候,向坐标轴正方向滚动;当参数为正的时候,向坐标轴负方向滚动。而作为我们的认知,应该是参数为负的时候,向坐标轴负方向滚动;当参数为正的时候,向坐标轴正方向滚动。
那为什么这两个方法传入参数和引起的滚动方向和我们平常的认知不同呢?
下面就让我们带着这个问题跟随源码分析。如果不想从它的执行过程一步步的去分析,可以直接看本文的最后一段源码。
源码执行过程分析
因为scrollBy(x, y)方法体只有一行,并且是调用scrollTo(x, y),所以我们只要通过scrollTo(x, y)来进行分析就可以了。
在scrollTo(x, y)中,x和y分别被赋值给了mScrollX和mScrollY,最后调用了方法invalidate(true)。貌似到了这里就无路可走了,其实不然,我们知道invalidate这个方法会通知View进行重绘。
那么接下来,我们就可以跳过scrollTo(x, y)去分析View的draw()方法了。照例,在分析onDraw方法之前上一段源码片段:
001.
/**
002.
* Manually render this view (and all of its children) to the given Canvas.
003.
* The view must have already done a full layout before this function is
004.
* called. When implementing a view, implement
005.
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
006.
* If you do need to override this method, call the superclass version.
007.
*
008.
* @param canvas The Canvas to which the View is rendered.
009.
*/
010.
public
void
draw(Canvas canvas) {
011.
if
(ViewDebug.TRACE_HIERARCHY) {
012.
ViewDebug.trace(
this
, ViewDebug.HierarchyTraceType.DRAW);
013.
}
014.
015.
final
int
privateFlags = mPrivateFlags;
016.
final
boolean
dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
017.
(mAttachInfo ==
null
|| !mAttachInfo.mIgnoreDirtyState);
018.
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
019.
020.
/*
021.
* Draw traversal performs several drawing steps which must be executed
022.
* in the appropriate order:
023.
*
024.
* 1. Draw the background
025.
* 2. If necessary, save the canvas' layers to prepare for fading
026.
* 3. Draw view's content
027.
* 4. Draw children
028.
* 5. If necessary, draw the fading edges and restore layers
029.
* 6. Draw decorations (scrollbars for instance)
030.
*/
031.
032.
// Step 1, draw the background, if needed
033.
int
saveCount;
034.
035.
if
(!dirtyOpaque) {
036.
final
Drawable background = mBGDrawable;
037.
if
(background !=
null
) {
038.
final
int
scrollX = mScrollX;
039.
final
int
scrollY = mScrollY;
040.
041.
if
(mBackgroundSizeChanged) {
042.
background.setBounds(
0
,
0
, mRight - mLeft, mBottom - mTop);
043.
mBackgroundSizeChanged =
false
;
044.
}
045.
046.
if
((scrollX | scrollY) ==
0
) {
047.
background.draw(canvas);
048.
}
else
{
049.
canvas.translate(scrollX, scrollY);
050.
background.draw(canvas);
051.
canvas.translate(-scrollX, -scrollY);
052.
}
053.
}
054.
}
055.
056.
// skip step 2 & 5 if possible (common case)
057.
final
int
viewFlags = mViewFlags;
058.
boolean
horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) !=
0
;
059.
boolean
verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) !=
0
;
060.
if
(!verticalEdges && !horizontalEdges) {
061.
// Step 3, draw the content
062.
if
(!dirtyOpaque) onDraw(canvas);
063.
064.
// Step 4, draw the children
065.
dispatchDraw(canvas);
066.
067.
// Step 6, draw decorations (scrollbars)
068.
onDrawScrollBars(canvas);
069.
070.
// we're done...
071.
return
;
072.
}
073.
074.
/*
075.
* Here we do the full fledged routine...
076.
* (this is an uncommon case where speed matters less,
077.
* this is why we repeat some of the tests that have been
078.
* done above)
079.
*/
080.
081.
boolean
drawTop =
false
;
082.
boolean
drawBottom =
false
;
083.
boolean
drawLeft =
false
;
084.
boolean
drawRight =
false
;
085.
086.
float
topFadeStrength =
0
.0f;
087.
float
bottomFadeStrength =
0
.0f;
088.
float
leftFadeStrength =
0
.0f;
089.
float
rightFadeStrength =
0
.0f;
090.
091.
// Step 2, save the canvas' layers
092.
int
paddingLeft = mPaddingLeft;
093.
094.
final
boolean
offsetRequired = isPaddingOffsetRequired();
095.
if
(offsetRequired) {
096.
paddingLeft += getLeftPaddingOffset();
097.
}
098.
099.
int
left = mScrollX + paddingLeft;
100.
int
right = left + mRight - mLeft - mPaddingRight - paddingLeft;
101.
int
top = mScrollY + getFadeTop(offsetRequired);
102.
int
bottom = top + getFadeHeight(offsetRequired);
103.
104.
if
(offsetRequired) {
105.
right += getRightPaddingOffset();
106.
bottom += getBottomPaddingOffset();
107.
}
108.
109.
final
ScrollabilityCache scrollabilityCache = mScrollCache;
110.
final
float
fadeHeight = scrollabilityCache.fadingEdgeLength;
111.
int
length = (
int
) fadeHeight;
112.
113.
// clip the fade length if top and bottom fades overlap
114.
// overlapping fades produce odd-looking artifacts
115.
if
(verticalEdges && (top + length > bottom - length)) {
116.
length = (bottom - top) /
2
;
117.
}
118.
119.
// also clip horizontal fades if necessary
120.
if
(horizontalEdges && (left + length > right - length)) {
121.
length = (right - left) /
2
;
122.
}
123.
124.
if
(verticalEdges) {
125.
topFadeStrength = Math.max(
0
.0f, Math.min(
1
.0f, getTopFadingEdgeStrength()));
126.
drawTop = topFadeStrength * fadeHeight >
1
.0f;
127.
bottomFadeStrength = Math.max(
0
.0f, Math.min(
1
.0f, getBottomFadingEdgeStrength()));
128.
drawBottom = bottomFadeStrength * fadeHeight >
1
.0f;
129.
}
130.
131.
if
(horizontalEdges) {
132.
leftFadeStrength = Math.max(
0
.0f, Math.min(
1
.0f, getLeftFadingEdgeStrength()));
133.
drawLeft = leftFadeStrength * fadeHeight >
1
.0f;
134.
rightFadeStrength = Math.max(
0
.0f, Math.min(
1
.0f, getRightFadingEdgeStrength()));
135.
drawRight = rightFadeStrength * fadeHeight >
1
.0f;
136.
}
137.
138.
saveCount = canvas.getSaveCount();
139.
140.
int
solidColor = getSolidColor();
141.
if
(solidColor ==
0
) {
142.
final
int
flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
143.
144.
if
(drawTop) {
145.
canvas.saveLayer(left, top, right, top + length,
null
, flags);
146.
}
147.
148.
if
(drawBottom) {
149.
canvas.saveLayer(left, bottom - length, right, bottom,
null
, flags);
150.
}
151.
152.
if
(drawLeft) {
153.
canvas.saveLayer(left, top, left + length, bottom,
null
, flags);
154.
}
155.
156.
if
(drawRight) {
157.
canvas.saveLayer(right - length, top, right, bottom,
null
, flags);
158.
}
159.
}
else
{
160.
scrollabilityCache.setFadeColor(solidColor);
161.
}
162.
163.
// Step 3, draw the content
164.
if
(!dirtyOpaque) onDraw(canvas);
165.
166.
// Step 4, draw the children
167.
dispatchDraw(canvas);
168.
169.
// Step 5, draw the fade effect and restore layers
170.
final
Paint p = scrollabilityCache.paint;
171.
final
Matrix matrix = scrollabilityCache.matrix;
172.
final
Shader fade = scrollabilityCache.shader;
173.
174.
if
(drawTop) {
175.
matrix.setScale(
1
, fadeHeight * topFadeStrength);
176.
matrix.postTranslate(left, top);
177.
fade.setLocalMatrix(matrix);
178.
canvas.drawRect(left, top, right, top + length, p);
179.
}
180.
181.
if
(drawBottom) {
182.
matrix.setScale(
1
, fadeHeight * bottomFadeStrength);
183.
matrix.postRotate(
180
);
184.
matrix.postTranslate(left, bottom);
185.
fade.setLocalMatrix(matrix);
186.
canvas.drawRect(left, bottom - length, right, bottom, p);
187.
}
188.
189.
if
(drawLeft) {
190.
matrix.setScale(
1
, fadeHeight * leftFadeStrength);
191.
matrix.postRotate(-
90
);
192.
matrix.postTranslate(left, top);
193.
fade.setLocalMatrix(matrix);
194.
canvas.drawRect(left, top, left + length, bottom, p);
195.
}
196.
197.
if
(drawRight) {
198.
matrix.setScale(
1
, fadeHeight * rightFadeStrength);
199.
matrix.postRotate(
90
);
200.
matrix.postTranslate(right, top);
201.
fade.setLocalMatrix(matrix);
202.
canvas.drawRect(right - length, top, right, bottom, p);
203.
}
204.
205.
canvas.restoreToCount(saveCount);
206.
207.
// Step 6, draw decorations (scrollbars)
208.
onDrawScrollBars(canvas);
209.
}
在这段代码片中,我们直接定位到onDrawScrollBars(canvas)方法,找到了这个方法离真相就不远了。上源码:
001.
/**
002.
* <p>Request the drawing of the horizontal and the vertical scrollbar. The
003.
* scrollbars are painted only if they have been awakened first.</p>
004.
*
005.
* @param canvas the canvas on which to draw the scrollbars
006.
*
007.
* @see #awakenScrollBars(int)
008.
*/
009.
protected
final
void
onDrawScrollBars(Canvas canvas) {
010.
// scrollbars are drawn only when the animation is running
011.
final
ScrollabilityCache cache = mScrollCache;
012.
if
(cache !=
null
) {
013.
014.
int
state = cache.state;
015.
016.
if
(state == ScrollabilityCache.OFF) {
017.
return
;
018.
}
019.
020.
boolean
invalidate =
false
;
021.
022.
if
(state == ScrollabilityCache.FADING) {
023.
// We're fading -- get our fade interpolation
024.
if
(cache.interpolatorValues ==
null
) {
025.
cache.interpolatorValues =
new
float
[
1
];
026.
}
027.
028.
float
[] values = cache.interpolatorValues;
029.
030.
// Stops the animation if we're done
031.
if
(cache.scrollBarInterpolator.timeToValues(values) ==
032.
Interpolator.Result.FREEZE_END) {
033.
cache.state = ScrollabilityCache.OFF;
034.
}
else
{
035.
cache.scrollBar.setAlpha(Math.round(values[
0
]));
036.
}
037.
038.
// This will make the scroll bars inval themselves after
039.
// drawing. We only want this when we're fading so that
040.
// we prevent excessive redraws
041.
invalidate =
true
;
042.
}
else
{
043.
// We're just on -- but we may have been fading before so
044.
// reset alpha
045.
cache.scrollBar.setAlpha(
255
);
046.
}
047.
048.
049.
final
int
viewFlags = mViewFlags;
050.
051.
final
boolean
drawHorizontalScrollBar =
052.
(viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
053.
final
boolean
drawVerticalScrollBar =
054.
(viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL
055.
&& !isVerticalScrollBarHidden();
056.
057.
if
(drawVerticalScrollBar || drawHorizontalScrollBar) {
058.
final
int
width = mRight - mLeft;
059.
final
int
height = mBottom - mTop;
060.
061.
final
ScrollBarDrawable scrollBar = cache.scrollBar;
062.
063.
final
int
scrollX = mScrollX;
064.
final
int
scrollY = mScrollY;
065.
final
int
inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) ==
0
? ~
0
:
0
;
066.
067.
int
left, top, right, bottom;
068.
069.
if
(drawHorizontalScrollBar) {
070.
int
size = scrollBar.getSize(
false
);
071.
if
(size <=
0
) {
072.
size = cache.scrollBarSize;
073.
}
074.
075.
scrollBar.setParameters(computeHorizontalScrollRange(),
076.
computeHorizontalScrollOffset(),
077.
computeHorizontalScrollExtent(),
false
);
078.
final
int
verticalScrollBarGap = drawVerticalScrollBar ?
079.
getVerticalScrollbarWidth() :
0
;
080.
top = scrollY + height - size - (mUserPaddingBottom & inside);
081.
left = scrollX + (mPaddingLeft & inside);
082.
right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
083.
bottom = top + size;
084.
onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom);
085.
if
(invalidate) {
086.
invalidate(left, top, right, bottom);
087.
}
088.
}
089.
090.
if
(drawVerticalScrollBar) {
091.
int
size = scrollBar.getSize(
true
);
092.
if
(size <=
0
) {
093.
size = cache.scrollBarSize;
094.
}
095.
096.
scrollBar.setParameters(computeVerticalScrollRange(),
097.
computeVerticalScrollOffset(),
098.
computeVerticalScrollExtent(),
true
);
099.
switch
(mVerticalScrollbarPosition) {
100.
default
:
101.
case
SCROLLBAR_POSITION_DEFAULT:
102.
case
SCROLLBAR_POSITION_RIGHT:
103.
left = scrollX + width - size - (mUserPaddingRight & inside);
104.
break
;
105.
case
SCROLLBAR_POSITION_LEFT:
106.
left = scrollX + (mUserPaddingLeft & inside);
107.
break
;
108.
}
109.
top = scrollY + (mPaddingTop & inside);
110.
right = left + size;
111.
bottom = scrollY + height - (mUserPaddingBottom & inside);
112.
onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
113.
if
(invalidate) {
114.
invalidate(left, top, right, bottom);
115.
}
116.
}
117.
}
118.
}
119.
}
上述代码,我们直接定位到if (drawVerticalScrollBar || drawHorizontalScrollBar)结构语句块。在水平方向滚动与垂直方向滚动语句块中,能够找到一行关键性代码invalidate(left, top, right, bottom),接着上源码:
01.
/**
02.
* Mark the area defined by the rect (l,t,r,b) as needing to be drawn.
03.
* The coordinates of the dirty rect are relative to the view.
04.
* If the view is visible, {@link #onDraw(android.graphics.Canvas)}
05.
* will be called at some point in the future. This must be called from
06.
* a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
07.
* @param l the left position of the dirty region
08.
* @param t the top position of the dirty region
09.
* @param r the right position of the dirty region
10.
* @param b the bottom position of the dirty region
11.
*/
12.
public
void
invalidate(
int
l,
int
t,
int
r,
int
b) {
13.
if
(ViewDebug.TRACE_HIERARCHY) {
14.
ViewDebug.trace(
this
, ViewDebug.HierarchyTraceType.INVALIDATE);
15.
}
16.
17.
if
(skipInvalidate()) {
18.
return
;
19.
}
20.
if
((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) ||
21.
(mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID ||
22.
(mPrivateFlags & INVALIDATED) != INVALIDATED) {
23.
mPrivateFlags &= ~DRAWING_CACHE_VALID;
24.
mPrivateFlags |= INVALIDATED;
25.
mPrivateFlags |= DIRTY;
26.
final
ViewParent p = mParent;
27.
final
AttachInfo ai = mAttachInfo;
28.
//noinspection PointlessBooleanExpression,ConstantConditions
29.
if
(!HardwareRenderer.RENDER_DIRTY_REGIONS) {
30.
if
(p !=
null
&& ai !=
null
&& ai.mHardwareAccelerated) {
31.
// fast-track for GL-enabled applications; just invalidate the whole hierarchy
32.
// with a null dirty rect, which tells the ViewAncestor to redraw everything
33.
p.invalidateChild(
this
,
null
);
34.
return
;
35.
}
36.
}
37.
if
(p !=
null
&& ai !=
null
&& l < r && t < b) {
38.
final
int
scrollX = mScrollX;
39.
final
int
scrollY = mScrollY;
40.
final
Rect tmpr = ai.mTmpInvalRect;
41.
tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
42.
p.invalidateChild(
this
, tmpr);
43.
}
44.
}
45.
}
invalidate(left, top, right, bottom)方法体中,倒数第5行tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY)设置一个view需要绘制的脏矩形,这个方法的传入参数不觉得很奇怪吗?
mScrollX和mScrollY都是作为参数的减数(负负得正,负正得负),再结合开头的Android屏幕直角坐标系的概念,通过简单的逻辑分析或者计算就可以证明:当scrollTo()的传入参数为负的时候,view就向坐标轴正方向滚动;当为正的时候,view就向坐标轴负方向滚动。