自定义View - 布局流程(测量与布局)

962 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

前言

最近在重新跟随扔物线的课程学习下自定义View 知识,本篇文章主要是介绍View 的布局过程.本文主要参考了扔物线课程中的一些知识,加上自己的一些理解. 另一篇文章讲述了View 事件分发机制重新学习 - View的触发反馈基本原理

布局过程的作用

自定义View 的主要有三个点 绘制 布局 触摸反馈.而布局则是 绘制 与触摸反馈的基础.

布局过程为 绘制触摸反馈的范围做支持.确定View何处绘制,确定用户点击的位置.

  • 作用: 为绘制和触摸范围做支持
    • 绘制:确定何处绘制
    • 触摸反馈:确定用户点击位置

测量和布局需要重写的方法

对于一个自定义View,需要重写onMeasure 去自定义尺寸.

对于一个自定义ViewGroup.需要重写onMeasure去自定义尺寸,并重写onLayout去管理布局内容.

布局的流程

View 的布局流程一般分为两层.先是测量流程(measure),再是布局流程(layout).

  • 测量流程: 从根View 递归调用每一级子View的 measure() 方法,对他们进行测量

  • 布局流程: 从根View递归调用每一级子View 的layout()方法,将测量过程中得出的子View的位置和尺寸传给子View,子View保存这些数据.

为什么View 的布局测量需要两个流程?

因为测量流程可能是多次的. 如果两个流程合一,则可能出现多次的 无用的layout流程.

测量流程是一个动态化的,反复的过程.当然,大多数情况下,只需要一次即可.

具体到View的布局过程

对于每个View:

  1. 运行前,开发者在xml文件或者 通过代码layout*参数,设置对View的布局要求 layout_XXX

  2. 父View 在自己的onMeasure() 中,根据开发者在xml中写的对子View的要求,和自己的可用空间,得出对子View的具体尺寸要求

  3. 子View 在自己的onMeasure() 中,根据父View的要求以及自己的特性计算出自己的期望尺寸.(在子View的期望大小和父View的指定大小发生冲突时,应该遵从父View.绝大多数情况下父View制定的尺寸和子View的期望是一致的.父View的要求指的是 根据View写的时候传入的layout_height等参数计算出的尺寸)

    • 如果是ViewGroup,还会在此处调用每个子View的measure()进行测量
  4. 父View 在子View 计算出期望尺寸后,得出子view的实际尺寸和位置

  5. 子View在自己的layout()方法中,将父View 传进来的自己的实际尺寸和位置保存.

    • 如果是ViewGroup,会在onLayout()方法中调用子View的layout()方法,传入子View的尺寸和位置.

onMeasure 方法分析

View 的尺寸定义是由 父View和 子View 共同决定的.而不管是 子View的尺寸还是父View的尺寸处理主要操作都是在onMeasure 方法中进行的.

默认的实现如下:

class View {
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
	}
}

View 定义自身的 width,height 的大小最终都是需要通过 setMeasuredDimension方法去设置的. 不过在onMeasure方法中调用setMeasuredDimension 中传入的宽高并不一定就是最终的宽高.onMeasure只是View自己声明了其想要获得的宽高,但是最终父View不一定给View想要的.我们可以看到onMeasure有两个参数

widthMeasureSpec,heightMeasureSpec两个参数,这两个参数中包含了 父View对于子View的宽高设置的要求.

一般而言,自定义一个View的时候需要去尽量满足父View对齐的要求,否则即使你通过setMeasuredDimension的,也不一定会生效,,在个别情况下,父View不一定认可子View要求,会重新强制校正.

正常的一个View要申请实际的尺寸,是结合自己期望尺寸和父View的要求来进行的.并且View 提供了对应的函数resolveSize让我们去处理.当然,如果你的View 有其他的尺寸策略(比如规定宽高至少要大于20px)可以按自身测量重写,.正常情况建议就用该种测量策略

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int wantWidthSize = {根据实际需要计算得出的值};// 
    int wantHeightSize = {根据实际需要计算得出的值};// 
	int widthSize = resolveSize(wantWidthSize,widthMeasureSpec);
    int heightSize = resolveSize(wantWidthSize,widthMeasureSpec);
    setMeasuredDimension(widthSize,heightSize);
}

如上主要是一个子View的一般策略,如果是一个ViewGroup.则还要考虑子View的大小.一般而言

