当前位置:   article > 正文

Android 自定义View 测量控件宽高、自定义viewgroup测量

Android 自定义View 测量控件宽高、自定义viewgroup测量

1、View生命周期以及View层

1.1、View生命周期

View的主要生命周期如下所示, 包括创建、测量(onMeasure)、布局(onLayout)、绘制(onDraw)以及销毁等流程。

自定义View主要涉及到onMeasure、onLayout和onDraw这三个过程,其中

(1)自定义View(继承自View类):主要实现onMeasure和onDraw,

(2)自定义ViewGroup(继承自ViewGroup类):主要实现onMeasure和onLayout。

1.2、View层级

View层级是一个树形结构。

onMeasure、onLayout和onDraw这三个过程都是按照View层级从上到下进行的:(1)ViewGroup主要负责onMeasure和onLayout,确定自身及其子View的大小和放置方式,例如LinearLayout通过onMeasure确定尺寸,通过onLayout对子View进行横向或者纵向布局;(2)View主要负责onMeasure和onDraw,例如TextView通过onMeasure确定自身尺寸,通过onDraw绘制文字。

2、View测量与MeasureSpec类

View测量中最难的一点就是如何根据View的LayoutParams参数确定其实际的宽高,如:

  1. android:layout_width="10dp"
  2. android:layout_width="match_parent"
  3. android:layout_width="wrap_content"

这三种情况,View的宽度究竟应该是多少?这就要从View的测量过程分析了,

2.1、MeasureSpec类

View类的内部类MeasureSpec用来辅助View的测量,使用一个int型变量measureSpec来表示View测量的模式和具体的尺寸(宽和高各一个measureSpec值)。measureSpec共32位,用高两位表示测量模式mode, 通过MeasureSpec.getMode(measureSpec)计算获得, 低30位表示尺寸size,通过MeasureSpec.getSize(measureSpec)计算获得。

mode共有三种情况:

MeasureSpec.UNSPECIFIED:不对View大小做限制,系统使用
MeasureSpec.EXACTLY:确切的大小,如:10dp
MeasureSpec.AT_MOST:大小不可超过某数值,最大不能超过其父类

2.2、父View的限制 :测量约束,限制最大宽度、最大高度等

View的测量过程受到父View的限制,如对一个ViewGroup测量时,其高度测量模式mode为EXACTLY,高度尺寸size为100dp,其子View的高度测量依据对应的android:layout_height参数来确定:

(1)具体尺寸值,如50dp,则该子View高度测量中mode为EXACTLY,尺寸为50dp;

(2)match_parent,则该子View高度和其父View高度相同,也是确定的,高度测量中mode为EXACTLY,尺寸为100dp;

(3)wrap_content, 则该子View最大高度为100dp, 确切高度需要根据内部逻辑确定,像TextView需要根据文字内容、宽度等综合确定,于是高度测量中mode为AT_MOST, 尺寸size为100dp。

其他情况类似,如父View的mode分别为AT_MOST、UNSPECIFIED,具体见下表:

高度测量中mode和size确定后,可通过MeasureSpec.makeMeasureSpec(size, mode)来确定heightMeasureSpec,widthMeasureSpec使用同样的方法确定。该方法的具体实现为ViewGroup.getChildMeasureSpec()方法。

2.3、子View的影响:实际测量

测量过程以LinearLayout作为例子说明:

(1) LinearLayout根据父View的measureSpec以及自身的LayoutParams确定了自身的widthMeasureSpec、heightMeasureSpec后, 调用measure(widthMeasureSpec, heightMeasureSpec) -----> onMeasure(widthMeasureSpec, heightMeasureSpec)来进行实际的测量;

(2) 当该LinearLayout方向为vertical时,实际测量中应该计算所有子View的高度之和,作为LinearLayout的测量高度needHeight;

(3) heightMeasureSpec中size为父类给该LinearLayout的限制高度,根据heightMeasureSpec中mode判断是取needHeight, 还是heightMeasureSpec中size, 然后调用setMeasuredDimension将测量的高度和宽度设置进去。

2.4、View的测量过程

Android中View测量是一种递归的过程(见下图),首先View调用measure方法,内部调用了自身的onMeasure方法,这个方法内部调用子View的measure方法(子View同样会调用自身的onMeasure方法),对子View进行测量,保存子View的测量尺寸,测量完所有的子View后再对自身测量,保存测量尺寸,之后便可以通过View.getMeasuredWidth()和View.getMeasuredHeight()来获取View的测量宽高。

3、自定义流式布局FlowLayout

主要思路:

对FlowLayout的所有子View逐个进行测量,获得measuredHeight和measuredWidth,在水平方向上根据这个尺寸依次对View进行放置,放不下则另起一行,每一行的高度取该行所有View的measuredHeight最大值。

