Android自定义ViewGroup,onMeasure、onLayout,实现流式布局

举报
yd_221104950 发表于 2020/12/08 00:28:15 2020/12/08
【摘要】 其实自定义ViewGroup与自定义View很像。它们本质都是View,区别在于ViewGroup是用来组织显示View的。自定义ViewGroup也有几个关键的方法需要实现,而且onLayout方法是必须实现的。在自定义ViewGroup中我们常常需要重写onMeasure、onLayout,而onDraw一般不需要重写。 onMeasure(int widthMe...

其实自定义ViewGroup与自定义View很像。它们本质都是View,区别在于ViewGroup是用来组织显示View的。自定义ViewGroup也有几个关键的方法需要实现,而且onLayout方法是必须实现的。在自定义ViewGroup中我们常常需要重写onMeasure、onLayout,而onDraw一般不需要重写。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这个方法是用来测试量尺寸的,这两个参数是父布局传递过来的。如果你自定义的ViewGroup已经是根布局,但是它一样有父布局,因为android系统最后都会将其被添加到一个FrameLayout布局里。这个方法不仅要测量ViewGroup的尺寸,那还必须测量ViewGroup内每个视图。测试子视图把这两个参数传递过去,剩下的就是子视图自己的onMeasure方法来测量出它自己的尺寸。这方法的参数各自都包含了测量模式和具体尺寸大小。我们根据不同的测量模式来决定其具体的尺寸。具体的测量模式两三种:

  • UNSPECIFIED:父布局没有任何限制(在自定义View或ViewGroup中都不常用这个)
  • EXACTLY:父布局已经确定了确切的尺寸
  • AT_MOST:可以任意大小,直到指定确切的尺寸

我们只关注后面两种。这个模式的选定是根据xml布局文件中android:layout_width和android:layout_height的取值情况来决定的:

  • EXACTLY模式:
    (1)match_parent:占满整个父布局的宽或高
    (2)具体的数值:如300dp
  • AT_MOST模式:
    (1)wrap_content:期望内容多大,宽和高相应多大,反正是能够包裹住内容。这个模式给过来时,大小是不会像如期望一样大小,它就是整个父布局的大小,因为父布局没有办法知道你的内容有多大,你必须在onMeasure方法亲自确定其大小。

从两个参数取出各自的测量模式与尺寸:

int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);

  
 
  • 1
  • 2
  • 3
  • 4

对于EXACTLY模式,ViewGroup的尺寸直接用分离出来的尺寸。而对于AT_MOST模式的,分离出来的尺寸不能直接使用,因为它是传过来的尺寸是父布局的尺寸,明显不符合我们的要求。我们要根据需要,计算出ViewGroup中的子视图尺寸并累加起来作为最终的尺寸,在这种模式下,还要考虑各个视图之间的外边距margin(在自定义View,则只需要考虑内边距padding)。

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec); int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec); int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec); int rowWidth = 0;// 临时记录行宽 int rowHeight = 0;// 临时记录行高 int maxWith = 0; int maxHeight = 0; measureChildren(widthMeasureSpec, heightMeasureSpec);// 测量children的大小 int count = getChildCount(); if (count != 0) { for (int i = 0; i < count; i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin; int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin; if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight()) { // 换行 maxWith = Math.max(maxWith, rowWidth); rowWidth = childWidth; maxHeight += rowHeight; rowHeight = childHeight; } else { // 不换行 rowWidth += childWidth; rowHeight = Math.max(childHeight, rowHeight); } //最后一个控件 if (i == count - 1) { maxWith = Math.max(maxWith, rowWidth); maxHeight += rowHeight; } } } String widthModeStr = null; switch (measuredWidthMode) { case MeasureSpec.AT_MOST: widthModeStr = "AT_MOST"; if (count == 0) { mWidth = 0; } else { mWidth = maxWith + getPaddingLeft() + getPaddingRight(); } break; case MeasureSpec.EXACTLY: widthModeStr = "EXACTLY"; mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize; break; default: throw new IllegalStateException("Unexpected value: " + measuredWidthMode); case MeasureSpec.UNSPECIFIED: break; } String heightModeStr = null; switch (measuredHeightMode) { case MeasureSpec.AT_MOST: heightModeStr = "AT_MOST"; if (count == 0) { mHeight = 0; } else { mHeight = maxHeight + getPaddingTop() + getPaddingBottom(); } break; case MeasureSpec.EXACTLY: heightModeStr = "EXACTLY"; mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize; break; default: throw new IllegalStateException("Unexpected value: " + measuredHeightMode); case MeasureSpec.UNSPECIFIED: break; } setMeasuredDimension(mWidth, mHeight); String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize; Log.d("Layout尺寸", str); }


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

