Android View的距离和位置信息

来源:互联网 发布:传奇盛世7升8翅膀数据 编辑:程序博客网 时间:2024/04/29 22:02

Android View的坐标系

Android View的坐标系

Android View的坐标系是一个三维直角坐标系,其中X轴正方向向右,Y轴正方向向下,Z轴正方向则是垂直于屏幕向上。这和通常的三维直角坐标系并不相同,通常的三维直角坐标系是Y轴向上的右手坐标系,而Android View的坐标系则是一个Y轴向下的左手坐标系。如图所示。

这里写图片描述

由于这里只讨论Android View的坐标系,后文会将Android View的坐标系简称为Android坐标系。但需要知道的是,Android坐标系除了Android View的坐标系之外,还有其他的坐标系。例如传感器坐标系,OpenGL ES坐标系等。

Android坐标系的坐标原点

在Android坐标系中坐标原点的选取至关重要。屏幕中同样的位置,选择不同的的坐标原点会得到不同的坐标。一般来说把坐标原点放在屏幕左上角的坐标系称为绝对坐标系(即上图中所示的坐标系),把坐标原点放在其他位置的坐标系称为相对坐标系。后文中会对坐标原点的选取做更仔细的分析。

Android View的坐标

这里先说明一点,在Android坐标系中,只有和View关联起来的点的坐标和距离才有意义,例如一个View左上角的坐标,中心点的坐标,View左边线到Y轴的距离等。脱离View谈论坐标和距离是没有意义的。

在Android坐标系中,一个View中某个点的X轴和Y轴的坐标表示的是该点在对应坐标轴的投影到坐标原点的距离(也是通常意义上的坐标的定义),以像素为单位。但是由于屏幕是二维的,View中点并不能显示在屏幕外,所以Z轴的坐标并非表示该点在Z轴的投影到坐标原点的距离,而是表示该View的Z序(Z order)。

