[关闭]
@linux1s1s 2015-06-14T22:28:15.000000Z 字数 9135 阅读 3018

Android 坐标常识

AndroidView


大部分Android开发人员都对View的坐标感到有点迷茫,尤其是初学自定义View的同学,这里博主和同学们一起来学习一下自定义View开发中用到的必不可少的Android坐标问题:

View 坐标

废话少说,先上图:

此处输入图片的描述

上面这个图就是Android的系统坐标,它和笛卡尔坐标Y轴方向正好是反的,而X轴方向相同,当屏幕内容显示不下的时候,会超出一屏,于是就出现了ScrollView,在该类的父类View中有个重要方法:

  1. /**
  2. * Return the scrolled left position of this view. This is the left edge of
  3. * the displayed part of your view. You do not need to draw any pixels
  4. * farther left, since those are outside of the frame of your view on
  5. * screen.
  6. *
  7. * @return The left edge of the displayed part of your view, in pixels.
  8. */
  9. public final int getScrollX() {
  10. return mScrollX;
  11. }
  12. /**
  13. * Return the scrolled top position of this view. This is the top edge of
  14. * the displayed part of your view. You do not need to draw any pixels above
  15. * it, since those are outside of the frame of your view on screen.
  16. *
  17. * @return The top edge of the displayed part of your view, in pixels.
  18. */
  19. public final int getScrollY() {
  20. return mScrollY;
  21. }

关于这两个方法的大概意思如下:
就是这个view相对于“坐标系统原点”(见上图右侧)在Y轴或者X轴上的偏移量。

另外还有两个相当让人迷惑的方法:scrollTo(int x, int y)scrollBy(int x, int y)

  1. /**
  2. * Set the scrolled position of your view. This will cause a call to
  3. * {@link #onScrollChanged(int, int, int, int)} and the view will be
  4. * invalidated.
  5. * @param x the x position to scroll to
  6. * @param y the y position to scroll to
  7. */
  8. public void scrollTo(int x, int y) {
  9. 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. postInvalidateOnAnimation();
  18. }
  19. }
  20. }
  21. /**
  22. * Move the scrolled position of your view. This will cause a call to
  23. * {@link #onScrollChanged(int, int, int, int)} and the view will be
  24. * invalidated.
  25. * @param x the amount of pixels to scroll by horizontally
  26. * @param y the amount of pixels to scroll by vertically
  27. */
  28. public void scrollBy(int x, int y) {
  29. scrollTo(mScrollX + x, mScrollY + y);
  30. }

从上面的解释你肯定会这么理解:scrollTo方法会回调onScrollChanged(int, int, int, int)并且引发重绘,其中两个int参数分别是你希望滚动的位置。

针对上面的理解我们来做个测试:scrollTo(100,0)会向哪里移动?scrollTo(0,100)又会如何呢?

如果没有经过调试和深刻理解,肯定会出错,所以我们先来结合坐标图理解一下上面的问题:

如下图所示。注意,图中黄色矩形区域表示的是一个parent View,绿色虚线矩形为parent view中的内容。一般情况下两者的大小一致,本文为了显示方便,将虚线框画小了一点。图中的黄色区域的位置始终不变,发生位置变化的是显示的内容。注意图中scrollX(...)应该更换为scrollTo(...)

此处输入图片的描述
此处输入图片的描述
此处输入图片的描述

