[关闭]
@TryLoveCatch 2022-05-06T11:31:16.000000Z 字数 11453 阅读 2526

Android知识体系之自定义View基本知识

Android知识体系


坐标系相关

View的setTranslationY(),会改变translationX和translationY的值,但是不会更改margin的值,所以getLeft()getRight()不会改变。所以,我们从这里开得出一个结论:View的位置是会受translationXtranslationY影响的,另外,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位置的改变。

获取View的坐标位置

https://www.jianshu.com/p/b22a33866736

getLeft()、getTop()、getRight()、getBottom()

获得 View 相对 父View 的坐标

  1. view.getLeft();
  2. view.getTop();
  3. view.getRight();
  4. view.getBottom();

getX()、getY()、getRawX()、getRawY()

获得点击事件处 相对点击控件 & 屏幕的坐标

  1. motionEvent event;
  2. event.getX();
  3. event.getY();
  4. event.getRawX();
  5. event.getRawY();

getLocationInWindow()

获取控件 相对 窗口Window 的位置

  1. int[] location = new int[2];
  2. view.getLocationInWindow(location);
  3. int x = location[0]; // view距离window 左边的距离(即x轴方向)
  4. int y = location[1]; // view距离window 顶边的距离(即y轴方向)
  5. // 注:要在onWindowFocusChanged()里获取,即等window窗口发生变化后

getLocationOnScreen()

获得 View 相对 屏幕 的绝对坐标

  1. int[] location = new int[2];
  2. view.getLocationOnScreen(location);
  3. int x = location[0]; // view距离 屏幕左边的距离(即x轴方向)
  4. int y = location[1]; // view距离 屏幕顶边的距离(即y轴方向)
  5. // 注:要在view.post(Runable)里获取,即等布局变化后

getGlobalVisibleRect()

View可见部分 相对于 屏幕的坐标。

  1. Rect globalRect = new Rect();
  2. view.getGlobalVisibleRect(globalRect);
  3. globalRect.getLeft();
  4. globalRect.getRight();
  5. globalRect.getTop();
  6. globalRect.getBottom();

getLocalVisibleRect()

View可见部分 相对于 自身View位置左上角的坐标。

  1. Rect localRect = new Rect();
  2. view.getLocalVisibleRect(localRect);
  3. localRect.getLeft();
  4. localRect.getRight();
  5. localRect.getTop();
  6. localRect.getBottom();

小结

width和measuredWidth的区别

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 值。

正确获取measureWidth的值

我们经常会遇到,调用getMeasureWidth()返回为0的问题,下面几种方法,可以解决这种问题

  1. public void onWindowFocusChanged(boolean hasWindowFocus) {
  2. super.onWindowFocusChanged(hasWindowFocus);
  3. if(hasWindowFocus){
  4. int width=view.getMeasuredWidth();
  5. int height=view.getMeasuredHeight();
  6. }
  7. }
  1. @Override
  2. protected void onStart() {
  3. super.onStart();
  4. view.post(new Runnable() {
  5. @Override
  6. public void run() {
  7. int width=view.getMeasuredWidth();
  8. int height=view.getMeasuredHeight();
  9. }
  10. });
  11. }
  1. @Override
  2. protected void onStart() {
  3. super.onStart();
  4. ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
  5. viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
  6. @Override
  7. public void onGlobalLayout() {
  8. view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
  9. int width=view.getMeasuredWidth();
  10. int height=view.getMeasuredHeight();
  11. }
  12. });
  13. }
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  2. setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
  3. getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
  4. }
  5. protected int getSuggestedMinimumWidth() {
  6. return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
  7. }
  8. public static int getDefaultSize(int size, int measureSpec) {
  9. int result = size;
  10. int specMode = MeasureSpec.getMode(measureSpec);
  11. int specSize = MeasureSpec.getSize(measureSpec);
  12. switch (specMode) {
  13. case MeasureSpec.UNSPECIFIED:
  14. result = size;
  15. break;
  16. case MeasureSpec.AT_MOST:
  17. case MeasureSpec.EXACTLY:
  18. result = specSize;
  19. break;
  20. }
  21. return result;
  22. }

所以,measure(0, 0)返回的就是mMinWidth或者mBackground.getMinimumWidth(),而mMinWidth默认貌似是100px

View生命周期

结论:

LayoutParameters没有效果

这里就会牵扯到这个问题,LayoutParameters没有效果,这个就是设置时机的问题。
在不恰当的生命周期中指定LayoutParameters,会被忽略掉,比如如下代码:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. // setContentView(R.layout.activity_main);
  5. view = new CusView(this);
  6. view.setImageResource(R.drawable.ic_launcher);
  7. FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(70, 70);
  8. view.setLayoutParams(params);
  9. setContentView(view);
  10. }

正确的方法应该是放到 onWindowFocusChanged 方法获取到焦点后再指定LayoutParameters,如下代码:

  1. @Override
  2. public void onWindowFocusChanged(boolean hasFocus) {
  3. // TODO Auto-generated method stub
  4. super.onWindowFocusChanged(hasFocus);
  5. if (hasFocus) {
  6. view.setImageResource(R.drawable.ic_launcher);
  7. FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(70, 70);
  8. view.setLayoutParams(params);
  9. }
  10. }

这个其实和子线程如何更新UI-在 Activity#onResume() 及以前更新 View的原理类似,因为onResume之前,ViewRootImpl还没有设置为DecorView的parent,所以并不会更新UI。

onMeasure()调用多次 & LinearLayout、RelativeLayout性能

关于onMeasure()调用多次的问题,我们来看一下测试结果:

