自定义ViewGroup—回顾

作者: 韩明泽 | 来源:发表于2019-02-17 00:23 被阅读23次

    自定ViewGroup要比自定义View要复杂一点,因为自定义ViewGroup不仅测量自身还要测量子元素和及重写onLayout()来一次排列子View。下面这篇文章是关于自定义ViewGroup的一些基本知识,这些主要内容来自《android开发艺术探索》,在文章最后又这本书的网上版本。

    image

    目录

    • ViewGroup的measure过程
    • onMeasure()函数
    • onLayout()函数
    • 对Padding和Margin的处理
    • 在Activity中获取View的宽高

    ViewGroup的measure过程

    ViewGroup是一个抽象类,他没有重写View的onMeasure()方法。因此并没有定义具体的测量过程,具体的测量过程交给了他的子类来完成,比如:LinearLayoutRelativeLayout等。ViewGroup提供了一个measureChildren的方法来测量子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);
            }
        }
    }
    

    下面为measureChild()的源码:

    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //获取子元素宽度MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        //获取子元素高度MeasureSpec        
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //子元素进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    measureChild()方法会先获取子View的LayoutParams参数,然后再通过getChildMeasureSpec()获取子View的宽高MeasureSpec,最后将获取到的MeasureSpec传递给view的()方法进行测量。具体的执行过程我在《View的绘制流程》这篇文章中介绍过,这里就不在多少说了。

    onMeasure()方法

    因为ViewGroup没有重写View的onMeasure方法,我们在自定义的时候集成了ViewGruppo成了View的子类,因此要写自己布局的测量过则。那我上篇文章《自定义ViewGroup—FlowLayout》中的部分代码为例:

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       ...
                              //##1
        //循环遍历子View
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
    
            //先测量子View
            measureChild(childView, widthMeasureSpec, widthMeasureSpec);
            ....
            //计算剩余空间
            remaining = widthSize - usedWidth;
                            //##2
            //判断子view的测量宽度是否大于剩余空间,如果大于则另起一行
            if (childWidth > remaining) {
                //另起一行,已用宽度应该为零
                usedWidth = 0;
                //添加View
                mLineView = new LineView();
                mLineViewList.add(mLineView);
            }
            mLineView.addView(childView);
            //已用宽度累加
            usedWidth += childWidth;
            mLineView.setTotalWidth(usedWidth);
        }
                         //##3
        for (int i = 0; i < mLineViewList.size(); i++) {
            //总高度=所有行数相加
            totalHeight += mLineViewList.get(i).mHeight;
        }
                   
        //父容器的总高度=上下padding的高度,在最后额外加一个底部marginBottom
        totalHeight += getPaddingTop() + getPaddingBottom() + marginBottom;
                         //##4
        setMeasuredDimension(widthSize, heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight);
    }
    

    上面代码中主要是测量FlowLayout的高度。它是通过遍历子View(//##1)计算剩余宽度,再通过子View的宽度和剩余宽度比较来判断是否换行(//##2)。FlowLayout的高度如果是在warp_cotent模式下高度就为子View的行数乘上子View的高度(//##3),最后通过setMeasuredDimension()计算View的宽高并保存起来。

    onLayout()函数

    onLayout()函数式是ViewGroup的一个抽象函数,ViewGroup的子类必须实现该函数,用于定义View的摆放规则。

    注:该方法有一个指的探讨的问题,下面onLayout()是被@Override注释着的,也就是这个方法是复用了View的onLayout()方法。那么问题来了,java中父类的方法是否能把子类重写为抽象方法?这个问题我在好多技术群中向大神请教过,有的说能有的说不能。后来自己在项目中亲自测试,发现可以重写但是不能调用。如果读者知道这个问题,欢迎在下方留言。

    @Override
    protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
    

    该方法的调用是在View的layout被调用,我们可以查看ViewGroup的layout()放知道ViewGroup的layout过程是交给父类完成的。

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            //调用父类的layout方法
            super.layout(l, t, r, b);
        } else {
             transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }
    

    View的layout方法中调用了onLayout()方法

    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        ...
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
           ...
        }
      ...
    }
    

    对Padding和Margin的处理

    Padding的处理比较简单,只需要getPaddingXXX()来获取padding的值,在计算ViewGroup的宽高的时候将其加上即可:

    paddingLeft = getPaddingLeft();
    paddingTop = getPaddingTop();
    paddingRight = getPaddingRight();
    paddingBottom = getPaddingBottom();
    //ViewGroup宽高计算
    viewGroupWidth = paddingLeft + viewsWidth + paddingRight;
    viewGroupHeight = paddingTop + viewsHeight + paddingBottom;
    

    Margin的处理比较麻烦一点,首先他要先从子View中获取layoutParams属性,通过子View的LayoutParams属性来获取设置的Margin值。其layoutParams获取方法为childView.getLayoutParams()。要注意下面两点:

    1. 获取的要是MarginLayoutParams()类型,记得做类型强转。
    2. 重新generateLayoutParams(),返回类型为MarginLayoutParams类或他的子类。否则回报类型转换异常

    下面为实现代码:

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        
        //获取Margin值
        MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
        marginLeft = Math.max(marginLeft, lp.rightMargin);
        marginTop = Math.max(marginTop, lp.topMargin);
        marginRight = Math.max(marginRight, lp.rightMargin);
        marginBottom = lp.bottomMargin;
        
        //计算子View四个坐标位置
        int cLeft = left + marginLeft;
        int cRight = left + childWidth + marginRight;
        int cTop = top + marginTop;
        int cBottom = top + childHeight + marginBottom;
        //设置View的具体位置
        childView.layout(cLeft, cTop, cRight, cBottom);
    }
    //重写generateLayoutParams()
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
    

    在Activity中获取View的宽高

    android中不能再Activity的生命周期onCreate()onStart()onResume()生命周期中获取到View的宽高,这是因为Activity的生命周期和View的测量过程不是同步执行的。对于上面的问题有四种解决方案。下面为三种解决方法,第四种方案比较复杂就没写出来。

    onWindowFoucusChanged()中获取

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        int width = mFlowL_testCustom.getMeasuredWidth();
        int height = mFlowL_testCustom.getMeasuredHeight();
        Log.i(TAG, "onWindowFocusChanged()测量的宽为:" + width + "高为:" + height);
    }
    

    通过View的post方法,经请求发送到消息队列中执行。

    mFlowL_testCustom.post(new Runnable() {
        @Override
        public void run() {
            int width = mFlowL_testCustom.getMeasuredWidth();
            int height = mFlowL_testCustom.getMeasuredHeight();
            Log.i(TAG, "mFlowL_testCustom.post()测量的宽为:" + width + "高为:" + height);
        }
    });
    

    通过为ViewTreeObserver添加OnGlobalLayoutListener()来实现

    ViewTreeObserver treeObserver=mFlowL_testCustom.getViewTreeObserver();
        treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                int width = mFlowL_testCustom.getMeasuredWidth();
                int height = mFlowL_testCustom.getMeasuredHeight();
                Log.i(TAG, "addOnGlobalLayoutListener()测量的宽为:" + width + "高为:" + height);
            }
    });
    
    

    总结

    文章先写到这里吧!最近一直在坚持每天写技术笔记,希望能慢慢将这种坚持当成一种习惯。最后祝所有看到这篇文章的人工作顺利,工资翻番。

    参考

    Android开发艺术探索完结篇——天道酬勤

    相关文章

      网友评论

        本文标题:自定义ViewGroup—回顾

        本文链接:https://www.haomeiwen.com/subject/udyweqtx.html