关于Z序,有如下几点需要了解。

  1. Z序表示的同一个Parent(Layout)中,层叠在一起的View的显示顺序。在同一个Layout中,如果有两个View的位置存在重叠,则Z序大的会显示在前面,Z序小的显示在后面,也就是说在两个View的重叠区域看到的将是Z序较大的View。
  2. 一个View的Z序只有在其Parent中才有意义,比较不同Parent中View的Z序是没有意义的。
    例如有如下布局,view1和view2并不在同一个parent中,view1的parent是layout1,view2的parent是layout2,所以比较view1和view2的Z序是没有意义的。即使view1的Z序大,view1也不能显示在view2上面。但是view1和layout2有同一个parent,所以可以比较view1和layout2的Z序。如果view1相比layout2的Z序大,则view1会显示在layout1前面,这时无论view2的Z序是多少,view1都会覆盖在view2的上面。

    <RelativeLayout    android:id="@+id/layout1"    android:layout_width="wrap_content"    android:layout_height="wrap_content">    <View        android:id="@+id/view1"        android:layout_width="wrap_content"        android:layout_height="wrap_content"/>    <FrameLayout        android:id="@+id/layout2"        android:layout_width="wrap_content"        android:layout_height="wrap_content">        <View            android:id="@+id/view2"            android:layout_width="wrap_content"            android:layout_height="wrap_content"/>    </FrameLayout></RelativeLayout>
  3. Z序是View的一个属性,同一个View中所有的点都有相同的Z序,而同一个View中不同点的X,Y的坐标是可以不一样的。

  4. 在Android 5.0之前的版本中,一个Layout中的View遵循自然Z序,也就是View的Z序等于它们在XML中定义的顺序。一个Layout中最先定义的View的Z序为0,之后依次加1。如果是通过代码添加的View,执行addView()时可以通过其index参数指定添加后View的Z序,如果不指定,则同样按照自然顺序,在上次View的Z序基础上加1。

    例如有如下布局,layout1是根View,它在其Parent中(即使是Activity的根Layout也有一个DecoView的Parent)的Z序为0,layout2是layout1中最先定义的View,其Z序为0。view1是layout2中最先定义的View,其Z序为0,view2的Z序则为1。view3和layout2同级,其Z序也为1。为layout2执行addView()添加一个新的View,并且不指定index,则新的View的Z序为2。

    <RelativeLayout    android:id="@+id/layout1"    android:layout_width="wrap_content"    android:layout_height="wrap_content">    <FrameLayout        android:id="@+id/layout2"        android:layout_width="wrap_content"        android:layout_height="wrap_content">        <View            android:id="@+id/view1"            android:layout_width="wrap_content"            android:layout_height="wrap_content"/>        <View            android:id="@+id/view2"            android:layout_width="wrap_content"            android:layout_height="wrap_content"/>    </FrameLayout>    <View        android:id="@+id/view3"        android:layout_width="wrap_content"        android:layout_height="wrap_content"/></RelativeLayout>
  5. 在Android 5.0及之后的版本中,引入了Elevation和TranslationZ两个属性,这两个属性会和View的自然Z序一起共同影响View的Z序。Elevation表示View在Z轴方向的高度,TranslationZ则表示属性动画中View在Z轴方向的平移距离。在Android 5.0及之后的版本中,会将Elevation和TranslationZ相加之和作为一个新的Z序,这里姑且将其称为ET Z序(外星人?)。一个Layout中的View的实际Z序会优先考虑ET Z序,如果ET Z序相同,再考虑自然Z序。也就是说在同一个Layout中,如果有两个View的位置存在重叠,则ET Z序大的会显示在前面,ET Z序小的显示在后面,如果ET Z序相同,则在比较两个View的自然Z序,自然Z序大的会显示在前面,自然Z序小的显示在后面。

    例如有如下布局,layout1是根View,它包含了view1和view2两个子View。view1先于view2定义,因此view1的自然Z序为0,view2的自然Z序为1。view1的elevation为2dp,translationZ为4dp,elevation和translationZ之和为6dp,因此view1的ET Z序为6dp,同样可以得到view2的ET Z序为5dp。由于ET Z序会被优先考虑,因此,尽管view2的自然Z序要大于view1的自然Z序,view1仍然会显示在view2前面。这时如果再通过代码为layout1添加一个新的view,假设为view3,将view3的translationZ设置为6dp,使得view3的ET Z序等于view1的ET Z序。由于view3的自然Z序为2,大于view1的自然Z序,因此view3会显示在view1前面。

    <FrameLayout    android:id="@+id/layout1"    android:layout_width="wrap_content"    android:layout_height="wrap_content">    <View        android:id="@+id/view1"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:translationZ="4dp"        android:elevation="2dp"/>    <View        android:id="@+id/view2"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:translationZ="1dp"        android:elevation="4dp"/></FrameLayout>

    还有几点需要注意。

    • 一个View的自然Z序是没有单位的,但是ET Z序是有单位的,它的单位是像素。
    • Elevation和TranslationZ两个属性除了会影响View的Z序外,它们还会影响View的背景阴影效果和动画属性。这里不做讨论。

以上是关于View Z序的一些概念和计算规则,但是Z序并完全不等同于Z轴坐标。

在Android5.0之前版本中,可以将View的自然Z序作为其Z轴坐标。获取View的自然Z序可以通过其parent的indexOfChild()方法。

在Android5.0及之后的版本中由于存在两套不同的Z序,且并没有一个简单的计算最终Z序的方法,所以View的Z轴坐标并不明确。由于ET Z序的优先级高于自然Z序,所以一般来说可以将ET Z序的值作为Z轴坐标来使用,或者可以将ET Z序的值乘以某个比较大的数(例如2的32次方),然后再加上自然Z序值作为最终的Z轴坐标。获取View的ET Z序可以通过其getZ()方法。