上面的配图是不是颠覆了你的原始理解,简直毁三观,那么为啥参数中正数向坐标原点靠拢,而负数却远离坐标原点?我们来看一下源代码,揭开它神秘面纱:
我们知道scrollTo(...)方法最后会引发重绘,我们直接看一下draw()方法即可:

  1. public void draw(Canvas canvas) {
  2. final int privateFlags = mPrivateFlags;
  3. final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
  4. (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
  5. mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
  6. /*
  7. * Draw traversal performs several drawing steps which must be executed
  8. * in the appropriate order:
  9. *
  10. * 1. Draw the background
  11. * 2. If necessary, save the canvas' layers to prepare for fading
  12. * 3. Draw view's content
  13. * 4. Draw children
  14. * 5. If necessary, draw the fading edges and restore layers
  15. * 6. Draw decorations (scrollbars for instance)
  16. */
  17. // Step 1, draw the background, if needed
  18. ...
  19. // Step 6, draw decorations (scrollbars)
  20. onDrawScrollBars(canvas);
  21. if (mOverlay != null && !mOverlay.isEmpty()) {
  22. mOverlay.getOverlayView().dispatchDraw(canvas);
  23. }
  24. }

为了看起来方便,上面绘制方法省略大部分无关代码,重点看一下L22 onDrawScrollBars(canvas);,接续跟下去

  1. protected final void onDrawScrollBars(Canvas canvas) {
  2. // scrollbars are drawn only when the animation is running
  3. final ScrollabilityCache cache = mScrollCache;
  4. ...
  5. if (invalidate) {
  6. invalidate(left, top, right, bottom);
  7. }
  8. }

这里同样省略了无关代码,重点看一下L6 invalidate(left, top, right, bottom);
继续跟进下去

  1. public void invalidate(int l, int t, int r, int b) {
  2. final int scrollX = mScrollX;
  3. final int scrollY = mScrollY;
  4. invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
  5. }

继续跟进下去,

  1. void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
  2. boolean fullInvalidate) {
  3. ...
  4. if (p != null && ai != null && l < r && t < b) {
  5. final Rect damage = ai.mTmpInvalRect;
  6. damage.set(l, t, r, b);
  7. p.invalidateChild(this, damage);
  8. }
  9. ...
  10. }

看到真相了,L4-L8主要是绘制脏矩形,这个矩形的参数就是上面invalidateInternal方法传递过去的,而这个参数的计算是l - scrollX, t - scrollY, r - scrollX, b - scrollY,居然使用减法,而不是加法,所以当scrollX是负数的时候,反而是往X正方向移动。

然后看一下scrollBy这个方法,理解了scrollTo方法一切就好解决了,scrollTo是计算相对位置,而scrollBy是绝对位置。

最后看一下左右方向的移动坐标如何计算,先上一张图:
此处输入图片的描述

整个坐标系是以手机屏幕左上角为原点(0,0),如果在屏幕没有滑动之前,这一理解肯定是ok的,但在滑屏之后,就会产生很多歧义和混淆,原因在于使用过程当中,很多方法的参数并非是参照屏幕,而是相对于父视图,对这整个过程和后面自定义控件的坐标变化带来的各个参数变化来说理解起来就并不那么适合了,最开始给人的感觉是越来越不清楚这个坐标该怎么设置,好像坐标系总在发生变化,后来才慢慢在思维当中构建起视图与视图容器以及屏幕之间的关系。

我们知道在View类中有个layout方法

  1. public void layout(int l, int t, int r, int b) {
  2. if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
  3. onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
  4. mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  5. }
  6. int oldL = mLeft;
  7. int oldT = mTop;
  8. int oldB = mBottom;
  9. int oldR = mRight;
  10. boolean changed = isLayoutModeOptical(mParent) ?
  11. setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
  12. if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
  13. onLayout(changed, l, t, r, b);
  14. mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
  15. ListenerInfo li = mListenerInfo;
  16. if (li != null && li.mOnLayoutChangeListeners != null) {
  17. ArrayList<OnLayoutChangeListener> listenersCopy =
  18. (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
  19. int numListeners = listenersCopy.size();
  20. for (int i = 0; i < numListeners; ++i) {
  21. listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
  22. }
  23. }
  24. }
  25. mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
  26. mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
  27. }

layout的过程就是确定View在屏幕上显示的具体位置,在代码中就是设置其成员变量mLeft,mTop,mRight,mBottom的值,这几个值构成的矩形区域就是该View显示的位置,不过这里的具体位置都是相对与父视图的位置。mLeft代表当前view.layout的这个view的左边缘离它的父视图左边缘的距离,拿上面“子视图2.layout(int l, int t, int r, int b) ”来说,它的父视图便是子视图1,2,3合起来形成的整个大矩形,那么这里将父视图的左上角定为(0,0),那么可以确定mLeft为一个子视图宽度320,以此类推,mTop指当前view的上边缘离父视图上边缘的距离。而以此为界,mRight所指的是当前view的右边缘离父视图左边缘的距离,一眼可以看出值为640(mLeft+自己的宽度),mBottom也是指当前view的下边缘离父视图的上边缘的距离。至于为何如此,大概是因为坐标系的缘故,坐标中的任何点都必须以(0,0)为起点,XY轴为衡量。