private List<Rect> childBounds = new ArrayList();
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int wantWidthSize = {根据实际需要计算得出的值};//
    int wantHeightSize = {根据实际需要计算得出的值};//
    for (int i =0 ;i < getChildCount();i++) {
        View child = getChildAt(i);
        ...//根据策略确定确定子View的位置
            if(i >= childBounds.size()) {
                childBounds.add(new Rect());
            }
        childBounds.get(i).set({根据测量确定的子View坐标});
        // 计算 wantWidthSize,wantHeightSize
    }
    int widthSize = resolveSize(wantWidthSize,widthMeasureSpec);
    int heightSize = resolveSize(wantWidthSize,widthMeasureSpec);
    setMeasuredDimension(widthSize,heightSize);
}

那么resolveSize 是如何去计算尺寸的呢?其代码如下:

public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}   
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

从如上代码我们可以看见 measureSpec 这个int型参数中既存储了 测量的模式要求,也存储了View最多可以达到的大小.为什么其能存储两种信息?这主要涉及到了与或运算, 一个 int参数占用了32bit,measureSpece使用前两位用于保存测量模式,后面的30位用于保存size数据,30bit用于保存尺寸数据显然是绰绰有余了.

测量模式一种有三种:

  • MeasureSpec.EXACTLY: 子View大小已被指定.(一般是通过xml中 layout_width,layout_height 制)

  • MeasureSpec.AT_MOST: 子View 大小被制定了一个最大范围. 子View可以在不超过最大值的情况喜爱根据自己的特性定义自己的尺寸.

  • MeasureSpec.UNSPECIFIED: 子View大小不被限制,完全由子View根据自己的特性定义自己的尺寸.

MeasureSpec.UNSPECIFIED 一般是在可滚动的布局中出现,传给其子View.当布局可滚动时,子View的大小很多时候就不需要被限制了

onLayout 方法分析

以下是一个自定义的ViewGroup的部分代码,后面将会描述该布局

private List<Rect> childBounds = new ArrayList();
override fun onLayout(changed: Boolean, left: Int, top: Int, rright: Int, bottom: Int) {
    for ((index,childView) in children.withIndex()) {
        val childBound = childBounds[index]
        childView.layout(childBound.left,childBound.top,childBound.right,childBound.bottom)

    }
}

onLayout方法一般而言,相对onMeasure方法复杂度少很多.因为,子View的布局位置和父View本身的布局大小,一般只要在onMeasure就可以测量完毕.

onLayout中只需要调用子View的layout方法传入对应的布局数据即可.后面将会有个自定义的TagLayout例子介绍.

自定义布局的几种情形

自定义布局时主要有以下三种情形.

  1. 继承已有的View,重写 oneMeasure 方法,修改尺寸
  2. 重写onMeasure定制全新的View的尺寸
  3. 重写onMeasure,onLayout来全新定义ViewGroup的内部布局.

情形1 继承已有 View

在有的时候,系统已有的View可能大部分情况都符合我们的一个需求,但是他的尺寸的定义可能不太符合,在这种情况下,我们可以重写onMeasure方法.接下来就是一个例子,实现一个SquareImageView ,该View用于保证其本身始终属于一个正方型的View.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:padding="20dp">
    <!--加上背景色显示View大小-->
    <com.example.viewlearn.view.SquareImageView
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:background="@color/colorDarkCyan"
        android:src="@drawable/ic3"
        />
</LinearLayout>

不重写onMeasure

class SquareImageView(context: Context, attrs: AttributeSet?) : androidx.appcompat.widget.AppCompatImageView(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }
}

image.png

修改后

class SquareImageView(context: Context, attrs: AttributeSet?) : androidx.appcompat.widget.AppCompatImageView(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //取最小值作为宽高
        val size = min(measuredWidth,measuredHeight)
        setMeasuredDimension(size,size)
    }
}

显示效果如下:

image.png

情形2 全新定义View的尺寸

很多情况下,一个View是需要根据一些绘制内容来决定 size的大小.比如TextView,在 layout_width,layout_heightwrap_content时,其显示的大小一般都是与其 text的内容长度以及字体大小等共同决定的.大多数情况下,在onMeasure的参数widthMeasureSpec ,hegithMeasureSpec 值不等于MeasureSpec.EXACTLY 时,就需要View去计算出自己需要的尺寸.

对了View 本身的默认onMeasure方法,在 layout_*属性为wrap_content时,一般情况是占据下剩余所有空间.

class View {
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
}

大多数的布局,在 layout_*属性为wrap_content时 ,传入的measureSpecMeasureSpec.AT_MOST.

以上是View的默认实现,接下来就让我们来看下一个自定义实现的CircleView 吧.

子View通过wantWidth,wantHeight描述了正常期望能获得的宽高


