安卓高级UI之自定义ViewGroup1.0

经过这一段时间的学习与整理,这次终于能把自定义控件做个小总结了,随着理解的加深,这篇文章一定会再次更新的。

一.自定义View的分类

自定义View分成两类:
①自定义View:可以称为控件
②自定义ViewGroup:可以称为组合

二.自定义View流程

在这里插入图片描述
注意:

①onMeasure()和onLayout()是用来布局的,onDraw()是用来绘制的,而且必须按这个顺序来。
②如果是自定义View,很少实现onLayout()方法
③如果是自定义ViewGroup,很少实现onDraw()方法,因为子控件都已经把自己绘制好了。除非绘制一个背景色之类的
所以:自定义View主要是实现onMeasure和onDraw方法
自定义ViewGroup主要是实现onMeasure和onLayout方法,而onLayout方法是必须实现的。

三.onMeasure方法详解

测量的时候用onMeasure

父控件会给自己一个参考,然后先测量自己的孩子,共同决定自己的大小,整个是一个递归的过程
在这里插入图片描述
所以:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //1.度量孩子

        //2.度量自己
    }

其中,widthMeasureSpec和heightMeasureSpec这两个参数是父亲的限制值,不能直接拿去测量孩子。

   override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //1.度量孩子
        var childCount = getChildCount()
        for(i in 0 until childCount){
            var childView = getChildAt(i)
            
            childView.measure(widthMeasureSpec,heightMeasureSpec)//这一步是错的
        }
        //2.度量自己
    }

比如这种就是错的,不能给孩子传递父亲的参考值,而应该传入自己的参考值

问题一

LayoutParams是什么?

它代表的就是layout_width和layout_height以及他们的值。所以首先要获得childView的LayoutParams
此时不能用measuredWidth和measuredHeight,因为还没有进行测量,就直接用测量之后的值,这样会报错的。

问题二

MeasureSpec是什么?

MeasureSpec是View中的内部类,基本都是二进制运算,由于int是32位的,所以用高2位表示mode,低30位表示size。MODE_SHIFT=30的作用是移位

在这里插入图片描述
其中,mode有三种

①UNSPECIFIED:不对View大小做限制(这种情况很少)
②EXACTLY:是确切的大小,如100dp
③AT_MOST:大小不可超过某数值,如match_parent是最大不能超过你爸爸

问题三:

LayoutParams如何转换到MeasureSpec
需要用到一个很重要的算法:getChildMeasureSpec()
部分源码展示(大概看看)

switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }

下面这两张图能对这个源码做个解释

这个图虽然很重要(虽然看不懂很正常)

这两张图对比着看,会恍然大悟
在这里插入图片描述
最后面三种情况,在安卓中,系统扮演“政府”的角色,所以我们无法决定,而这种情况我们基本也不用管。
下面再来解释下这个算法的参数

getChildMeasureSpec(int spec, int padding, int childDimension)

第一个参数是父亲的spec,因为它要和父亲去讨论
第三个参数是儿子的参数大小
第二个参数是父亲的padding,因为父亲和母亲也要生活,除了把他们的spec拿去讨论之外,也得把他们需要生活的钱(padding)拿去讨论,不能全拿走。

这样的话就完成了对孩子的测量,代码展示如下

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
		//首先测量父亲
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //1.度量孩子
        var childCount = getChildCount()
        for(i in 0 until childCount){
            var childView = getChildAt(i)
            //得到孩子的width和height
            var childLP: LayoutParams = childView.layoutParams

            var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
            var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)
            
			childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)//正确的
			//childView.measure(widthMeasureSpec,heightMeasureSpec)错误的
        }
        //2.度量自己
    }

四.例:流式布局

因为每一个自定义ViewGroup都要制定规则的,所以这里以流式布局为例,写一下自定义ViewGroup如何测量和布局

(1)测量onMeasure

1.保存一行中View的对象,行宽和行高

因为流式布局是一行一行地来布局的,所以用一个集合来保存一行中所有的View,另外,使用lineWidthUsed和lineHeight来保存当前行的行宽和行高。这些变量都是为了方便换行。

		//这里使用mutableList,是可变的
        var lineViews: MutableList<View> = mutableListOf()//保存一行中所有的view
        var mHorizontalSpacing = 30//右间距
        var mVerticalSpacing = 30//下间距
         var lineWidthUsed = mHorizontalSpacing //记录这一行已经使用了多宽的size
        var lineHeight = 0//记录这一行的行高