3.1、单个子View测量
对其指定子View----child的测量代码如下,其中paddingLeft、paddingRight、paddingTop、paddingBottom分别是FlowLayout四边上的padding,widthMeasureSpec以及heightMeasureSpec是FlowLayout中onMeasure中的两个参数。

  1. int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
  2. paddingLeft + paddingRight, child.getLayoutParams().width);
  3. int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec,
  4. paddingTop + paddingBottom, child.getLayoutParams().height);
  5. child.measure(childWidthSpec, childHeightSpec);

于是子View的测量宽、高分别可以通过child.getMeasuredWidth() 和child.getMeasuredHeight()来进行获得。

3.2、onMeasure:测量与模拟布局View
  1. //子View的横向间隔、纵向间隔
  2. private final int horizontalSpace = dp2px(20);
  3. private final int verticalSpace = dp2px(10);
  4. //保存测量的子View, 每一个元素为一行的子View数组
  5. private final List<List<View>> allLines = new ArrayList<>();
  6. //记录每一行的最大高度,用于布局
  7. private final List<Integer> heights = new ArrayList<>();
  8. @Override
  9. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  10. allLines.clear();
  11. heights.clear();
  12. int paddingLeft = getPaddingLeft();
  13. int paddingRight = getPaddingRight();
  14. int paddingTop = getPaddingTop();
  15. int paddingBottom = getPaddingBottom();
  16. int usedWidth = 0;
  17. int height = 0;
  18. //父布局对FlowLayout的约束宽高
  19. int seftWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft -
  20. paddingRight;
  21. int seftHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop -
  22. paddingBottom;
  23. //FlowLayout的测量宽高
  24. int needHeight = 0;
  25. int needWidth = 0;
  26. List<View> line = new ArrayList<>();
  27. int count = getChildCount();
  28. for (int i = 0; i < count; i++) {
  29. View child = getChildAt(i);
  30. int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
  31. paddingLeft + paddingRight, child.getLayoutParams().width);
  32. int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec,
  33. paddingTop + paddingBottom, child.getLayoutParams().height);
  34. child.measure(childWidthSpec, childHeightSpec);
  35. if (usedWidth + horizontalSpace + child.getMeasuredWidth() > seftWidth) {
  36. //当前行无法在放下下一个view,则保存当前行的Views集合以及当前行的最大高度,
  37. heights.add(height + verticalSpace);
  38. allLines.add(line);
  39. //所有行的最大宽度
  40. needWidth = Math.max(needWidth, usedWidth);
  41. //所有行的高度之和
  42. needHeight += height + verticalSpace;
  43. //重置下一行的使用宽度、高度、View集合
  44. usedWidth = 0;
  45. height = 0;
  46. line = new ArrayList<>();
  47. }
  48. //获取当前行的最大高度,作为当前行的高度
  49. height = Math.max(height, child.getMeasuredHeight());
  50. //记录已经使用的宽度(第一个元素不需要加横向间隔
  51. usedWidth += child.getMeasuredWidth() + (line.size() == 0 ? 0 :
  52. horizontalSpace);
  53. //保存已经测量及模拟布局的View
  54. line.add(child);
  55. //记录最后一行的数据
  56. if (i == count - 1) {
  57. heights.add(height + verticalSpace);
  58. allLines.add(line);
  59. needWidth = Math.max(needWidth, usedWidth);
  60. needHeight += height + verticalSpace;
  61. }
  62. }
  63. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  64. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  65. //如果mode为MeasureSpec.EXACTLY, 则使用widthMeasureSpec中的size,
  66. //不然使用测量得到的size, 宽高同理
  67. int realWidth = widthMode == MeasureSpec.EXACTLY ? seftWidth : needWidth;
  68. int realHeight = heightMode == MeasureSpec.EXACTLY ? seftHeight : needHeight;
  69. //保存测量的宽和高
  70. setMeasuredDimension(realWidth + paddingLeft + paddingRight,
  71. //如果只有一行,不需要纵向间隔
  72. realHeight + paddingTop + paddingBottom - (allLines.size() > 0 ?
  73. verticalSpace : 0));
  74. }


3.3、布局:onLayout

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. int left = getPaddingLeft();
  4. int top = getPaddingTop();
  5. for (int i = 0; i < allLines.size(); i++) {
  6. List<View> line = allLines.get(i);
  7. for (int j = 0; j < line.size(); j++) {
  8. View child = line.get(j);
  9. child.layout(left, top, left + child.getMeasuredWidth(),
  10. top + child.getMeasuredHeight());
  11. //一行中View布局后每次向后移动child的测量宽 + 横向间隔
  12. left += child.getMeasuredWidth() + horizontalSpace;
  13. }
  14. //每一行布局从paddingLeft开始
  15. left = getPaddingLeft();
  16. //布局完成一行,向下移动当前行的最大高度
  17. top += heights.get(i);
  18. }
  19. }


