Android绘制——自定义view之onLayout
简介
在自定义view的时候,其实很简单,只需要知道3步骤:
- 测量——onMeasure():决定View的大小,关于此请阅读《Android自定义控件之onMeasure》
- 布局——onLayout():决定View在ViewGroup中的位置
- 绘制——onDraw():如何绘制这个View。
View视图结构
View视图可以是单一的一个如TextView,也可以是一个视图组(ViewGroup)如LinearLayout。
如图:对于多View的视图他的结构是树形结构,最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View。
这个树的概念很重要,因为无论我们是在测量大小或是调整布局的时候都是从树的顶端开始一层一层,一个分支一个分支的进行(树形递归)。
onLayout函数
measure的作用就是为整个View树计算实际的大小,而通过刚才对View树的介绍知道,想计算整个View树的大小,就需要递归的去计算每一个子视图的大小(Layout同理)。
对每一个视图通过onMeasure方法的一系列测量流程后计算出实际的高(mMeasuredHeight)和宽(mMeasureWidth)传入setMeasuredDimension()方法完成单个View的测量,如果所测的视图是ViewGroup则可以通过measureChild方法递归的计算其中的每一个子view。对于每个View的实际宽高都是由父视图和本身视图决定的。
Layout的作用就是为整个View树计算实际的位置,而通过刚才对View树的介绍知道,想计算整个View树的位置,就需要递归的去计算每一个子视图的位置(Measure同理)。
而确定这个位置很简单,只需要mLeft,mTop,mRight,mBottom四个值(注意:这4个值是子View相对于父View的值,下面会详细介绍)。
在代码中如何设置这4个值呢?
首先,无论是系统提供的LinearLayout还是我们自定义的View视图,他都需要继承自ViewGroup类,之后必须要做的就是重写onLayout方法(因为在onLayout在ViewGroup中被定义为抽象方法)。
自定义view—onLayout
view类的onLayout()是个空方法
viewGroup的onLayout()是个抽象方法
layou()中的onLayout() 是用来设置viewgroup中子view的位置的 ,而不是用来设置当前view的位置的
/** * 存储所有的View,按行记录 */
private List> mAllViews = new ArrayList>()
/** * 记录每一行的最大高度 */
private ListmLineHeight = new ArrayList();
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
mAllViews.clear();
mLineHeight.clear();
int width = getWidth();
int lineWidth = 0; // 记录每一行 每加入一个子view之后的当前行宽
int lineHeight = 0 ; // 记录每一行 每加入一个子view之后的当前行高(取最大值)
ListlineViews = new ArrayList();
int cCount = getChildCount();
// 遍历所有的孩子
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 如果已经需要换行
if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width) {
// 记录这一行所有的View以及最大高度
mLineHeight.add(lineHeight);
// 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView mAllViews.add(lineViews);
lineWidth = 0;// 重置行宽
lineViews = new ArrayList();
}
/**
* 如果不需要换行,则累加
*/
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + lp.topMargin+ lp.bottomMargin);
lineViews.add(child);
}
// 记录最后一行
mLineHeight.add(lineHeight);
mAllViews.add(lineViews);
此循环小结
// 获取到所有的子view 以及子view的Marginlayoutparams
// 根据当前子view的宽度左右margin 以及当前行的lineWindth 判断是否换行
// 如果换行 则 将行高加入保存下来 并重置行宽行高以及行集合
// 并将行集合保存到总集合之中
// 如果不换行 则记录下当前行的行宽行高 并将当前view加入行集合
// 遍历完所有的集合之后将行高与行集合分别保存下来
// (因为遍历完所有的子view之后,最后一行肯定是不换行,所以行高和行集合都没有保存)
int left = 0;
int top = 0;
// 得到总行数
int lineNums = mAllViews.size();
for (int i = 0; i < lineNums; i++)
{
// 每一行的所有的views
lineViews = mAllViews.get(i);
// 当前行的最大高度
lineHeight = mLineHeight.get(i);
Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews);
Log.e(TAG, "第" + i + "行, :" + lineHeight);
// 遍历当前行所有的View
for (int j = 0; j < lineViews.size(); j++)
{
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE)
{
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
//计算childView的left,top,right,bottom
int lc = left + lp.leftMargin; 左
int tc = top + lp.topMargin; 上
int rc =lc + child.getMeasuredWidth(); 右
int bc = tc + child.getMeasuredHeight(); 下
Log.e(TAG, child + " , l = " + lc + " , t = " + t + " , r ="
+ rc + " , b = " + bc);
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
left = 0;
top += lineHeight;
}
}
此循环小结
之后遍历总集合 得到行集合 然后根据相应的下标获取到每一行的行高遍历行集合 得到每一行的子view 然后获取每个子view的 左上坐标 右下坐标 然后调用子view的layout()获取子view的左坐标 初始left为0 每次计算完之后 将当前view的宽度相加最后设置每个子view的layout() 。