因为我们在处理AT_MOST模式时,必须获得每个子视图的外边距信息:

MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();

  
 
  • 1

所以必须加入以下代码,否则会报错:

 // 自定义ViewGroup必须要有以下这个方法,否则拿不到child的margin的信息 @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

onLayout(boolean changed, int l, int t, int r, int b)

在自定义View中一般不需要重写这个方法,但是在自定义ViewGroup中必须重写这个方法,因为ViewGroup是View的集合,必须处理它们的位置关系。

 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight()); ChildPosition pos = mChildPos.get(i); //设置View的左边、上边、右边底边位置 child.layout(pos.left, pos.top, pos.right, pos.bottom); } } }

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

小结

所以自定义ViewGroup还是相当简单的。因为我们只需要量好ViewGroup的视图,在测量过程中,比较值得注意的就是当模式是AT_MOST时,需要计算各个子View的尺寸再累加起来,得到就是ViewGroup的尺寸。onMeasure的参数都是父布局传给子控件的,这就是为什么在自定义ViewGroup中,测量子View时的这两个参数是来自定义的ViewGroup。最后就是就是在onLayout中布局子View的位置。

流式布局

我们继承ViewGroup定义一个流式布局CustomFlowLayout。在流式布局中,我额外增加了滚动的功能,因为当我们的内容超过我们自定义ViewGroup的可视范围后,就需要用到滚动功能。滚动功能的实现思路

  1. 首先重写onTouchEvent方法,消费滑动事件,根据我们的手指滑动的方向,来移动我们的视图。
  2. 添加上边界的检查,一旦到达上边界就不允许再滚动
  3. 添加下边界的检查,一旦到达下边界就不允许再滚动

那么在处理边界问题时,需要我们有一定的知识储备,就是以下这个方面的知识:

  1. android的事件传递方面,Activity->View group ->View,这可以了事件传递的顺序。
  2. android的事件传递机制,要了解好dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent这个方法的工作过程,明白谁消费了事件,谁传递了事件。
  3. 理解好scrollTo与scrollBy的区别,前者是绝对位置移动,后者是相对位置移动。
  4. 要理解getY与getRawY与getScrollY它们的坐标系的特点。
  5. 最后一点,要知道android屏幕的坐标系是无限大,布局视图并没有边界,我们的屏幕只是这个坐标系的一部分,显示出来的视图只是刚好在这一部分而已。、
  6. 我们滚动视图,实质是改变了视图整体的位置而已。

下面是一个完全的流式布局CustomFlowLayout:

package com.wong.customtextview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

import java.util.ArrayList;
import java.util.List;