3.4、测试
测试代码如下:

  1. private final List<String> words = Arrays.asList("家用电器", "手机", "运营商", "数码",
  2. "电脑", "办公", "电子书", "惠普星系列高清一体机", "格力2匹移动空调");
  3. @Override
  4. public void onCreate(@Nullable Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.layout_flow);
  7. FlowLayout layout = findViewById(R.id.flow_layout);
  8. for (int i = 0; i < words.size(); i++) {
  9. TextView textView = new TextView(this);
  10. textView.setText(words.get(i));
  11. textView.setBackground(ContextCompat.getDrawable(this,
  12. R.drawable.round_background));
  13. textView.setLayoutParams(new ViewGroup.LayoutParams(
  14. ViewGroup.LayoutParams.WRAP_CONTENT, FlowLayout.dp2px(60)));
  15. //textView.setLayoutParams(new ViewGroup.LayoutParams(
  16. // ViewGroup.LayoutParams.WRAP_CONTENT,
  17. // ViewGroup.LayoutParams.WRAP_CONTENT));
  18. int padding = FlowLayout.dp2px(5);
  19. textView.setPadding(padding, padding, padding, padding);
  20. layout.addView(textView);
  21. }
  22. }


效果图:


 


android中获取view在布局中的高度和宽度

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

这里贴一个比较好用的, AndroidUtilCode收藏的方法。

  1. public static int[] measureView(final View view) {
  2. ViewGroup.LayoutParams lp = view.getLayoutParams();
  3. if (lp == null) {
  4. lp = new ViewGroup.LayoutParams(
  5. ViewGroup.LayoutParams.MATCH_PARENT,
  6. ViewGroup.LayoutParams.WRAP_CONTENT
  7. );
  8. }
  9. int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width);
  10. int lpHeight = lp.height;
  11. int heightSpec;
  12. if (lpHeight > 0) {
  13. heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
  14. } else {
  15. heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
  16. }
  17. view.measure(widthSpec, heightSpec);
  18. return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()};
  19. }

