Android 中流式布局讲解

Android 流式布局

作为一个码农,在平时UI的各种要求中,流式布局应该是一种比较普遍的展示,通常来说就是:根据父控件给与的大小来进行合理的展示子控件。本文就是通过自定义ViewGroup的方式,进一步实现onMeasure和onLayout方法进行实现,需要你对View的测量,大小有一定的了解。


onMeasure

通常我们进行自定义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方法进行测量,好像有点偏了,下面就不继续深究了,毕竟说的是流式布局。

onLayout

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<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)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;i<getChildCount();i++){

            View child=getChildAt(i);

            measureChild(child,widthSize,heightSize);


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

            int childWidth=child.getMeasuredWidth()+marginLayoutParams.leftMargin+marginLayoutParams.rightMargin;

            int childHeight=child.getMeasuredHeight()+marginLayoutParams.topMargin+marginLayoutParams.bottomMargin;


            if(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<View> linesView=new ArrayList<>();


        int linesWidth = 0;
        int linesHeight =0;

        int width=getWidth();

        for(int i=0;i<getChildCount();i++){

            View child=getChildAt(i);

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

            int childWidth=child.getMeasuredWidth()+marginLayoutParams.leftMargin+marginLayoutParams.rightMargin;

            int childHeight=child.getMeasuredHeight()+marginLayoutParams.topMargin+marginLayoutParams.bottomMargin;


            if(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<heightView.size();i++){

            List<View> lv=allView.get(i);

            int hv=heightView.get(i);

            for(int j=0;j<lv.size();j++){

                View child=lv.get(j);


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


                int left=marginLayoutParams.leftMargin+childWidth;

                int top=marginLayoutParams.topMargin+childHeight;

                int right=left+child.getMeasuredWidth();

                int bottom=top+child.getMeasuredHeight();

                child.layout(left,top,right,bottom);

                childWidth+=child.getMeasuredWidth()+marginLayoutParams.leftMargin+marginLayoutParams.rightMargin;

            }

            childWidth=getPaddingLeft();

            childHeight+=hv;
        }
    }

通过layout方法来设置子view的位置。首先遍历子View,使用集合存储每一行的集合子View,存储每一行的高度,这里要注意存储到最后一行时,要注意存储该行。后续遍历每一行的子View,调用 child.layout(left,top,right,bottom)方法来循环设置子View位置,要记得一行结束,高度增加,下一行宽度初始化。

最后

若父View还有其余情况,还可以自行设置,懂了上面的内容,再添加就会更加容易了,写此篇文章,仅用来记录自定义View中的一些技术点,希望自己的技术能够提高。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值