Android触摸滑动全解(三)——View坐标体系详解
当我们触摸屏幕上的View时,有时候想要获取此时View的一些属性状态,比如说在屏幕的坐标,或者相对于父布局的坐标,或者View的宽高等,但是由于View有很多属性,我们很苦恼不知道应该去选择哪个方法去调用,今天,我们就梳理一下View的坐标体系。
一、屏幕区域划分
Android系统的屏幕区域划分如图:
Android屏幕区域划分
获取上述区域宽高的方法
获取屏幕区域的宽高等尺寸获取:
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
应用程序App区域宽高等尺寸获取:
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
获取状态栏高度:
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
View布局区域宽高等尺寸获取:
Rect rect = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
二、View坐标轴
我们平时的开发工作中,一般都是在APP的区域,因此我们比较关系的是APP部分的坐标体系。
Android系统和我们平时接触的坐标轴不一样,它是以屏幕左上角为原点,向右为X正方向,向下为Y轴正方向,因此屏幕左上角坐标为(0,0)。
View坐标体系
1、View的尺寸和相对于父布局的位置
1.1 View的位置(相对于父布局)
1.1.1 View的初始位置(在XML中布局时的位置)
View中有四个属性:
protected int mLeft;
protected int mRight;
protected int mTop;
protected int mBottom;
其值由layout过程的四个参数(l,t,r,b)确定。这四个参数的设置一般会参考measure过程中测量出来的值。View的四个属性值表示layout过程中确定的基本位置。含义如下图所示,坐标系是父View的视图坐标:
View的位置
并且有四个方法获取它们:
-
view.getLeft()
:View左侧到父View左侧的距离。 -
view.getRight()
:View右侧到父View左侧的距离。 -
view.getTop()
:View上侧到父View上侧的距离。 -
view.getBottom()
:View下侧到父View上侧的距离。
可通过两个方法改变它们的值:
-
view.offsetLeftAndRight(int offset)
:改变mLeft
和mRight
的值,offset
为正View整体位置向右偏移,为负则向左偏移。 -
view.offsetTopAndBottom(int offset)
:改变mTop
和mBottom
的值,offset
为正View整体位置向下偏移,为负则向上偏移。
1.1.2 获取移动后的偏移量
View中还有两个方法可以设置View的偏移量,这两个方法可以改变当前View的位置:
-
view.setTranslationX(int offset)
:offset
为正View整体位置向右偏移,为负则向左偏移。 -
view.setTranslationY(int offset)
:offset
为正View整体位置向下偏移,为负则向上偏移。
相应的获取偏移量:
-
view.getTranslationX()
:获取View在X轴方向的偏移量。 -
view.getTranslationY()
:获取View在Y轴方向的偏移量。
1.1.3 获取View当前的位置
View中还有两个方法可以获取View当前的位置:
-
view.getX()
:获取View在X轴方向的当前位置,返回值为getLeft()
+getTranslationX()
,当setTranslationX()
时getLeft()
不变,getX()
变。 -
view.getY()
:获取View在Y轴方向当前位置,返回值为getTop()
+getTranslationY()
,当setTranslationY()
时getTop()
不变,getY()
变。
同样的,也可以通过setX()
和setY()
来改变getX
和getY()
的值,它们相当于设置setTranslationX(int offset)
和setTranslationY(int offset)
。
1.1.4 三种获取位置方法总结(以X轴举例)
-
view.getLeft()
:布局后View相对于父布局的原始距离。 -
view.getTranslationX()
:布局后如果View有移动,那么可通过此方法获取View移动后的偏移量。 -
view.getX()
:布局后如果View没有移动,那么此方法获取的值等同于getLeft()
,如果View有移动,此方法获取的值等同于getLeft()
+getTranslationX()
。
1.2 View的尺寸
View的尺寸也就是View的宽高,获取方法有两种:
1.2.1 获取宽高:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
从源码中,我们可以看到,实际上View获取宽高时也就是用mRight
减去mLeft
,mBottom
减去mTop
(所以说mRight
、mLeft
、mBottom
和mTop
的值永远不会改变)。
1.2.2 获取测量的宽高:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
这里mMeasuredWidth & MEASURED_SIZE_MASK
表示的是测量阶段结束之后,View真实的值。而且这个值会在measure()
调用了setMeasuredDimensionRaw()
函数之后会被设置。所以getMeasuredWidth()
的值是measure()
阶段结束之后得到的view的原始的值。
1.2.3 1和2中的两种方法比较和区别
-
我们知道,
measure()
方法是在layout()
方法之前调用的,因此,mMeasuredWidth
和mMeasuredHeight
值在measure()
后就被赋值,而getWidth()
和getHeight()
的值需要在layout()
之后才能得到。 -
由1得知,
getMeasuredWidth()
获取的是view原始的大小,也就是这个view在XML文件中配置或者是代码中设置的大小。getWidth()
获取的是这个view最终显示的大小,这个大小有可能等于原始的大小也有可能不等于原始大小。
1.2.4 Activity中无法获取View宽高的解决办法
Activity在onCreate()
、onStart()
和onResume()
时无法获取View的宽高,解决的办法一般有如下四种:
-
onWindowFocusChanged() :
在Activity或者View的onWindowFocusChanged()
中获取,其中hasFocus
表示当前窗口(Activity或者View)是否获取窗口,true
表示获取:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
L.i("onWindowFocusChanged : v_view1.getWidth():" + v_view1.getWidth()
+ " v_view1.getHeight():" + v_view1.getHeight());
}
-
view.post(runnable):
通过post可以将一个runnable投递到消息队列的尾部,然后等待UI线程Looper调用此runnable的时候,view也已经初始化好了。
v_view1.post(new Runnable() {
@Override
public void run() {
L.i("post(Runnable) : v_view1.getWidth():" + v_view1.getWidth()
+ " v_view1.getHeight():" + v_view1.getHeight());
}
});
-
ViewTreeObserver:
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取view的宽高一个很好的时机。需要注意的是,伴随着view树的状态改变等,onGlobalLayout会被调用多次。
v_view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
L.i("ViewTreeObserver : v_view1.getWidth():" + v_view1.getWidth()
+ " v_view1.getHeight():" + v_view1.getHeight());
}
});
-
view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对view进行measure来得到view的宽/高,这种情况比较复杂,这里要分情况处理,根据view的layoutparams来分:
MATCH_PARENT
:直接放弃,无法measure出具体的宽/高。原因很简单,根据view的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量处view的大小。
WRAP_CONTENT
:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
v_view1.measure(widthMeasureSpec, heightMeasureSpec);
具体数值(比如宽高都是100dp/px):
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
v_view1.measure(widthMeasureSpec, heightMeasureSpec);
2、View的相对屏幕的坐标
下面我们再来看看关于View获取屏幕中位置的一些方法,不过这些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。
View在屏幕中的坐标
如图所示,View1(绿色)在屏幕中的左上角和右下角坐标分别是(30,100)
和(440,200)
,View2(紫色)在屏幕中的左上角和右下角坐标分别是(30,250)
和(440,800)
,其中View2可见位置的右下角坐标是(440,720)
。
下面我们就给出上面这幅图涉及的View的一些坐标方法的结果(结果采用使用方法返回的实际坐标,不依赖上面实际绝对坐标转换,上面绝对坐标只是为了说明例子中的位置而已),如下:
View的方法 | View1的结果 | View2的结果 | 结论描述 |
---|---|---|---|
getLocalVisibleRect() | (0, 0, 410, 100) | (0, 0, 410, 470) | 获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。 |
getGlobalVisibleRect() | (30, 100, 440, 200) | (30, 250, 440, 720) | 获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。 |
getLocationOnScreen() | (30, 100) | (30, 250) | 坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。 |
getLocationInWindow() | (30, 100) | (30, 250) | 如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。 |
三、View移动自身或者内容的方法
3.1 改变自身的位置
改变自身的位置在方法前面其实已经介绍过了,就是下面几种:
-
view.offsetLeftAndRight(int offset)
:水平方向挪动View,offset
为正则x轴正向移动,移动的是整个View,getLeft()
会变的。 -
view.offsetTopAndBottom(int offset)
:垂直方向挪动View,offset
为正则Y轴向下移动,移动的是整个View,getTop()
会变的。 -
+
view.setTranslationX(int offset)
:水平方向挪动View,offset
为正则x轴正向移动,移动的是整个View,getLeft()
不会改变。 -
view.setTranslationY(int offset)
:水平方向挪动View,offset
为正则Y轴向下移动,移动的是整个View,getTop()
不会改变。 -
view.layout(int left, int top, int right, int bottom)
:重新布局View在父布局中的位置,此方法会改变getLeft()
等方法的值。 -
LayoutParams
:通过设置View的margin
值来改变自身的位置:LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mtv.getLayoutParams(); layoutParams.leftMargin = 20; layoutParams.bottomMargin = 20; mtv.setLayoutParams(layoutParams);
-
动画
:通过设置View的margin
值来改变自身的位置:
3.2 自身内容的滚动
滚动相关的方法只是改变View中内容的位置,而整体View在屏幕中的位置不会移动!
-
view.scrollTo(int x, int y)
:将View中内容(不是整个View)滑动到相应的位置,参考坐标原点为ParentView左上角,x,y表示滑动到的左上角坐标,为正则向xy轴反方向移动,反之同理。 -
view.scrollBy(int x, int y)
:将View中内容(不是整个View)相对滑动x,y的距离,为正则向xy轴反方向移动,反之同理。 -
view.setScrollX(int value)
:实质为scrollTo(int x, int y)
,只是改变X轴方向的内容。 -
view.setScrollY(int value)
:实质为scrollTo(int x, int y)
,只是改变Y轴方向的内容。 -
getScrollX()/getScrollY()
:获取当前滑动位置偏移量。 -
Scroller
:通过Scroller
类也可以实现View的滑动,并且Scroller
效果看起来更加顺滑自然,此类我们会在后续介绍。
scrollTo()和scrollBy()方法特别注意:如果你给一个ViewGroup调用scrollTo()方法滚动的是ViewGroup里面的内容,如果想滚动一个ViewGroup则再给他嵌套一个外层,滚动外层即可。
四、总结
- 我们知道了Android中屏幕各区域的划分以及获取屏幕各区域的方法。
- 我们知道了View在父布局位置的布局方式以及获取位置的方法。
- 我们知道了View得到宽高的两种方法的异同点。
- 我们知道了View在屏幕中的位置以及得到屏幕中位置坐标的方法。
- 我们知道了改变View自身位置和内容的方法。
网友评论