@TryLoveCatch
2022-05-06T03:31:16.000000Z
字数 11453
阅读 3020
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)
@Overrideprotected void onStart() {super.onStart();view.post(new Runnable() {@Overridepublic void run() {int width=view.getMeasuredWidth();int height=view.getMeasuredHeight();}});}
ViewTreeObsever
@Overrideprotected void onStart() {super.onStart();ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic 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,会被忽略掉,比如如下代码:
@Overrideprotected 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,如下代码:
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {// TODO Auto-generated method stubsuper.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。所以需要横向纵向分别进行一次排序测量。
@Overrideprotected 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 0oldHeight = 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 绘制流程