@TryLoveCatch
2022-05-06T11:31:16.000000Z
字数 11453
阅读 2526
Android知识体系
View 提供的获取坐标方法
MotionEvent 提供的获取坐标方法
View的setTranslationY()
,会改变translationX和translationY的值,但是不会更改margin
的值,所以getLeft()
和getRight()
不会改变。所以,我们从这里开得出一个结论:View的位置是会受translationX
和translationY
影响的,另外,setTranslationX(0)
会恢复TranslationX的值。为了使View的移动显得更为平滑,因此可以使用View的属性动画来指定translationX和translationY。
View提供了分别提供了getLeft()、getRight、getRight()、getBottom()四个方法获取对于的信息。除此之外3.0之后View还提供了四个比较重要的位置参数信息,X、Y、translationX、translationY。View的宽高是有top、left、right、bottom参数决定的
而X,Y和translationX,和translationY则负责View位置的改变。
https://www.jianshu.com/p/b22a33866736
获得 View 相对 父View 的坐标
view.getLeft();
view.getTop();
view.getRight();
view.getBottom();
获得点击事件处 相对点击控件 & 屏幕的坐标
motionEvent event;
event.getX();
event.getY();
event.getRawX();
event.getRawY();
获取控件 相对 窗口Window 的位置
int[] location = new int[2];
view.getLocationInWindow(location);
int x = location[0]; // view距离window 左边的距离(即x轴方向)
int y = location[1]; // view距离window 顶边的距离(即y轴方向)
// 注:要在onWindowFocusChanged()里获取,即等window窗口发生变化后
获得 View 相对 屏幕 的绝对坐标
int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0]; // view距离 屏幕左边的距离(即x轴方向)
int y = location[1]; // view距离 屏幕顶边的距离(即y轴方向)
// 注:要在view.post(Runable)里获取,即等布局变化后
View可见部分 相对于 屏幕的坐标。
Rect globalRect = new Rect();
view.getGlobalVisibleRect(globalRect);
globalRect.getLeft();
globalRect.getRight();
globalRect.getTop();
globalRect.getBottom();
View可见部分 相对于 自身View位置左上角的坐标。
Rect localRect = new Rect();
view.getLocalVisibleRect(localRect);
localRect.getLeft();
localRect.getRight();
localRect.getTop();
localRect.getBottom();
getWidth(): xxxx
getMeasuredWidth(): xxxx
measuredWidth 与 width 分别对应于视图绘制 的 measure 与 layout 阶段。measuredWidth 值在 View 的 measure 阶段决定的,是通过 setMeasuredDimension() 方法赋值的;width 值在 layout 阶段决定的,是由 layout() 方法决定的。有一点需要注意,通常来讲,View 的 width 和 height 是由 View 本身和 parent 容器共同决定的。
比如,View 通过自身 measure() 方法向 parent 请求 100x100 的宽高,那么这个宽高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 方法中,通过调用 childview.layout() 方法只分配给 childview 50x50 的宽高。那么,这个 50x50 宽高就是 childview 实际绘制并显示到屏幕的宽高,也就是 width 和 height 值。
我们经常会遇到,调用getMeasureWidth()
返回为0的问题,下面几种方法,可以解决这种问题
onWindowFocusChanged
onWindowFocusChanged
方法表示 View
已经初始化完毕了,宽高已经准备好了,这个时候去获取是没问题的。这个方法会被调用多次,当 Activity
继续执行或者暂停执行的时候,这个方法都会被调用,典型代码如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
View.post(runnable)
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
ViewTreeObsever
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
view.measure()
measure(0, 0)
,这个到底是什么意思呢?这个可以另写为measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
。而View.MeasureSpec.UNSPECIFIED
意味着,父视图不对子视图有任何约束,它可以达到所期望的任意尺寸,常见于ListView
和ScrollView
。那么为什么这么写就会得到数值呢?参考我的一篇文章测量(Measure)。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
所以,measure(0, 0)
返回的就是mMinWidth
或者mBackground.getMinimumWidth()
,而mMinWidth
默认貌似是100px
。
Context
的构造;如果需要布局中使用,则必须使用带有AttributeSet
参数的构造。其它的还有带有主题的构造。onFinishInflate()
:当一个View
和它的所有子View
从布局文件中inflate
完成后调用。onMeasure(int,int)
:测量当前View
和它的子View
的需要的尺寸大小onLayout(boolean,int,int,int,int)
:给当前View
的所有的子View
分配尺寸大小和位置onSizeChanged(int,int,int,init)
:当前View
的尺寸发生变化时调用 onDraw(Canvas)
:当View
需要渲染内容时调用(将我们需要的内容画到view
时调用) onKeyDown(int,KeyEvent)
:当前键盘(物理键)上某个键按下时调用onKeyUp(int,KeyEvent)
:当按下的键弹起时调用onTrackballEvent(MotionEvent)
:当轨迹球发生运动时调用onTouchEvent(MotionEvent)
:当触摸事件发生时调用 onFocusChanged(boolean,int,Rect)
:当焦点发生变化时调用(这里变化包括获取到焦点和失去焦点)onWindowFocusChanged(boolean)
:包含当前View
的Window
的焦点发生变化时调用(这里变化包括获取到焦点和失去焦点)onAttachedToWindow()
:当View
附着到Window
上时调用onDetachedFromWindow()
:当View
从Window
上分离时调用onWindowVisibilityChanged(int)
:包含当前View
的Window
的可见性发生改变时调用onSizeChanged(int, int, int, int)
:该方法在当前View
尺寸变化时被调用。onVisibilityChanged(View, int)
:该方法在当前View
或其祖先的可见性改变时被调用。结论:
View
默认为可见的,否则先调用onVisibilityChanged(
),但是此时该View
的长宽以及位置等信息都不知道。View
是invisible
,则不会回调onDraw()
方法View
是gone
,则不会回调onMeasure()
,onLayout()
,onDraw()
方法View
是什么状态,总是会回调onAttachedToWindow()
以及onDetachedFromWindow()
方法View
的销毁流程和可见性没有关系这里就会牵扯到这个问题,LayoutParameters没有效果
,这个就是设置时机的问题。
在不恰当的生命周期中指定LayoutParameters
,会被忽略掉,比如如下代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
view = new CusView(this);
view.setImageResource(R.drawable.ic_launcher);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(70, 70);
view.setLayoutParams(params);
setContentView(view);
}
正确的方法应该是放到 onWindowFocusChanged
方法获取到焦点后再指定LayoutParameters
,如下代码:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
view.setImageResource(R.drawable.ic_launcher);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(70, 70);
view.setLayoutParams(params);
}
}
这个其实和子线程如何更新UI-在 Activity#onResume() 及以前更新 View的原理类似,因为onResume之前,ViewRootImpl还没有设置为DecorView的parent,所以并不会更新UI。
关于onMeasure()
调用多次的问题,我们来看一下测试结果:
ViewGroup | 子View onMeasure() |
---|---|
1个RelativeLayout | 执行4次 |
2个RelativeLayout | 执行8次 |
ViewGroup | 子View onMeasure() |
---|---|
1个LinearLayout | 执行2次 |
2个LinearLayout | 执行4次 |
为什么会这样,下面会详细说明原因。
关于LinearLayout&RelativeLayout性能问题, 先说结论:
当
LinearLayout
和RelativeLayout
都能使用时,优先LinearLayout
,因为RelativeLayout
会让子View
调用至少2次onMeasure
,LinearLayout
有weight
时,才会让子多次调用onMeasure
.
我们看一下RelativeLayout
的onMeasure()
的源码:
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight);
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
if (isLayoutRtl()) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, myWidth - params.mLeft);
} else {
width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
}
} else {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, params.mRight);
} else {
width = Math.max(width, params.mRight + params.rightMargin);
}
}
}
if (isWrapContentHeight) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
height = Math.max(height, params.mBottom);
} else {
height = Math.max(height, params.mBottom + params.bottomMargin);
}
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
我们发现RelativeLayout会对子View做两次measure。这是为什么呢?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
如果不使用weight
属性,LinearLayout
会在当前方向上进行一次measure
的过程,如果使用weight
属性,LinearLayout
会避开设置过weight
属性的view
做第一次measure
,完了再对设置过weight
属性的view
做第二次measure
。
为什么这样?其实原因很简单,如果设置了weight
,那么就是需要把剩余的空间平分,所以肯定是需要先得到剩余空间,所以第一次measure
也是为了得到剩余空间,然后第二次measure
才能设置带有weight
的子view
。
如图所示:
这里需要注意,子View
调用requestLayout()
,会调用父容器的requestLayout()
,并逐层向上提交,直到根View
,相当于整个View树都重新执行了measure
、layout
、draw
这三个流程。
https://blog.csdn.net/weixin_44819566/article/details/124306567
https://blog.csdn.net/song_shao_hua/article/details/105652026
Android 获取 View 宽高的常用正确方式,避免为零
从源码的角度分析,getWidth() 与 getMeasuredWidth() 的不同之处
Android 生命周期 - View
View-相关介绍
View 的工作原理上 View 绘制流程梳理及 Measure 过程详解
调用view.measure(0,0)时发生了什么
自定义控件的测量探究
Android中RelativeLayout和LinearLayout性能分析
公共技术点之 View 绘制流程