由于Z序只是用来决定View的显示顺序,假设view1和view2在同一个Layout中,对这两个View来说,要让view1显示在view2前面,只需要让view1的Z序大于view2即可。至于view1和view2的Z序是10和5,还是99和30并无区别(当然Elevation和TranslationZ的值对View的背景阴影和动画效果还是有影响的,不过不再本文的讨论范围)。由于Z序值只需要比较大小,所以Z轴坐标原点的选取也不重要。此外,大多数情况下同一个Layout中的两个View之间是不需要重叠显示的,如果两个View没有重叠,那么比较它们的Z序大小也是没有意义的。从这几点上来看,讨论View的Z轴实际坐标并没有太大意义,只需要知道Z序的计算规则即可,大可以忽略其在Z轴上坐标的概念。后文将不再讨论Z轴的坐标,坐标轴和坐标原点。

获取Android View的距离和位置

下面介绍Android View类中用来获取View的距离和位置信息的一些API,这些API大都需要在Attach到Window之后才能获取到预期的结果。所以不能放在构造方法,onCreate(),onMeasure(),onLayout()等方法中执行,一般都需要放到onWindowFocusChanged()里,或者放到某个异步事件中执行。

getTranslationX()/getTranslationY()/getTranslationZ()

TranslationX和TranslationY是从Android3.0引入的View的属性,TranslationZ则是Android5.0引入的View的属性。它们分别表示将View从其原始位置沿着X,Y,Z轴方向平移的距离。

getTranslationX()/getTranslationY()/getTranslationZ()分别用来获取view沿着X,Y,Z轴方向平移的距离。getTranslationX()和getTranslationY()只有在Android3.0及之后的系统才可以使用,getTranslationZ()只有在Android5.0及之后的系统中才可以使用。

将坐标系的坐标原点放到view原始位置的左上角,然后测量其平移之后的左上角位置的坐标,即可得到 getTranslationX()和getTranslationY()的结果。

将上述例子中的TextView向X平移50dp,向Y平移20dp,则TextView的getTranslationX()和getTranslationY()的结果如图所示。

这里写图片描述

getTop()/getBottom()/getLeft()/getRight()/getElevation()

getTop()/getBottom()/getLeft()/getRight()/getElevation()用来获取一个view相对其parent view的距离和位置信息。其中getElevation()只有在Android5.0及之后的系统中才可以使用,所以除非APP的最低支持的版本是Android5.0及以上,否则在调用getElevation()之前都需要判断下当前的系统版本。

将坐标系的坐标原点放在parent view的左上角,然后分别测量它的上下左右四条边线到坐标轴的距离,即可得到 getTop()/getBottom()/getLeft()/getRight()的结果。而getElevation()的结果则是之前在XML中通过android:elevation配置的值,或者是通过setElevation()设置的值。

假设有如下布局

<RelativeLayout    android:layout_width="match_parent"    android:layout_height="match_parent"    android:padding="10dp">    <FrameLayout        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:background="#B8B8B8"        android:padding="15dp">        <TextView            android:layout_width="100dp"            android:layout_height="55dp"            android:background="#FF0000"            android:text="测试"/>    </FrameLayout></RelativeLayout>

RelativeLayout中包含了一个FrameLayout,FrameLayout中包含了一个TextView,则TextView的getTop()/getBottom()/getLeft()/getRight()的结果如图所示。

这里写图片描述

需要注意的是,这里坐标原点在parent view当前位置的左上角,如果parent view有Translation, 则坐标原点在parent view平移之后的位置上。所以无论parent view是否有Translation,以及Translation的距离有多少,getTop()/getBottom()/getLeft()/getRight()/getElevation()的值都是不变的。

此外,getTop()/getBottom()/getLeft()/getRight()/getElevation()的值只和View在其parent中的位置有关,和它的可见性无关,无论View是INVISIBLE还是GONE,getTop()/getBottom()/getLeft()/getRight()/getElevation()的值都不会改变。

getX()/getY()/getZ()

getX()/getY()/getZ()同样是用来获取一个view到parent view的距离,和getTop()/getLeft()/getElevation()不同是,getX()/getY()/getZ()会考虑TranslationX,TranslationY和TranslationZ的影响。可以简单的将getTop()/getLeft()/getElevation()结果和getTranslationX()/getTranslationY()/getTranslationZ()结果相加,即可得到getX()/getY()/getZ()的结果。getX()和getY()只有在Android3.0及之后的系统才可以使用,getZ()只有在Android5.0及之后的系统中才可以使用。