2.每次计算完之后对每个View进行保存

			//获取子view的宽和高
            var childMeasuredWidth = childView.measuredWidth
            var childMeasureHeight = childView.measuredHeight
           
            //因为view是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
            lineViews.add(childView)
            //每行都会有自己的宽和高
            lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing//最后一个参数是间距
            lineHeight = Math.max(lineHeight,childMeasureHeight + mVerticalSpacing )

3.判断是否需要换行

比较的依据是ViewGroup的限制

		//ViewGroup解析的宽度与高度(它的父亲给他的参考值)
        var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
        var selfHeight = MeasureSpec.getSize(heightMeasureSpec)
        //流式布局要多宽多高
        var parentNeededWidth = 0
        var parentNeededHeight = 0
		//通过宽度来判断是否需要换行,通过换行后的每行的行高来获取整个ViewGroup的行高
           if(lineWidthUsed + childMeasuredWidth + mHorizontalSpacing > selfWidth){
               //需要换行

               //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
               parentNeededHeight = parentNeededHeight + lineHeight
               parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
               //清0
               lineViews = mutableListOf()
               lineWidthUsed = 0
               lineHeight = 0
           }

4.判断最后一行是否有数据

		//判断最后一行是否有数据
        if(lineViews.size != 0){
            //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
            parentNeededHeight = parentNeededHeight + lineHeight
            parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)

            //需要换行
            allLines.add(lineViews)//把每一行的View记录下来
            lineHeights.add(lineHeight)//把行高保存下来

        }

5.最终的度量自己

		//2.度量自己
        //setMeasuredDimension(parentNeededWidth,parentNeededHeight)这样是错的,因为ViewGroup也是一个View布局,
        //它的大小也需要父亲提供给他的宽高来测量
        //所以应该是下面这5行
        var wideMode = MeasureSpec.getMode(widthMeasureSpec)
        var heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var realWidth = if(wideMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        setMeasuredDimension(realWidth,realHeight)

度量的全部代码

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
		//首先测量父亲
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //1.度量孩子
        var childCount = getChildCount()

        //这里使用mutableList,是可变的
        var lineViews: MutableList<View> = mutableListOf()//保存一行中所有的view
        var mHorizontalSpacing = 30//右间距
        var mVerticalSpacing = 30//下间距
        var lineWidthUsed = mHorizontalSpacing //记录这一行已经使用了多宽的size
        var lineHeight = mVerticalSpacing //记录这一行的行高

        //ViewGroup解析的宽度与高度(它的父亲给他的参考值)
        var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
        var selfHeight = MeasureSpec.getSize(heightMeasureSpec)

        //流式布局开始要多宽多高
        var parentNeededWidth = 0
        var parentNeededHeight = 0
        for(i in 0 until childCount){
            var childView = getChildAt(i)
            //得到孩子的width和height
            var childLP: LayoutParams = childView.layoutParams

            var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
            var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)

            childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)

            //获取子view的宽和高
            var childMeasuredWidth = childView.measuredWidth
            var childMeasureHeight = childView.measuredHeight

            //通过宽度来判断是否需要换行,通过换行后的每行的行高来获取整个ViewGroup的行高
            if(lineWidthUsed + childMeasuredWidth + mHorizontalSpacing > selfWidth){
                //需要换行

                //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
                parentNeededHeight = parentNeededHeight + lineHeight
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
                //清0
                lineViews = mutableListOf()
                lineWidthUsed = 0
                lineHeight = 0
            }
            //判断最后一行是否有数据
       	   if(lineViews.size != 0){
            //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
            parentNeededHeight = parentNeededHeight + lineHeight
            parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)

            //需要换行
            allLines.add(lineViews)//把每一行的View记录下来
            lineHeights.add(lineHeight)//把行高保存下来

        }

            //因为view是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
            lineViews.add(childView)
            //每行都会有自己的宽和高
            lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing//最后一个参数是间距
            lineHeight = Math.max(lineHeight,childMeasureHeight + mVerticalSpacing )
        }

        //2.度量自己
        //setMeasuredDimension(parentNeededWidth,parentNeededHeight)这样是错的,因为ViewGroup也是一个View布局,
        //它的大小也需要父亲提供给他的宽高来测量
        //所以应该是下面这5行
        var wideMode = MeasureSpec.getMode(widthMeasureSpec)
        var heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var realWidth = if(wideMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        setMeasuredDimension(realWidth,realHeight)
    }