private val PADDING = 5.dp
class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val DEFAULT_RADIUS = 100.dp
    private var radius = DEFAULT_RADIUS
    init {
        val typedArray = context.obtainStyledAttributes(attrs,R.styleable.CircleView)
        typedArray.run {
            radius = this.getDimension(R.styleable.CircleView_circleRadius,DEFAULT_RADIUS)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val wantWidth = (radius + PADDING) * 2
        val wantHeight = (radius + PADDING) * 2
        val widthSize = resolveSize(wantWidth.toInt(),widthMeasureSpec)
        val heightSize = resolveSize(wantHeight.toInt(),heightMeasureSpec)
        setMeasuredDimension(widthSize,heightSize)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width/2f,height/2f,radius,paint)
    }
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    >
   <com.example.viewlearn.view.CircleView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:background="@color/colorYellow"
       app:radius="100dp"                                   
       />
</LinearLayout>

效果如下:

image.png

情形3 全新定义ViewGroup的内部布局

ViewGroup布局的尺寸自定义,相比于View的自定义复杂很多,其要计算子View的位置大小,还要计算得出本身的大小.

下面来展示一个标签布局.先看下,效果图如下

image.png

具体代码如下四个文件TagLayout.kt、·ColorTextView.ktTagLayoutActivity.ktactivity_tag_layout.xml.

重点在于TagLayout的重写,此处重点描述下其整个逻辑。后面也会贴上其余三个文件的代码,供读者去粘贴复制验证效果。

private const val TAG = "TagLayout"
class TagLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    /**
     * 记录 child 位置
     */
    private val childBounds = mutableListOf<Rect>()
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        Log.i(TAG, "onMeasure: onMeasure")
        //被指定的大小
        var specWidthMode = MeasureSpec.getMode(widthMeasureSpec)
        //被指定的大小
        var specWidthSize = MeasureSpec.getSize(widthMeasureSpec)

        var specHeightMode = MeasureSpec.getMode(heightMeasureSpec)
        //被指定的大小
        var specHeightSize = MeasureSpec.getSize(heightMeasureSpec)
        var maxAvailableWidth = specWidthSize - paddingLeft - paddingRight
        var maxWidth = 0
        var needWidth = 0
        var needHeight = paddingTop //起点
        var lineWidthUsed = 0 // 该行已用
        var lineMaxHeight = 0 //本行最大高

        for ((index,childView) in children.withIndex()) {
            val lp = childView.layoutParams as MarginLayoutParams
            //代码1 此处widthUsed传入0,尽可能的满足子View 的宽度要求
            measureChildWithMargins(childView,widthMeasureSpec,0,needHeight,heightMeasureSpec)
            
            //代码2
            // 不是 UNSPECIFIED. 说明是 EXACTLY 或 AT_MOST.如果是 EXACTLY,则宽度是固定的,如果View 宽度超过,当需要换行.
            // 如果是 AT_MOST,说明值也是固定的
            if( (specWidthMode != MeasureSpec.UNSPECIFIED)
                && (lineWidthUsed + lp.leftMargin + childView.measuredWidth) > maxAvailableWidth) {
                needHeight += lineMaxHeight //下一层起点
                lineWidthUsed = 0
                lineMaxHeight = 0
                //重新计算
                measureChildWithMargins(childView,widthMeasureSpec,0,needHeight,heightMeasureSpec)
            }
            if (index >= childBounds.size) {
                childBounds.add(Rect())
            }
            val childRect = childBounds[index]
            // 孩子布局位置需要加入边界的偏移
            childRect.set(paddingLeft + lineWidthUsed + lp.leftMargin,needHeight + lp.topMargin,paddingLeft + lineWidthUsed + lp.leftMargin + childView.measuredWidth,needHeight + lp.topMargin + childView.measuredHeight)
            lineWidthUsed += childView.measuredWidth + lp.leftMargin + lp.rightMargin
            maxWidth = max(lineWidthUsed,maxWidth)
            lineMaxHeight = max(childView.measuredHeight + lp.topMargin + lp.bottomMargin,lineMaxHeight)
        }
        //计算需要的高度 宽度值
        needHeight += lineMaxHeight + paddingBottom + paddingTop
        needWidth = maxWidth + paddingLeft + paddingRight
        var height = when(specHeightMode) {
            MeasureSpec.EXACTLY -> specHeightSize
            MeasureSpec.AT_MOST -> min(needHeight,specHeightSize)
            else -> needHeight
        }
        var width = when(specWidthMode) {
            MeasureSpec.EXACTLY -> specWidthSize
            MeasureSpec.AT_MOST -> min(needWidth,specWidthSize)
            else -> needWidth
        }
        setMeasuredDimension(width,height)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, rright: Int, bottom: Int) {
       for ((index,childView) in children.withIndex()) {
           val childBound = childBounds[index]
           val lp = childView.layoutParams as MarginLayoutParams
           childView.layout(childBound.left,childBound.top,childBound.right,childBound.bottom)
       }
    }
	/**
	** 代码3 重写 generateLayoutParams 方法,生成MarginlayoutParmas,
	**/
    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context,attrs)
    }

}