将坐标系的坐标原点放在parent view的左上角,然后测量其平移之后的左上角位置的坐标,即可得到 getX()和getY()的结果。

同样的上述例子中的TextView向X平移50dp,向Y平移20dp,则TextView的getX()和getY()的结果如图所示。

这里写图片描述

同样,这里坐标原点在parent view当前位置的左上角,如果parent view有Translation, 则坐标原点在parent view平移之后的位置上。所以getX()/getY()/getZ()的值不受parent view的Translation的影响。

getX()/getY()/getZ()的值只和View在其parent中的位置有关,和它的可见性无关,无论View是INVISIBLE还是GONE,getX()/getY()/getZ()的值都不会改变。

getScrollX()/getScrollY()

getScrollX()/getScrollY()用来获取一个view滑动的距离,一般来说,如果View是不可滑动的,例如TextView,Button等, getScrollX()/getScrollY()获取到的始终是0。如果View是可以纵向滑动的,例如ListView,ScrollView等,getScrollX()获取到的始终是0,getScrollY()获取到的是Y轴滚动的距离。如果View是可以横向滑动的,例如HorizontalScrollView,getScrollY()获取到的始终是0,getScrollX()获取到的是X轴滚动的距离。getScrollX()和getScrollY()的值始终大于等于0。

上面所说的是一般情况。查看Android View类的源码,可以发现 getScrollX()返回的是mScrollX成员变量,getScrollY()返回的是mScrollY成员变量,这两个变量都是在scrollTo()方法中赋值的。View类中的scrollTo()方法定义如下,可以看到它直接将传入的参数赋值给了mScrollX和mScrollY。所以,如果对一个不可滑动的View,手动执行了scrollTo()方法,虽然并不能让这个View滑动起来,但是却可以改变mScrollX和mScrollY的值,从而改变getScrollX()和getScrollY()的值。例如mButton是一个Button,执行mButton.scrollTo(-5, -10);,然后再执行getScrollX()和getScrollY(),这时返回的就是-5和-10。但显然button并没有真的滑动一段距离。对可滑动的View,一般都重写了scrollTo()方法,所以并不会出现这类问题。例如mListView是一个ListView,执行mListView.scrollTo(-5, 22),然后再执行getScrollX()和getScrollY(),可以看到返回的结果分别是0和22,这是一个正确的结果。后文将不再考虑这种对一个不可滑动的View,手动执行了scrollTo()方法修改mScrollX和mScrollY值的情况。

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();        }    }}

同样,getScrollX()和getScrollY()的值不受当前View Translation的影响,也不受View可见性的影响。

getDrawingRect()

getDrawingRect()用来获取一个View的绘制区域,它本身没有返回值,需要传入一个不为null的Rect对象作为输出参数。一般来说,一个View的绘制区域就是从(0, 0)到(width, height)的区域,但如果View是可滑动的,则它的绘制区域还要加上滑动的距离,也就是从(scrollX, scrollY)到(width+scrollX, height+scrollY)的距离。

需要注意的是getDrawingRect()并没有考虑setScaleX(),setScaleY()和setRotation()的影响,所以它返回的区域大小并不一定会和View当前实际显示的区域大小相同。

getLocationOnScreen()和getLocationInWindow()

getLocationOnScreen()用来获取一个View在屏幕中的位置,而getLocationInWindow()用来获取一个View在其所在窗口中的位置。

这两个方法同样都是没有返回值,通过参数来输出结果。参数是一个长度为2的整形数组。示例代码如下。

int[] screenLocation = new int [2];int[] windowLocation = new int [2];getLocationInWindow();getLocationOnScreen(windowLocation)

getLocationOnScreen()和getLocationInWindow()返回的都是view左上角的坐标,不同的getLocationOnScreen()得到的是相对于屏幕的坐标,也就是坐标原点在屏幕的左上角。而getLocationInWindow()得到的是相对于当前窗口的坐标,也就是坐标原点在窗口的左上角。