易混点一:measureChild和getChildMeasureSpec、childView.measure的区别?

①看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);
    }

可以看到,measureChild方法里面,首先得到了子控件的layoutParams,然后得到了子控件宽和高的Spec,最后对子控件进行了测量。也就是说,measureChild方法里面包括了getChildMeasureSpec和measure方法

②此时我们再看一下流式布局时用到的测量步骤
			//得到孩子的width和height
            var childLP: LayoutParams = childView.layoutParams

            var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
            var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)

            childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)

可以看到,两者几乎是一样的,但是如果用measureChild方法,可能间距我们难以控制(现在我还没找到方法),但是如果使用getChildMeasureSpec和measure方法,就可以很轻松地自己完成对间距的设置。所以一般情况下都是使用后两个方法。

易混点二:getMeasureSpec和makeMeasureSpec的区别?

①首先看getMeasureSpec的源码,可以发现在最后一行,返回了makeMeasureSpec
		//noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
②getMeasureSpec是得到子控件的Spec,传入的参数是
getChildMeasureSpec(int spec, int padding, int childDimension)

即父容器的限制,间距以及子控件的宽度/高度,这个方法会根据这些参数,得到子控件宽度/高度的Spec,也就是说如果想让方法自己去计算子控件的Spec,就用getMeasureSpec,如果想自己“制作”Spec,那就使用makeMeasureSpec(不过这个用的不多)

(2)布局 onLayout

干货:getMeasuredWidth与getWidth的区别

①getMeasuredWidth是在measure()过程结束后就可以获取到对应的值,这个值是通过setMeasuredDimension()方法来设置的
②getWidth是在layout()过程结束后才能获取到的,通过视图右边的坐标减去左边的坐标计算出来的
所以在layout之前,别调用getWidth方法,因为调用的可能是错的(如果是第二次测量,有可能就对了,但是这只是侥幸)

首先看一下坐标系

在这里插入图片描述

1.创建全局变量

	 //记录所有的行,一行一行的存储,用于layout
    private var allLines: MutableList<MutableList<View>> = mutableListOf()
    //记录每一行的行高,用于layout
    var lineHeights: MutableList<Int> = mutableListOf()
	//并且在需要换行的if语句中
	//需要换行
    allLines.add(lineViews)//把每一行的View记录下来
    lineHeights.add(lineHeight)//把行高保存下来

2.进行一个控件的布局

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //获取到一共有多少行
        var lineCount = allLines.size

        //定义父母的“养老钱”,,也就是间距
        var curL = mHorizontalSpacing 
        var curT = mVerticalSpacing

        for(i in 0 until lineCount){
            //把这一行保存下来
            var lineViews: MutableList<View> = allLines.get(i)
            for(j in 0 until lineViews.size){
                var view = lineViews.get(j)
                var left = curL
                var top = curT

                var right = left + view.measuredWidth
                var bottom = top + view.measuredHeight
                
                view.layout(left,top,right,bottom)
            }
        }

3.完成所有控件的布局

 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //获取到一共有多少行
        var lineCount = allLines.size

        //定义父母的“养老钱”
       var curL = mHorizontalSpacing 
        var curT = mVerticalSpacing

        for(i in 0 until lineCount){
            //把这一行保存下来
            var lineViews: MutableList<View> = allLines.get(i)
            var lineHeight = lineHeights.get(i)
            for(j in 0 until lineViews.size){
                var view = lineViews.get(j)
                var left = curL
                var top = curT

                var right = left + view.measuredWidth
                var bottom = top + view.measuredHeight

                view.layout(left,top,right,bottom)
                curL = right + mHorizontalSpacing 
            }
            curL = mHorizontalSpacing 
            curT = curT + lineHeight//假设每一行与下面一行的间距是相等的
        }
    }

注意

构造函数就调用一次,也就是说ViewGroup就创建一次,但是onMeasure,onLayout或者onDraw可能调用多次,那么问题就来了,在调用第二次第三次第四次的时候,可能还在使用第一次定义的变量。所以要在onMeasure中进行某些量的初始化,而不是在构造函数中进行。

完整代码