public class CustomFlowLayout extends ViewGroup { private int mWidth = 0; private int mHeight = 0; private int realHeight; private boolean scrollable = false; private boolean isInterceptedTouch; /** * 判定为拖动的最小移动像素数 */ private int mTouchSlop; private int topBorder; // 上边界 private int bottomBorder;// 下边界 //记录每个View的位置 private List<ChildPosition> mChildPos = new ArrayList<ChildPosition>(); private static class ChildPosition { int left, top, right, bottom; public ChildPosition(int left, int top, int right, int bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } } public CustomFlowLayout(Context context) { this(context, null); } public CustomFlowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); ViewConfiguration configuration = ViewConfiguration.get(context); // 获取TouchSlop值,用于判断当前用户的操作是否是拖动 mTouchSlop = configuration.getScaledPagingTouchSlop(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec); int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec); int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec); int rowWidth = 0;// 临时记录行宽 int rowHeight = 0;// 临时记录行高 int maxWith = 0; int maxHeight = 0; measureChildren(widthMeasureSpec, heightMeasureSpec);// 测量children的大小 int count = getChildCount(); if (count != 0) { for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin; int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin; if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight()) { // 换行 maxWith = Math.max(maxWith, rowWidth); rowWidth = childWidth; maxHeight += rowHeight; rowHeight = childHeight; } else { rowWidth += childWidth; rowHeight = Math.max(childHeight, rowHeight); } //最后一个控件 if (i == count - 1) { maxWith = Math.max(maxWith, rowWidth); maxHeight += rowHeight; } } } String widthModeStr = null; switch (measuredWidthMode) { case MeasureSpec.AT_MOST: widthModeStr = "AT_MOST"; if (count == 0) { mWidth = 0; } else { mWidth = maxWith + getPaddingLeft() + getPaddingRight(); } break; case MeasureSpec.EXACTLY: widthModeStr = "EXACTLY"; mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize; break; default: throw new IllegalStateException("Unexpected value: " + measuredWidthMode); case MeasureSpec.UNSPECIFIED: break; } String heightModeStr = null; switch (measuredHeightMode) { case MeasureSpec.AT_MOST: heightModeStr = "AT_MOST"; if (count == 0) { mHeight = 0; } else { mHeight = maxHeight + getPaddingTop() + getPaddingBottom(); } break; case MeasureSpec.EXACTLY: heightModeStr = "EXACTLY"; mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize; break; default: throw new IllegalStateException("Unexpected value: " + measuredHeightMode); case MeasureSpec.UNSPECIFIED: break; } //真实高度 realHeight = maxHeight + getPaddingTop() + getPaddingBottom(); //测量高度 if (measuredHeightMode == MeasureSpec.EXACTLY) { scrollable = realHeight > mHeight; } else { scrollable = realHeight > measuredHeightSize; } if (scrollable) { // 初始化上下边界值 MarginLayoutParams lp1 = (MarginLayoutParams) getChildAt(0).getLayoutParams(); topBorder = getChildAt(0).getTop() - lp1.topMargin; if (measuredHeightMode == MeasureSpec.EXACTLY) { bottomBorder = realHeight - mHeight + getPaddingBottom(); } else { bottomBorder = realHeight - measuredHeightSize + getPaddingBottom(); } } setMeasuredDimension(mWidth, mHeight); String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize; Log.d("Layout尺寸", str); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mChildPos.clear(); int rowWidth = 0;// 临时记录行宽 int rowHeight = 0;// 临时记录行高 int maxWith = 0; int maxHeight = 0; int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin; int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin; if (childWidth + rowWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { // 换行 maxWith = Math.max(maxWith, rowWidth); rowWidth = childWidth; maxHeight += rowHeight; rowHeight = childHeight; mChildPos.add(new ChildPosition( getPaddingLeft() + mlp.leftMargin, getPaddingTop() + maxHeight + mlp.topMargin, getPaddingLeft() + childWidth - mlp.rightMargin, getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin )); } else { // 不换行 mChildPos.add(new ChildPosition( getPaddingLeft() + rowWidth + mlp.leftMargin, getPaddingTop() + maxHeight + mlp.topMargin, getPaddingLeft() + rowWidth + childWidth - mlp.rightMargin, getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin )); rowWidth += childWidth; rowHeight = Math.max(childHeight, rowHeight); } } // 布局每一个child for (int i = 0; i < count; i++) { View child = getChildAt(i); Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight()); ChildPosition pos = mChildPos.get(i); //设置View的左边、上边、右边底边位置 child.layout(pos.left, pos.top, pos.right, pos.bottom); } } // 自定义ViewGroup必须要有以下这个方法,否则拿不到child的margin的信息 @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } private float mLastYMove; private float currentY; @Override public boolean onTouchEvent(MotionEvent event) { if (scrollable) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: this.mLastYMove = event.getRawY(); break; case MotionEvent.ACTION_MOVE: this.currentY = event.getRawY(); int scrolledY = getScrollY(); float diff = Math.abs(this.mLastYMove - this.currentY); // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件 if (diff > mTouchSlop) { int dy = (int) (this.mLastYMove - this.currentY); if (scrolledY + dy < topBorder) { dy = 0; scrollTo(0, topBorder); return true; //最顶端,超过0时,不再下拉,要是不设置这个,getScrollY一直是负数 } else if (scrolledY + dy > bottomBorder) { dy = 0; scrollTo(0, bottomBorder); return true; } scrollBy(0, dy); this.mLastYMove = event.getRawY(); } break; } } return true; }
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252

文章来源: blog.csdn.net,作者:WongKyunban,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/weixin_40763897/article/details/110742374

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。