作为一个码农,在平时UI的各种要求中,流式布局应该是一种比较普遍的展示,通常来说就是:根据父控件给与的大小来进行合理的展示子控件。本文就是通过自定义ViewGroup的方式,进一步实现onMeasure和onLayout方法进行实现,需要你对View的测量,大小有一定的了解。
通常我们进行自定义View和ViewGroup时,基本上都会使用到该类,可能有些人会问onMeasure方法有什么作用,这里就不得不提一下View绘制的三大步骤:测量,布局,绘制。其中measure用来确定view的测量宽高,layout用来设置View的最终宽高和四个顶点的位置,而draw方法将View绘制到屏幕上。说道这里就不得不提一下ViewGroup的测量过程,因为下面会用到:ViewGroup时抽象类,除了测量自身外还需要测量子View的大小,因此ViewGroup么没有重写View的OnMeasure方法,但是提供了一个measureChildren(int widthMeasureSpec, int heightMeasureSpec)
方法用于测量子View,注意:该方法比较重要,因为接下来的流式布局需要使用到该类去测量子View的大小 看一下其中的实现方式吧.
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
可以看到,measureChildren
也是循环白努力child,然后调用measureChild
方法,再看一下该方法.
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
代码也比较简单,取出child的LayoutParams ,然后通过getChildMeasureSpec
方法来创建子元素的MeasureSpec
然后调用子元素的measure方法进行测量,好像有点偏了,下面就不继续深究了,毕竟说的是流式布局。
Latout的作用是ViewGroup用来确定子View的位置,当ViewGroup 的位置被确定以后。会调用onLayout来遍历所有的子View并调用其layout方法,在layout中又会调用onLayout方法。可以看出layout确定View的本身位置,内部调用onLayout确定子View的位置。简单看一下layout的源码,这里不深究:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
...
}
通过上面的源码我们可以看出,首先调用setFrame方法来确定View的四个顶点坐标。接下来调用onLayout方法测量子View ,View 的源码中并没有实现onLayout的具体方法,需要去自己实现,如果想看具体实现的话,可以看一下View的子类,例如LInearLayout等。就写到这,接下来看看具体源码吧。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int lastWidth=0;
int lastHeight=0;
int linesWidth = 0;
int linesHeight =0;
for(int i=0;iif(linesWidth+childWidth>widthSize-getPaddingLeft()-getPaddingRight()){
lastWidth=Math.max(linesWidth,lastWidth);
lastHeight+=linesHeight;
linesWidth=childWidth;
linesHeight=childHeight;
}else{
linesWidth+=childWidth;
linesHeight=Math.max(childHeight,linesHeight);
}
if(i==getChildCount()-1){
lastWidth=Math.max(linesWidth,lastWidth);
lastHeight+=linesHeight;
}
}
/**
* match_parent:Exactly
* wrap_content:wrap_content
*/
setMeasuredDimension(widthMode==MeasureSpec.EXACTLY?widthSize:lastWidth+getPaddingLeft()+getPaddingRight(),
heightMode==MeasureSpec.EXACTLY?heightSize:lastHeight+getPaddingTop()+getPaddingBottom());
}
首先遍历子View,获取View,调用measureChild方法来测量子View的大小,这里必须调用,否则测量出来的View宽高为0,然后获取View的实际宽高。根据屏幕宽度来判断子View是否满足于该行,若超出屏幕宽度则换下一行,高度相加,这里需要注意的是,当取到最后一个View,要最终测量一下实际ViewGroup所需要的宽高,然后调用setMeasuredDimension设计宽高,别忘记如果设置了padding,也要计算在内,这里要注意如果ViewGroup的宽高设置的是match_parent或者具体的值,则测量模式为Exactly,反之为wrap_content,如果是Exactly则直接赋值宽高原始宽高,否则赋值测量宽高。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
heightView.clear();
allView.clear();
List linesView=new ArrayList<>();
int linesWidth = 0;
int linesHeight =0;
int width=getWidth();
for(int i=0;iif(childWidth+linesWidth>width -getPaddingLeft()-getPaddingRight()){
heightView.add(linesHeight);
allView.add(linesView);
linesView=new ArrayList<>();
linesWidth=0;
linesHeight=childHeight;
}
linesWidth+=childWidth;
linesHeight=Math.max(linesHeight,childHeight);
linesView.add(child);
}
heightView.add(linesHeight);
allView.add(linesView);
allView.add(linesView);
//绘制每行的子View
int childWidth=getPaddingLeft();
int childHeight=getPaddingTop();
for(int i=0;i lv=allView.get(i);
int hv=heightView.get(i);
for(int j=0;j
通过layout方法来设置子view的位置。首先遍历子View,使用集合存储每一行的集合子View,存储每一行的高度,这里要注意存储到最后一行时,要注意存储该行。后续遍历每一行的子View,调用 child.layout(left,top,right,bottom)方法来循环设置子View位置,要记得一行结束,高度增加,下一行宽度初始化。
若父View还有其余情况,还可以自行设置,懂了上面的内容,再添加就会更加容易了,写此篇文章,仅用来记录自定义View中的一些技术点,希望自己的技术能够提高。