ViewGroup 子View onMeasure()
1个RelativeLayout 执行4次
2个RelativeLayout 执行8次
ViewGroup 子View onMeasure()
1个LinearLayout 执行2次
2个LinearLayout 执行4次

为什么会这样,下面会详细说明原因。

关于LinearLayout&RelativeLayout性能问题, 先说结论:

LinearLayoutRelativeLayout都能使用时,优先LinearLayout,因为RelativeLayout会让子View调用至少2次onMeasureLinearLayoutweight时,才会让子多次调用onMeasure.

RelativeLayout

我们看一下RelativeLayoutonMeasure()的源码:

  1. View[] views = mSortedHorizontalChildren;
  2. int count = views.length;
  3. for (int i = 0; i < count; i++) {
  4. View child = views[i];
  5. if (child.getVisibility() != GONE) {
  6. LayoutParams params = (LayoutParams) child.getLayoutParams();
  7. int[] rules = params.getRules(layoutDirection);
  8. applyHorizontalSizeRules(params, myWidth, rules);
  9. measureChildHorizontal(child, params, myWidth, myHeight);
  10. if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
  11. offsetHorizontalAxis = true;
  12. }
  13. }
  14. }
  15. views = mSortedVerticalChildren;
  16. count = views.length;
  17. final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
  18. for (int i = 0; i < count; i++) {
  19. View child = views[i];
  20. if (child.getVisibility() != GONE) {
  21. LayoutParams params = (LayoutParams) child.getLayoutParams();
  22. applyVerticalSizeRules(params, myHeight);
  23. measureChild(child, params, myWidth, myHeight);
  24. if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
  25. offsetVerticalAxis = true;
  26. }
  27. if (isWrapContentWidth) {
  28. if (isLayoutRtl()) {
  29. if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
  30. width = Math.max(width, myWidth - params.mLeft);
  31. } else {
  32. width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
  33. }
  34. } else {
  35. if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
  36. width = Math.max(width, params.mRight);
  37. } else {
  38. width = Math.max(width, params.mRight + params.rightMargin);
  39. }
  40. }
  41. }
  42. if (isWrapContentHeight) {
  43. if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
  44. height = Math.max(height, params.mBottom);
  45. } else {
  46. height = Math.max(height, params.mBottom + params.bottomMargin);
  47. }
  48. }
  49. if (child != ignore || verticalGravity) {
  50. left = Math.min(left, params.mLeft - params.leftMargin);
  51. top = Math.min(top, params.mTop - params.topMargin);
  52. }
  53. if (child != ignore || horizontalGravity) {
  54. right = Math.max(right, params.mRight + params.rightMargin);
  55. bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
  56. }
  57. }
  58. }

我们发现RelativeLayout会对子View做两次measure。这是为什么呢?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。

LinearLayout

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. if (mOrientation == VERTICAL) {
  4. measureVertical(widthMeasureSpec, heightMeasureSpec);
  5. } else {
  6. measureHorizontal(widthMeasureSpec, heightMeasureSpec);
  7. }
  8. }
  1. for (int i = 0; i < count; ++i) {
  2. final View child = getVirtualChildAt(i);
  3. if (child == null) {
  4. mTotalLength += measureNullChild(i);
  5. continue;
  6. }
  7. if (child.getVisibility() == View.GONE) {
  8. i += getChildrenSkipCount(child, i);
  9. continue;
  10. }
  11. if (hasDividerBeforeChildAt(i)) {
  12. mTotalLength += mDividerHeight;
  13. }
  14. LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
  15. totalWeight += lp.weight;
  16. if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
  17. // Optimization: don't bother measuring children who are going to use
  18. // leftover space. These views will get measured again down below if
  19. // there is any leftover space.
  20. final int totalLength = mTotalLength;
  21. mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
  22. } else {
  23. int oldHeight = Integer.MIN_VALUE;
  24. if (lp.height == 0 && lp.weight > 0) {
  25. // heightMode is either UNSPECIFIED or AT_MOST, and this
  26. // child wanted to stretch to fill available space.
  27. // Translate that to WRAP_CONTENT so that it does not end up
  28. // with a height of 0
  29. oldHeight = 0;
  30. lp.height = LayoutParams.WRAP_CONTENT;
  31. }
  32. // Determine how big this child would like to be. If this or
  33. // previous children have given a weight, then we allow it to
  34. // use all available space (and we will shrink things later
  35. // if needed).
  36. measureChildBeforeLayout(
  37. child, i, widthMeasureSpec, 0, heightMeasureSpec,
  38. totalWeight == 0 ? mTotalLength : 0);
  39. if (oldHeight != Integer.MIN_VALUE) {
  40. lp.height = oldHeight;
  41. }
  42. final int childHeight = child.getMeasuredHeight();
  43. final int totalLength = mTotalLength;
  44. mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
  45. lp.bottomMargin + getNextLocationOffset(child));
  46. if (useLargestChild) {
  47. largestChildHeight = Math.max(childHeight, largestChildHeight);
  48. }
  49. }

如果不使用weight属性,LinearLayout会在当前方向上进行一次measure的过程,如果使用weight属性,LinearLayout会避开设置过weight属性的view做第一次measure,完了再对设置过weight属性的view做第二次measure
为什么这样?其实原因很简单,如果设置了weight,那么就是需要把剩余的空间平分,所以肯定是需要先得到剩余空间,所以第一次measure也是为了得到剩余空间,然后第二次measure才能设置带有weight子view

requestLayout()和invalidate()的区别

如图所示:

这里需要注意,子View调用requestLayout(),会调用父容器的requestLayout(),并逐层向上提交,直到根View,相当于整个View树都重新执行了measurelayoutdraw这三个流程。

参考

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 绘制流程

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