仍然以之前的布局为例,对TextView执行getLocationOnScreen()结果如图所示。坐标x=screenLocation[0],y=screenLocation[1]。(这里借用上面示例代码中的变量名字)

这里写图片描述

接着来看对TextView执行getLocationInWindow()的结果。

由于getLocationInWindow()和getLocationOnScreen()的区别仅在于坐标原点的位置不同,知道了窗口的左上角的位置自然也就知道了getLocationInWindow()的结果。

对Activity来说,如果Activity是全屏的(在其style中配置了android:windowFullscreen=true或者在代码中设置了getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)),那么显然Activity对应窗口的左上角就在屏幕的左上角,所以这时getLocationInWindow()和getLocationOnScreen()的结果是完全一样的。

如果Activity不是全屏的,那么Activity的内容会显示在状态栏下方,就像上图中显示的那样。这时你也许会认为Activity对应窗口的左上角在状态栏下方,这时getLocationInWindow()和getLocationOnScreen()的结果中坐标x是一样的,坐标y会相差一个状态栏高度。如图所示。

这里写图片描述

然而事实上却并非如此,这时getLocationInWindow()和getLocationOnScreen()的结果仍然是完全一样的。这是因为对非全屏的Activity,Activity所在的Window以及最外层的View还是占满整个屏幕的。之所以Activity的内容会显示在状态栏下方,是因为对非全屏的Activity,Android会在最外层View的某个子View中插入了一个paddingTop,使得Activity布局对应的View(通过setContentView指定的布局文件对应的View)正好被放置在状态栏下方。

一般可以用如下代码查看这个padding值。注意这里的contentView并不是Activity布局对应的View,它和Activity布局对应的View之间仍然还隔着好几层的布局。

View decorView = getWindow().getDecorView();View contentView = ((FrameLayout) decorView).getChildAt(0);int padding = contentView.getPaddingTop();  

综上所述,无论Activity是否是全屏的,getLocationInWindow()和getLocationOnScreen()的结果都是一样的。但这只是对Activity,对Dialog来说,情况却并不是这样。

对Dialog来说,Dialog对应窗口的左上角一般并非在屏幕左上角,而是在屏幕中间的某个位置。仍然是上述布局,将其应用到一个居中显示的Dialog上。如图所示,这里为了看得更清楚,为对话框设置了一个蓝色的背景。

这里写图片描述

这时getLocationInWindow()的结果显然和getLocationOnScreen()的结果是不一样的。

需要注意的是,以上对Activity和Dialog执行getLocationInWindow()和getLocationOnScreen()结果的分析只是针对一般情况。事实上,Activity和Dialog都是Window,对Activity完全可以通过设置其窗口的LayoutParams让其像Dialog一样居中显示,对Dialog也可以通过设置其窗口的LayoutParams让其像Activity一样全屏显示,对这类情况也可以很容易的分析出getLocationInWindow()和getLocationOnScreen()的执行结果,这里就不再讨论了。

还有一点需要注意,getLocationInWindow()和getLocationOnScreen()结果反应的都是View左上角的坐标,这里的左上角是指该View在正常状态下的左上角,这里的正常状态是指没有经过旋转,缩放和平移之前的状态。

在上述例子中,将TextView旋转90°,然后缩放为0.5倍大小,X方向平移5px,Y方向平移50px。最终的左上角位置如图所示,getLocationInWindow()和getLocationOnScreen()结果反应的也都是这个位置的坐标。

这里写图片描述

和之前的API一样,getLocationInWindow()和getLocationOnScreen()结果不受View可见性的影响,但是如果View原本是VISIBILE的,然后将其设置为GONE,这可能会带来布局的变化。这种布局的变化可能会导致getLocationInWindow()和getLocationOnScreen()的结果在View为GONE时和View为VISIBLE时产生差异,这恰恰表明getLocationInWindow()和getLocationOnScreen()结果在View为GONE的时候也是可用的。

getGlobalVisibleRect()和getLocalVisibleRect()

0 0