如上代码,基本也写了很多的注释了。其总体一个逻辑就是 TagLayout 不会去主动限制子View的宽度,让其尽可能的去根据自己的特性来定义尺寸。

代码1: 使用measureChildWithMargins函数去测量子View,其实就是在该方法中调用了chilView.measure 方法。其实现如下,具体就是除去父View自身的Padding 和 子View margin数据后,告诉子View其可以申请的最大宽高。因为考虑计算了 Margin,因此需要使用 MarginLayoutParams类,而ViewGroup 默认为子View生成的是LayoutParams类,因此如果是从xml文件解析View时,需要重写generateLayoutParams 方法。

class ViewGroup {
    protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                                                                  mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                                                                  + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                                                                   mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                                                                   + heightUsed, lp.height);

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
 	public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        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 them 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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}

代码2

当View的宽度在当前行不被满足时,就会去换一行,并重新测量子View的宽高,具体可从代码2 处看出。当然这种做法下,可能出现这样的情况,那就是子View期望的宽度甚至比当前TagLayout的最大宽度还大,这种情况下只能显示不全了。

注: 如上代码中使用 childBounds 在onMeasure时期去View的位置与尺寸,然后在 onLayout只是简单的遍历。这种做法只是为了简单化,不想再在onLayout中重写一堆与onMeasure类似的逻辑了。但是在实际开发中,有些情况这种做法并不好。因为在onMeasure时期,TagLayout的大小还并未确定。TagLayout的父View可能在onLayout的方法中传入的宽高并不是 TagLayout 期望的。

如上TagLayout讲解完毕 ,接下来就贴下其他三个文件代码吧。

ColorTextView.kt

private val CORNER_RADIUS = 4.dp
private val PADDING_X = 16.dp.toInt()
private val PADDING_Y = 8.dp.toInt()
class ColorTextView(context: Context, attrs: AttributeSet?,textSize:Float,backgroundColor:Int,text:CharSequence) : AppCompatTextView(context, attrs) {
    private var backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    init {
        setTextColor(Color.WHITE)
        isSingleLine = true
        setPadding(PADDING_X, PADDING_Y, PADDING_X, PADDING_Y)
        setTextSize(textSize)
        maxLines = 1
        backgroundPaint.color = backgroundColor
        setText(text)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawRoundRect(
            0f,
            0f,
            width.toFloat(),
            height.toFloat(),
            CORNER_RADIUS,
            CORNER_RADIUS,
            backgroundPaint
        )
        super.onDraw(canvas)
    }
}

activity_tag_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:padding="20dp">
    <com.example.viewlearn.view.TagLayout
        android:id="@+id/tagLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#03DAC5"
        >
    </com.example.viewlearn.view.TagLayout>
</LinearLayout>

TagLayoutActivity.kt

class TagLayoutActivity : AppCompatActivity() {
    private val tagList = arrayListOf<String>(
        "Android",
        "Java",
        "Kotlin",
        "Rust",
        "Python",
        "C++",
        "C",
        "C#",
        "Ruby",
        "后端",
        "前端",
        "Html",
        "Javascript",
        "Go",
        "Hadoop",
        "大数据",
    )

    private val colors =
        intArrayOf(Color.parseColor("#00ff00"),
            Color.parseColor("#000099"),
            Color.parseColor("#330033"),
            Color.parseColor("#663300"),
            Color.parseColor("#996600"))
    private val textSizes = intArrayOf(14, 16, 18)
    private lateinit var tagLayout: TagLayout
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_tag_layout)
        tagLayout = findViewById<TagLayout>(R.id.tagLayout)
        val random = Random()
        for (tag in tagList) {
            val textSize = textSizes[random.nextInt(textSizes.size)]
            val color = colors[random.nextInt(colors.size)]
            val text = tag
            val colorTextView = ColorTextView(this, null, textSize.toFloat(), color, text)
            val marginLayoutParams = ViewGroup.MarginLayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            ).apply {
                leftMargin = 5.dp.toInt()
                rightMargin = 5.dp.toInt()
                topMargin = 5.dp.toInt()
                bottomMargin = 5.dp.toInt()
            }
            tagLayout.addView(colorTextView, marginLayoutParams)
        }
    }
}