class myViewGroup: ViewGroup {
    //记录所有的行,一行一行的存储,用于layout
    private var allLines: MutableList<MutableList<View>> = mutableListOf()
    //记录每一行的行高,用于layout
    var lineHeights: MutableList<Int> = mutableListOf()
    var mHorizontalSpacing = 30//右间距
    var mVerticalSpacing = 30//下间距

    constructor(context: Context, attrs: AttributeSet): super(context,attrs){}

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //1.度量孩子
        var childCount = getChildCount()

        //这里使用mutableList,是可变的
        var lineViews: MutableList<View> = mutableListOf()//保存一行中所有的view

        var lineWidthUsed = mHorizontalSpacing //记录这一行已经使用了多宽的size
        var lineHeight = mVerticalSpacing //记录这一行的行高

        //ViewGroup解析的宽度与高度(它的父亲给他的参考值)
        var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
        var selfHeight = MeasureSpec.getSize(heightMeasureSpec)

        //流式布局开始要多宽多高
        var parentNeededWidth = 0
        var parentNeededHeight = 0
        for(i in 0 until childCount){
            var childView = getChildAt(i)
            //得到孩子的width和height
            var childLP: LayoutParams = childView.layoutParams

            var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
            var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)

            childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)

            //获取子view的宽和高
            var childMeasuredWidth = childView.measuredWidth
            var childMeasureHeight = childView.measuredHeight

            //通过宽度来判断是否需要换行,通过换行后的每行的行高来获取整个ViewGroup的行高
            if(lineWidthUsed + childMeasuredWidth + mHorizontalSpacing > selfWidth){
                //需要换行

                //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
                parentNeededHeight = parentNeededHeight + lineHeight
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)

                //需要换行
                allLines.add(lineViews)//把每一行的View记录下来
                lineHeights.add(lineHeight)//把行高保存下来

                //清0
                lineViews = mutableListOf()
                lineWidthUsed = 0
                lineHeight = 0
            }

            //因为view是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
            lineViews.add(childView)
            //每行都会有自己的宽和高
            lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing//最后一个参数是间距
            lineHeight = Math.max(lineHeight,childMeasureHeight + mVerticalSpacing )
        }

        //判断最后一行是否有数据
        if(lineViews.size != 0){
            //一旦换行,就可以判断当前行的宽和高了,所以需要记录下来
            parentNeededHeight = parentNeededHeight + lineHeight
            parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)

            //需要换行
            allLines.add(lineViews)//把每一行的View记录下来
            lineHeights.add(lineHeight)//把行高保存下来

        }
        //2.度量自己
        //setMeasuredDimension(parentNeededWidth,parentNeededHeight)这样是错的,因为ViewGroup也是一个View布局,
        //它的大小也需要父亲提供给他的宽高来测量
        //所以应该是下面这5行
        var wideMode = MeasureSpec.getMode(widthMeasureSpec)
        var heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var realWidth = if(wideMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        setMeasuredDimension(realWidth,realHeight)
    }
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //获取到一共有多少行
        var lineCount = allLines.size

        //定义父母的“养老钱”
        var curL = mHorizontalSpacing
        var curT = mVerticalSpacing

        for(i in 0 until lineCount){
            //把这一行保存下来
            var lineViews: MutableList<View> = allLines.get(i)
            var lineHeight = lineHeights.get(i)
            for(j in 0 until lineViews.size){
                var view = lineViews.get(j)
                var left = curL
                var top = curT

                var right = left + view.measuredWidth
                var bottom = top + view.measuredHeight

                view.layout(left,top,right,bottom)
                curL = right + mHorizontalSpacing
            }
            curL = mHorizontalSpacing
            curT = curT + lineHeight//假设每一行与下面一行的间距是相等的
    get    }
    }
}

(3)总结

我认为从后往前写是比较容易的,除非你写的次数多了,记住了从前往后怎么写。
举个例子,比如在onMeasure里面,我们知道最后要进行子控件的measure,传入的是子控件的Spec,那就要用getChildMeasureSpec方法来得到子控件的Spec,而getChildMeasureSpec方法需要的参数是父亲的限制,间距以及子控件的宽度,所以要得到子控件的layoutParams,要得到子控件的layoutParams,就要得到子控件,所以总体思路就出来了。

五.自定义ViewGroup的三种情况

①父容器大小是定的,来决定子容器的大小

class MyViewGroup:ViewGroup {
    private val space = 30