核心代码:

  1. public class FlowLayout extends ViewGroup {
  2. //存放容器中所有的View
  3. private List<List<View>> mAllViews = newArrayList<List<View>>();
  4. //存放每一行最高View的高度
  5. private List<Integer> mPerLineMaxHeight = new ArrayList<>();
  6. public FlowLayout(Context context) {
  7. super(context);
  8. }
  9. public FlowLayout(Context context, AttributeSet attrs) {
  10. super(context, attrs);
  11. }
  12. public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr){
  13. super(context, attrs, defStyleAttr);
  14. }
  15. @Override
  16. protected LayoutParams generateLayoutParams(LayoutParams p) {
  17. return new MarginLayoutParams(p);
  18. }
  19. @Override
  20. public LayoutParams generateLayoutParams(AttributeSet attrs) {
  21. return new MarginLayoutParams(getContext(), attrs);
  22. }
  23. @Override
  24. protected LayoutParams generateDefaultLayoutParams() {
  25. return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
  26. }
  27. //测量控件的宽和高
  28. @Override
  29. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  30. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  31. //获得宽高的测量模式和测量值
  32. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  33. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  34. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  35. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  36. //获得容器中子View的个数
  37. int childCount = getChildCount();
  38. //记录每一行View的总宽度
  39. int totalLineWidth = 0;
  40. //记录每一行最高View的高度
  41. int perLineMaxHeight = 0;
  42. //记录当前ViewGroup的总高度
  43. int totalHeight = 0;
  44. for (int i = 0; i < childCount; i++) {
  45. View childView = getChildAt(i);
  46. //对子View进行测量
  47. measureChild(childView, widthMeasureSpec, heightMeasureSpec);
  48. MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
  49. //获得子View的测量宽度
  50. int childWidth = childView.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
  51. //获得子View的测量高度
  52. int childHeight = childView.getMeasuredHeight() + lp.topMargin +lp.bottomMargin;
  53. if (totalLineWidth + childWidth > widthSize) {
  54. //统计总高度
  55. totalHeight +=perLineMaxHeight;
  56. //开启新的一行
  57. totalLineWidth = childWidth;
  58. perLineMaxHeight = childHeight;
  59. } else {
  60. //记录每一行的总宽度
  61. totalLineWidth += childWidth;
  62. //比较每一行最高的View
  63. perLineMaxHeight =Math.max(perLineMaxHeight, childHeight);
  64. }
  65. //当该View已是最后一个View时,将该行最大高度添加到totalHeight中
  66. if (i == childCount - 1) {
  67. totalHeight +=perLineMaxHeight;
  68. }
  69. }
  70. //如果高度的测量模式是EXACTLY,则高度用测量值,否则用计算出来的总高度(这时高度的设置为wrap_content)
  71. heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
  72. setMeasuredDimension(widthSize, heightSize);
  73. }
  74. //摆放控件
  75. //1.表示该ViewGroup的大小或者位置是否发生变化
  76. //2.3.4.5.控件的位置
  77. @Override
  78. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  79. mAllViews.clear();
  80. mPerLineMaxHeight.clear();
  81. //存放每一行的子View
  82. List<View> lineViews = new ArrayList<>();
  83. //记录每一行已存放View的总宽度
  84. int totalLineWidth = 0;
  85. //记录每一行最高View的高度
  86. int lineMaxHeight = 0;
  87. /*********************************遍历所有View,将View添加到List<List<View>>集合中***************************************/
  88. //获得子View的总个数
  89. int childCount = getChildCount();
  90. for (int i = 0; i < childCount; i++) {
  91. View childView = getChildAt(i);
  92. MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams();
  93. int childWidth = childView.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
  94. int childHeight = childView.getMeasuredHeight() + lp.topMargin +lp.bottomMargin;
  95. if (totalLineWidth + childWidth > getWidth()) {
  96. mAllViews.add(lineViews);
  97. mPerLineMaxHeight.add(lineMaxHeight);
  98. //开启新的一行
  99. totalLineWidth = 0;
  100. lineMaxHeight = 0;
  101. lineViews = newArrayList<>();
  102. }
  103. totalLineWidth += childWidth;
  104. lineViews.add(childView);
  105. lineMaxHeight = Math.max(lineMaxHeight, childHeight);
  106. }
  107. //单独处理最后一行
  108. mAllViews.add(lineViews);
  109. mPerLineMaxHeight.add(lineMaxHeight);
  110. /**********************************遍历集合中的所有View并显示出来******************************************/
  111. //表示一个View和父容器左边的距离
  112. int mLeft = 0;
  113. //表示View和父容器顶部的距离
  114. int mTop = 0;
  115. for (int i = 0; i < mAllViews.size(); i++) {
  116. //获得每一行的所有View
  117. lineViews = mAllViews.get(i);
  118. lineMaxHeight = mPerLineMaxHeight.get(i);
  119. for (int j = 0; j < lineViews.size(); j++) {
  120. View childView =lineViews.get(j);
  121. MarginLayoutParams lp =(MarginLayoutParams) childView.getLayoutParams();
  122. int leftChild = mLeft +lp.leftMargin;
  123. int topChild = mTop +lp.topMargin;
  124. int rightChild = leftChild+ childView.getMeasuredWidth();
  125. int bottomChild = topChild +childView.getMeasuredHeight();
  126. //四个参数分别表示View的左上角和右下角
  127. childView.layout(leftChild,topChild, rightChild, bottomChild);
  128. mLeft += lp.leftMargin +childView.getMeasuredWidth() + lp.rightMargin;
  129. }
  130. mLeft = 0;
  131. mTop += lineMaxHeight;
  132. }
  133. }
  134. }

Android中View与ViewGroup获取内容宽高

1. 什么是内容的高度?


如图中,绿色的为View,Content为内容,如果View是ViewGroup,content可看做所有子节点


2. 为什么获取内容宽高

当我们自定义滑动时,期望滑动到内容最底部时,不能再往下滑动,故需要获取内容的宽高来限定。


3. 如何获取内容高度?

3.1 ViewGroup获取内容高度?(以竖直方向的LinearLayout为例)

不同的ViewGroup会有不同的内部规则,需要根据不同的ViewGroup通过不同的规则获取。

  1. linearLayout.post(new Runnable() {
  2. @Override
  3. public void run() {
  4. View last = linearLayout.getChildAt(linearLayout.getChildCount() - 1);
  5. int contentHeight = last.getTop() + last.getHeight() + linearLayout.getPaddingBottom();
  6. }
  7. });

 

3.2 View的内容高度获取(以TextView为例)

很多View的内容宽高是和View的宽高一致的,但是有些时候,会不统一,比如长文字,文字总高度高于 TextView的高度时。如果其他View需要获取内容高度与宽度,需要了解内部实现,并依据推算出获取方法。

  1. int contentHeight = textView.getLayout().getHeight() // 文字的高度
  2. + textView.getPaddingTop()
  3. + textView.getPaddingBottom();

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/381224
推荐阅读
相关标签
  

闽ICP备14008679号