这四个方法所获取到的各个左上右下的值与layout的四个参数代表的是一样的,都是相对父视图的左边缘与上边缘。

这两个方法获取的是该view的高和宽,仅仅在滑动的情况下,或者说该view的大小如果不发生变化,它的值是不会变的。

说到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对函数之间的区别,getMeasuredWidth()、getMeasuredHeight()返回的是measure过程得到的mMeasuredWidth和mMeasuredHeight的值,而getWidth()和getHeight()返回的是mRight - mLeft和mBottom - mTop的值。一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子视图在父视图中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子视图的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子视图的具体位置。

MotionEvent 坐标

这个类中有两组关于坐标的方法

  1. /**
  2. * {@link #getX(int)} for the first pointer index (may be an
  3. * arbitrary pointer identifier).
  4. *
  5. * @see #AXIS_X
  6. */
  7. public final float getX() {
  8. return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
  9. }
  10. /**
  11. * {@link #getY(int)} for the first pointer index (may be an
  12. * arbitrary pointer identifier).
  13. *
  14. * @see #AXIS_Y
  15. */
  16. public final float getY() {
  17. return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
  18. }
  19. /**
  20. * Returns the original raw X coordinate of this event. For touch
  21. * events on the screen, this is the original location of the event
  22. * on the screen, before it had been adjusted for the containing window
  23. * and views.
  24. *
  25. * @see #getX(int)
  26. * @see #AXIS_X
  27. */
  28. public final float getRawX() {
  29. return nativeGetRawAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
  30. }
  31. /**
  32. * Returns the original raw Y coordinate of this event. For touch
  33. * events on the screen, this is the original location of the event
  34. * on the screen, before it had been adjusted for the containing window
  35. * and views.
  36. *
  37. * @see #getY(int)
  38. * @see #AXIS_Y
  39. */
  40. public final float getRawY() {
  41. return nativeGetRawAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
  42. }

对于getRawY()以及getRawX()这两个方法返回值表示:触摸点相对于屏幕原点的x坐标。
对于getX()以及getY()这两个方法返回值表示:触摸点相对于其所在组件原点的x坐标。

于是乎: getRawY() + getY() 就得到了view中的触摸点在Y轴上的偏移量。

TextView 坐标

这个类关于坐标的方法: getLayout(),其返回类型是Layout,也就是返回textView的布局。 然后通过这个布局可以得到在垂直方向上的Line。

  1. /**
  2. * @return the Layout that is currently being used to display the text.
  3. * This can be null if the text or width has recently changes.
  4. */
  5. public final Layout getLayout() {
  6. return mLayout;
  7. }

注意上面的这个Layout返回值有可能为空。然后通过这个布局可以进一步得到在垂直方向上的行数。

  1. /**
  2. * Get the line number corresponding to the specified vertical position.
  3. * If you ask for a position above 0, you get 0; if you ask for a position
  4. * below the bottom of the text, you get the last line.
  5. */
  6. // FIXME: It may be faster to do a linear search for layouts without many lines.
  7. public int getLineForVertical(int vertical) {
  8. int high = getLineCount(), low = -1, guess;
  9. while (high - low > 1) {
  10. guess = (high + low) / 2;
  11. if (getLineTop(guess) > vertical)
  12. high = guess;
  13. else
  14. low = guess;
  15. }
  16. if (low < 0)
  17. return 0;
  18. else
  19. return low;
  20. }

组件开发的一般步骤

如果需要自定义组件,一方面需要了解layout和measure的基本流程,更重要一方面,当你想要实现某一个效果的时候,比如slidingmenu那样的控件,查看源码我们可以知道它是继承的ViewGroup,该怎样入手去做呢。

总结下来,构建类似这样的控件,也就这三点,明确父子视图和屏幕的关系,通过坐标和位置参数设置它们的关系,处理这些关系发生变化的情况。

当然,事实上slidingmenu远远没这么简单,其中为了方便后续开发,它内置了很多接口和处理,大多数都是位置坐标和事件监听相关联,而万变不离其宗的是,它也肯定有这三个方面的构建,理解了这些基本的东西,尝试做一些自己想象的效果,对自定义的理解来说,进步会非常大。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注