    constructor(context:Context,attrs:AttributeSet?):super(context, attrs){
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //测量自己 size mode  -> 提供给子控件进行测量自己的(限制条件)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        //获取父容器的size
        val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
        val parentHeight = MeasureSpec.getSize(heightMeasureSpec)

        //确定子控件的尺寸
        var childWidth = 0
        var childHeight = 0

        if (childCount == 1) {
            childWidth = parentWidth - 2 * space
            childHeight = parentHeight - 2 * space
        }else{
            childWidth = (parentWidth - 3 * space)/2
            //计算有多少行
            val row = (childCount+1)/2
            childHeight = (parentHeight - (row+1)*space)/row
        }

        //将尺寸设置给子控件
        //先确定限制条件 MeasureSpec EXACTLY AT_MOST UNSPECIFIC
        val wspec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY)
        val hspec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            child.measure(wspec, hspec)
        }
    }
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //对子控件进行布局
        var left = 0
        var top = 0
        var right = 0
        var bottom = 0
        for (i in 0 until childCount) {
            //确定i具体的位置 row column
            val row = i/2
            val column = i % 2

            val child = getChildAt(i)
            left = space + column*(child.measuredWidth + space)
            top = space + row*(child.measuredHeight + space)
            right = left + child.measuredWidth
            bottom = top + child.measuredHeight

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

②子控件大小来决定ViewGroup的大小(子控件大小是一样的)

class MyViewGroup:ViewGroup {
    private val space = 30

    constructor(context:Context,attrs:AttributeSet?):super(context, attrs){
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //测量自己 size mode  -> 提供给子控件进行测量自己的(限制条件)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        //获取子控件的尺寸
        //MeasureSpec
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            //测量子控件
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
        }

        //确定父容器的尺寸
        var w = 0
        var h = 0
        var row = (childCount-1)/2
        val column = (childCount-1) % 2
        val child = getChildAt(0)
        if (childCount == 1){
            w = child.measuredWidth + 2*space
        }else{
            w = 2*child.measuredWidth + 3*space
        }
        h = space + (row+1)*(child.measuredHeight + space)

        setMeasuredDimension(w, h)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

        //对子控件进行布局
        var left = 0
        var top = 0
        var right = 0
        var bottom = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            var row = i/2
            val column = i % 2
            left = space + column*(child.measuredWidth + space)
            top = space + row*(child.measuredHeight + space)
            right = left + child.measuredWidth
            bottom = top + child.measuredHeight
            child.layout(left, top, right, bottom)
        }
    }
}

③子控件大小和父控件大小暂时都不确定(流式布局,上面已有了展示)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的商品列表展示界面的UI和Java代码: 1. 商品列表展示界面的UI ```xml <?xml version="1.0" encoding="utf-8"?> <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" /> ``` 2. 商品列表展示界面的Java代码 ```java public class ProductListActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private ProductListAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_product_list); mRecyclerView = findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); List<Product> productList = getProductList(); // 获取商品列表数据 mAdapter = new ProductListAdapter(productList); mRecyclerView.setAdapter(mAdapter); } private List<Product> getProductList() { // 从数据库中获取商品列表数据 // TODO: 实现从数据库获取商品列表数据的逻辑 return new ArrayList<>(); } private class ProductListAdapter extends RecyclerView.Adapter<ProductViewHolder> { private List<Product> mProductList; public ProductListAdapter(List<Product> productList) { mProductList = productList; } @NonNull @Override public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false); return new ProductViewHolder(view); } @Override public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) { Product product = mProductList.get(position); holder.mNameTextView.setText(product.getName()); holder.mPriceTextView.setText(String.valueOf(product.getPrice())); holder.mQuantityTextView.setText(String.valueOf(product.getQuantity())); } @Override public int getItemCount() { return mProductList.size(); } } private static class ProductViewHolder extends RecyclerView.ViewHolder { private TextView mNameTextView; private TextView mPriceTextView; private TextView mQuantityTextView; public ProductViewHolder(@NonNull View itemView) { super(itemView); mNameTextView = itemView.findViewById(R.id.tv_name); mPriceTextView = itemView.findViewById(R.id.tv_price); mQuantityTextView = itemView.findViewById(R.id.tv_quantity); } } } ``` 其中,`Product`是一个商品类,包含商品的名称、价格和数量等信息。`item_product.xml`是商品列表项的布局文件,包含商品名称、价格和数量等控件。在`ProductListAdapter`中,我们将商品列表项展示在了RecyclerView中,并根据需要实现了自定义ViewHolder。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值