Android理解自定義View

當(dāng)Android SDK中提供的系統(tǒng)UI控件無法滿足業(yè)務(wù)需求時(shí)精置,我們就需要考慮自己實(shí)現(xiàn)UI控件姐直。

自定義UI控件有2種方式:

  1. 繼承自系統(tǒng)提供的成熟控件(比如LinearLayout、RelativeLayout葛家、ImageView等)
  2. 直接繼承自系統(tǒng)View或ViewGroup, 并且繪制顯示內(nèi)容名挥。

繼承自成熟控件

相對(duì)而言狼钮,這種方式相對(duì)簡(jiǎn)單霜大,因?yàn)榇蟛糠趾诵墓ぷ鞴共福热缈丶笮y(cè)量,控件位置擺放位置等計(jì)算战坤,在系統(tǒng)控件中Google已為我們實(shí)現(xiàn)了曙强,我們不需要關(guān)心這部分的內(nèi)容,只需要在基礎(chǔ)上進(jìn)行擴(kuò)展需求即可途茫。因?yàn)榛旧媳容^簡(jiǎn)單碟嘴,所以我們這種我們暫時(shí)不做研究。

繼承自View或ViewGroup

這種方式相較第一種麻煩囊卜,但是更加靈活臀防,也能實(shí)現(xiàn)更加復(fù)雜的UI界面。一般情況下使用這種實(shí)現(xiàn)方式可以解決以下幾個(gè)問題:

  1. 如何根據(jù)相應(yīng)的屬性將UI元素繪制到界面
  2. 如何自定義控件大小边败,也就是測(cè)量布局的寬高
  3. 如果是ViewGroup,該如何安排其內(nèi)部子View的擺放位置

以上3個(gè)問題依次在如下3個(gè)方法中可以得到解決:

  1. onDraw
  2. onMeasure
  3. onLayout

因此自定義View的重點(diǎn)工作就是復(fù)寫并實(shí)現(xiàn)這3個(gè)方法捎废。
注意:并不是每個(gè)自定義View都需要實(shí)現(xiàn)這3個(gè)方法笑窜,大多數(shù)情況下實(shí)現(xiàn)其中1個(gè)或2個(gè)就可以滿足需求

我們先來依次研究一下如上3個(gè)方法

onDraw

onDraw方法接收一個(gè)Canvas類型的參數(shù),Canvas可以理解為一個(gè)畫布登疗,在這塊畫布上可以繪制各種類型的UI元素排截。

系統(tǒng)提供了一系列Canvas操作方法,如下:


Ciqc1F66brqANYwaAAFgenmfG7o790.png

從上圖可以看出辐益,Canvas每次繪制都需要傳入一個(gè)Paint對(duì)象断傲,Paint就相當(dāng)于一個(gè)畫筆,我們可以通過畫筆的各種屬性智政,來實(shí)現(xiàn)不同的繪制效果:


CgqCHl66bsKAC3aYAAEfignRLSI590.png

我們通過一個(gè)測(cè)試代碼來看看:

我們首先定義PieImageView繼承自View认罩, 在onDraw方法中,分別使用Canvas的drawArc和drawCircle方法繪制弧度和圓形续捂。

class PieImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var progress: Int = 0
    private val MAX_PROGRESS: Int = 100
    private var arcPaint: Paint? = null
    private var circlePaint : Paint? = null
    private var bound: RectF? = RectF()

    fun setProgress(progress: Int) {
        this.progress = progress
        ViewCompat.postInvalidateOnAnimation(this)
    }

    init {
        arcPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        arcPaint?.style = Paint.Style.FILL_AND_STROKE
        arcPaint?.strokeWidth =  dpToPixel(0.1f, context)
        arcPaint?.color = Color.RED

        circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
        circlePaint?.style = Paint.Style.STROKE
        circlePaint?.strokeWidth = dpToPixel(2f, context)
        circlePaint?.color = Color.argb(120, 0xff, 0xff, 0xff)
    }

    //布局加載完成執(zhí)行
    override fun onFinishInflate() {
        super.onFinishInflate()
        Log.d("TAG", "onFinishInflate")
    }

    //布局控件大小發(fā)生變化時(shí)調(diào)用垦垂,只在初始化執(zhí)行一次
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        val min = Math.min(w, h)
        val max = w + h - min
        val r = Math.min(w, h) / 2
        Log.d(
            "TAG",
            "onSizeChanged w = $w, h = $h, oldW = $oldw, oldH = $oldh, min = $min, max = $max, r = $r"
        )
        val left = ((max shr 1) - r).toFloat()
        val top = ((min shr 1) - r).toFloat()
        val right = ((max shr 1) + r).toFloat()
        val bottom = ((min shr 1) + r).toFloat()
        bound?.set(left, top, right, bottom)
        Log.d(
            "TAG",
            "onSizeChanged bound left = $left, top = $top, right = $right, bottom = $bottom"
        )
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        Log.d("TAG", "onDraw")
        if (progress != MAX_PROGRESS && progress != 0) {
            val angle = progress * 360f / MAX_PROGRESS
            canvas?.drawArc(bound!!, 270f, angle, true, arcPaint!!)
            canvas?.drawCircle(
                bound?.centerX()!!,
                bound?.centerY()!!,
                bound?.height()!! / 2,
                circlePaint!!
            )
        }
    }
}

在xml中我們使用上述的PieImageView, 設(shè)置寬高為200dp,并在Activity中設(shè)置PieImageView的進(jìn)度為45牙瓢,如下代碼

<?xml version="1.0" encoding="utf-8"?>#### onMeasure
自定義View為什么要進(jìn)行測(cè)量劫拗。正常情況下,我們直接在XML不居中定義好View的寬高矾克,然后讓自定義View在此寬高的區(qū)域顯示即可页慷。但是為了更好的兼容不同尺寸的屏幕,Android系統(tǒng)提供了wrap_content和match_parent屬性來規(guī)范控件的顯示規(guī)則。分別代表**自適應(yīng)大小**和**填充父布局的大小**酒繁,但是這兩個(gè)屬性并沒有指定具體大小滓彰,因此我們需要在onMeasure方法中過濾出這兩種情況,真正的測(cè)量出自定義View應(yīng)該顯示的寬高大小欲逃。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.eegets.measureview.PieImageView
        android:id="@+id/pieImageView"
        android:layout_width="300dp"
        android:layout_height="200dp"
        tools:ignore="MissingConstraints" />

</FrameLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        pieImageView.setProgress(45)
    }
}

運(yùn)行結(jié)果如下圖:


QQ圖片20201116165717.png

輸出一下PieImageView Log日志:

 D/TAG: onFinishInflate
 D/TAG: onSizeChanged w = 788, h = 525, oldW = 0, oldH = 0, min = 525, max = 788, r = 262
 D/TAG: onSizeChanged bound left = 132.0, top = 0.0, right = 656.0, bottom = 524.0
 D/TAG: onDraw

從log日志中我們能得到幾點(diǎn)信息

  • 1洗做、onFinishInflate和onSizeChanged只執(zhí)行了一次
  • 2、w = 788 和 h = 525 對(duì)應(yīng)xml中的 android:layout_width="300dp" android:layout_height="200dp"得到的真實(shí)的寬高
    注意:w = 788 和 h = 525 隨著手機(jī)分辨率的不同值也會(huì)不同
  • 3彰居、使用kotlin的shr相當(dāng)于java的<<移位運(yùn)算符限定了bound在界面顯示的區(qū)域
  • 4诚纸、設(shè)置bound邊界的left, top, right, bottom[具體值可以看上圖的標(biāo)注]

位移運(yùn)算符<<>>抬闯、>>>
<< : 左移運(yùn)算符井辆,num << 1,相當(dāng)于num乘以2
>> : 右移運(yùn)算符,num >> 1,相當(dāng)于num除以2
>>> : 無符號(hào)右移溶握,忽略符號(hào)位杯缺,空位都以0補(bǔ)齊

如上布局,我們?cè)趚ml中將PieImageView的寬高設(shè)置成了固定值"300dp"和"200dp"睡榆,我們嘗試將布局設(shè)置成自適應(yīng)wrap_content萍肆,重新運(yùn)行顯示效果如下:

WeChat Image_20201116172442.png

另外我們也看看此時(shí)的日志輸出:

D/TAG: onFinishInflate
D/TAG: onSizeChanged w = 1080, h = 1584, oldW = 0, oldH = 0, min = 1080, max = 1584, r = 540
D/TAG: onSizeChanged bound left = 252.0, top = 0.0, right = 1332.0, bottom = 1080.0
D/TAG: onDraw

很明顯,PieImageView并沒有正常顯示胀屿,并且log日志輸出的right = 1332.0,很明顯大于了屏幕的寬度w = 1080塘揣,這也是PieImageView超出屏幕沒有正常顯示的原因。根本原因是PieImageView中沒有在onMeasure方法中進(jìn)行重新測(cè)量宿崭,并重新設(shè)置寬高亲铡。

當(dāng)我們不設(shè)置onMeasure時(shí),父view其實(shí)已經(jīng)實(shí)現(xiàn)了onMeasure方法葡兑,我們看一下父類onMeasure做了什么


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //父布局傳入寬奴愉,高約束
    //通過比較最小的尺寸和父布局傳入的尺寸,找出合適的尺寸
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
    //size 為默認(rèn)大小
    int result = size;
    //獲取父布局傳入的測(cè)量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //獲取父布局傳入的測(cè)量尺寸
    int specSize = MeasureSpec.getSize(measureSpec);

    //根據(jù)測(cè)量模式選擇不同的測(cè)量尺寸
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            //父布局不對(duì)子布局施加任何約束铁孵,使用默認(rèn)尺寸
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY: //[重要代碼]
            //使用父布局給的尺寸
            result = specSize;
            break;
    }
    //返回子布局確定后的尺寸
    return result;
}

如上代碼可以看出锭硼,當(dāng)我們?cè)O(shè)置了wrap_content時(shí),父布局的onMeasure給子View返回了父布局給的尺寸蜕劝,也就是[重要代碼]處檀头,也就是上述log日志中輸出的w = 1080轰异,這也就說明了為什么我們布局的顯示是錯(cuò)誤的。

onMeasure

自定義View為什么要進(jìn)行測(cè)量暑始。正常情況下搭独,我們直接在XML不居中定義好View的寬高,然后讓自定義View在此寬高的區(qū)域顯示即可廊镜。但是為了更好的兼容不同尺寸的屏幕牙肝,Android系統(tǒng)提供了wrap_content和match_parent屬性來規(guī)范控件的顯示規(guī)則。分別代表自適應(yīng)大小填充父布局的大小嗤朴,但是這兩個(gè)屬性并沒有指定具體大小配椭,因此我們需要在onMeasure方法中過濾出這兩種情況,真正的測(cè)量出自定義View應(yīng)該顯示的寬高大小雹姊。

我們首先用一個(gè)比喻來看看Measure的測(cè)量過程股缸,如下圖


WeChat Image_20201117142811.png

所有工作都是在 onMeasure 方法中完成,方法定義如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

可以看出吱雏,該方法會(huì)傳入2個(gè)參數(shù)widthMeasureSpecheightMeasureSpec敦姻。這兩個(gè)參數(shù)是父視圖傳遞給子View的兩個(gè)參數(shù),包含了2種信息:寬歧杏、高以及測(cè)量模式镰惦。
我們獲取一下寬、高和測(cè)量模式犬绒,通過Android SDK中的MeasureSpec.java類獲取旺入。代碼如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    //寬度測(cè)量模式
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)

    Log.d(
        "TAG",
        "MeasureSpecMode MeasureSpec.AT_MOST = ${MeasureSpec.AT_MOST}, MeasureSpec.EXACTLY = ${MeasureSpec.EXACTLY}, MeasureSpec.UNSPECIFIED = ${MeasureSpec.UNSPECIFIED}"
    )
    Log.d(
        "TAG",
        "widthMode widthMode = $widthMode, heightMode = $heightMode"
    )

    // 判斷是wrap_content的測(cè)量模式
    if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode) {
        val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
        val measuredHeight = MeasureSpec.getSize(heightMeasureSpec)
        // 將寬高設(shè)置為傳入寬高的最小值
        val size = if (measuredWidth > measuredHeight) measuredHeight else measuredWidth
        // 調(diào)用setMeasuredDimension設(shè)置View實(shí)際大小
        setMeasuredDimension(size, size)
        Log.d(
            "TAG",
            "onMeasure +++++ measuredWidth = $size, measureHeight = $size"
        )
    } else {
        setMeasuredDimension(getDefaultSize(suggestedMinimumWidth, widthMeasureSpec), getDefaultSize(suggestedMinimumHeight, heightMeasureSpec))
        Log.d(
            "TAG",
            "onMeasure ----- defaultMeasuredWidth = ${getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)}, defaultMeasuredHeight = ${getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)}"
        )
    }
}

同時(shí)我們輸出一下log日志對(duì)比看一下:

D/TAG: MeasureSpecMode MeasureSpec.AT_MOST = -2147483648, MeasureSpec.EXACTLY = 1073741824, MeasureSpec.UNSPECIFIED = 0
D/TAG: widthMode widthMode = -2147483648, heightMode = -2147483648
D/TAG: onMeasure +++++ measuredWidth = 1080, measureHeight = 1080
D/TAG: MeasureSpecMode MeasureSpec.AT_MOST = -2147483648, MeasureSpec.EXACTLY = 1073741824, MeasureSpec.UNSPECIFIED = 0
D/TAG: widthMode widthMode = -2147483648, heightMode = -2147483648
D/TAG: onMeasure +++++ measuredWidth = 1080, measureHeight = 1080
D/TAG: onSizeChanged w = 1080, h = 1080, oldW = 0, oldH = 0, min = 1080, max = 1080, r = 540
D/TAG: onSizeChanged bound left = 0.0, top = 0.0, right = 1080.0, bottom = 1080.0
D/TAG: onDraw

可以看到,通過onMeasure進(jìn)行測(cè)量懂更,我們最終在onSizeChanged中的left = 0.0, top = 0.0, right = 1080.0, bottom = 1080.0 right變成了1080,也就是屏幕的寬度

ViewGroup中的onMeasure

如果我們自定義的控件是一個(gè)容器急膀,onMeasure的測(cè)量會(huì)更復(fù)雜一點(diǎn),因?yàn)閂iewGroup在測(cè)量自身之前,首先需要測(cè)量?jī)?nèi)部子View所占大小赞厕,然后才能確定自己的大小蜈项。比如以下代碼:


WeChat Image_20201117150004.png

上圖可以看出LinearLayout的最終寬度是由其內(nèi)部最大的子View寬度決定的。

當(dāng)我們自定義一個(gè)ViewGroup時(shí)晨雳,也需要在onMeasure中綜合考慮子View的寬度行瑞。比如要實(shí)現(xiàn)一個(gè)流式布局FlowLayout,效果如下:


Ciqc1F66b0uANdyTAASLs9Xvo14469.png

在大多數(shù)App的搜索界面經(jīng)常會(huì)用到FlowLayout來展示歷史搜索記錄以及熱門搜索項(xiàng)餐禁。
FlowLayout的每一行item個(gè)數(shù)都不一定血久,當(dāng)每行的item累計(jì)寬度超過可用總寬度時(shí),則需要重啟一行擺放Item帮非。因此我么需要在onMeasure方法中主動(dòng)的分行計(jì)算出FlowLayou的最終高度氧吐,代碼如下所示:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    var heightSize = MeasureSpec.getSize(heightMeasureSpec)

    //獲取容器中子View的個(gè)數(shù)
    val childCount = childCount
    //記錄每一行View的總寬度
    var totalLineWidth = 0
    //記錄每一行最高View的高度
    var perLineMaxHeight = 0
    //記錄當(dāng)前ViewGroup的總高度
    var totalHeight = 0

    Log.d("TAG", "onMeasure childCount = $childCount")

    for (index in 0 until childCount) {
        val childView = getChildAt(index)
        measureChild(childView, widthMeasureSpec, heightMeasureSpec)
        val lp = childView.layoutParams as MarginLayoutParams
        //獲得子View的測(cè)量寬度
        val childWidth = childView.measuredWidth + lp.leftMargin + lp.rightMargin
        //獲得子VIew的測(cè)量高度
        val childHeight = childView.measuredHeight + lp.topMargin + lp.bottomMargin
        Log.d("TAG", "onMeasure totalLineWidth=$totalLineWidth, childWidth=$childWidth, totalLineWidth + childWidth = ${totalLineWidth + childWidth}, widthSize=$widthSize")
        if (totalLineWidth + childWidth > widthSize) {
            //統(tǒng)計(jì)總高度
            totalHeight += perLineMaxHeight
            //開啟新的一行
            totalLineWidth = childWidth
            perLineMaxHeight = childHeight
            Log.d("TAG", "onMeasure true totalLineWidth=$totalLineWidth, perLineMaxHeight=$perLineMaxHeight, childHeight=$childHeight")
        } else {
            //記錄每一行的總寬度
            totalLineWidth += childWidth
            //比較每一行最高的View
            perLineMaxHeight = Math.max(perLineMaxHeight, childHeight)
            Log.d("TAG", "onMeasure false totalLineWidth=$totalLineWidth, perLineMaxHeight=$perLineMaxHeight, childHeight=$childHeight")
           
        }
        //當(dāng)該View已是最后一個(gè)View時(shí)讹蘑,將該行最大高度添加到totalHeight中
        if (index == childCount - 1) {
            totalHeight += perLineMaxHeight
        }

        //如果高度的測(cè)量模式是EXACTLY,則高度用測(cè)量值筑舅,否則用計(jì)算出來的總高度(這時(shí)高度的設(shè)置為wrap_content)
        heightSize = if (heightMode == MeasureSpec.EXACTLY) heightSize else totalHeight
        Log.d(
            "TAG",
            "onMeasure childMargin measuredWidth = $childWidth, leftMargin = ${lp.leftMargin}, rightMargin = ${lp.rightMargin}, totalHeight=$totalHeight, heightSize=$heightSize"
        )
        setMeasuredDimension(widthSize, heightSize)
    }
}

上述 onMeasure 方法的主要目的有 2 個(gè):

1座慰、調(diào)用 measureChild 方法遞歸測(cè)量子 View;
2翠拣、通過疊加每一行的高度版仔,計(jì)算出最終 FlowLayout 的最終高度 totalHeight。

onLayout

根據(jù)之前的思維導(dǎo)圖误墓,我們知道蛮粮,老父親給三個(gè)兒子,老大(老大兒子:兒子)优烧、老二蝉揍、老三分配了具體的良田面積,三個(gè)兒子及老大的兒子也都確認(rèn)了自己的需要的良田面積畦娄。這就是:Measure過程

既然知道了分配給各個(gè)兒孫的良田大小又沾,那他們到底分到哪一塊呢,是靠邊熙卡、還是中間杖刷、還是其它位置呢?先分給誰呢驳癌?
老父親想按到這個(gè)家的時(shí)間先后順序來吧(對(duì)應(yīng)addView 順序)滑燃,老大是自己的長(zhǎng)子,先分配給他颓鲜,于是從最左側(cè)開始表窘,劃出3畝田給老大。現(xiàn)在輪到老二了甜滨,由于老大已經(jīng)分配了左側(cè)的3畝乐严,那么給老二的5畝地只能從老大右側(cè)開始劃分,最后剩下的就分給老三衣摩。這就是:ViewGroup onLayout 過程昂验。
老大拿到老父親給自己指定的良田的邊界,將這個(gè)邊界(左艾扮、上既琴、右、下)坐標(biāo)記錄下來泡嘴。這就是:View Layout過程
接著老大告訴自己的兒子:你爹我也要為自己考慮哈甫恩,從你爺爺那繼承的5畝田地不能全分給你,我留一些養(yǎng)老酌予。這就是設(shè)置:padding 過程
如果老二在最開始測(cè)量的時(shí)候就想:我不想和老大填物、老三的田離得太近纹腌,那么老父親就會(huì)給老大、老三與老二的土地之間留點(diǎn)縫隙滞磺。這就是設(shè)置:margin 過程

上面的FlowLayout的onMeasure只是算出了ViewGroup的最終顯示寬高升薯,但是并沒有規(guī)定某個(gè)子View應(yīng)該在何處顯示、間距是多少击困。要定義ViewGroup內(nèi)部子View的顯示規(guī)則涎劈,則需要復(fù)寫并實(shí)現(xiàn)onLayout方法。
onLayout聲明如下:

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
      TODO("Not yet implemented")
}

它是一個(gè)抽象方法阅茶,也就是每一個(gè)ViewGroup必須要實(shí)現(xiàn)如何排列子View蛛枚,具體的就是循環(huán)遍歷子View,調(diào)用子View.layout(left, top, right, bottom)來設(shè)置布局位置脸哀。FlowLayout設(shè)置布局代碼如下:


/**
 * 擺放控件
 * 通過循環(huán)并通過‘totalLineWidth + childWidth > width’進(jìn)行寬度比較將我們的子View存儲(chǔ)到lineViews中蹦浦,也就是一列能裝幾個(gè)子View
 * 同樣通過循環(huán)將每一行顯示的子View的lineViews存儲(chǔ)到MAllViews中,mAllViews中存儲(chǔ)了n行l(wèi)ineViews列(每列的個(gè)數(shù)可能不一致)組成的數(shù)組
 *
 * 最后通過遍歷mAllViews和lineViews得到子View并通過`childView.layout(leftChild, topChild, rightChild, bottomChild)`擺放到合適的位置
 */
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    mAllViews.clear()
    mPerLineMaxHeight.clear()

    //存放每一行的子View
    var lineViews = mutableListOf<View>()
    //記錄每一行已存放View的總寬度
    var totalLineWidth = 0

    //記錄每一行最高View的高度
    var lineMaxHeight = 0

    /*************遍歷所有View撞蜂,將View添加到List<List></List><View>>集合中</View> */
    Log.d("TAG", "onLayout ")
    //獲得子View的總個(gè)數(shù)
    val childCount = childCount
    for (i in 0 until childCount) {
        val childView: View = getChildAt(i)
        val lp = childView.layoutParams as MarginLayoutParams
        val childWidth: Int = childView.measuredWidth + lp.leftMargin + lp.rightMargin
        val childHeight: Int = childView.measuredHeight + lp.topMargin + lp.bottomMargin
        Log.d("TAG", "onLayout width=$width, totalLineWidth=$totalLineWidth, childWidth=$childWidth, totalLineWidth + childWidth=${totalLineWidth + childWidth}")
        if (totalLineWidth + childWidth > width) {
            mAllViews.add(lineViews)
            mPerLineMaxHeight.add(lineMaxHeight)
            //開啟新的一行
            totalLineWidth = childWidth
            lineMaxHeight = childHeight
            lineViews = mutableListOf()
            Log.d("TAG", "onLayout true lineViews size=${lineViews.size}, mAllViews size=${mAllViews.size}")
        } else {
            totalLineWidth += childWidth
            Log.d("TAG", "onLayout false lineViews size=${lineViews.size}, mAllViews size=${mAllViews.size}")
        }
        lineViews.add(childView)
        lineMaxHeight = Math.max(lineMaxHeight, childHeight)
    }
    //單獨(dú)處理最后一行
    mAllViews.add(lineViews)
    mPerLineMaxHeight.add(lineMaxHeight)
    Log.d(
        "TAG",
        "onLayout mAllViews size=${mAllViews.size}, mPerLineMaxHeight size=${mPerLineMaxHeight.size}, lineViews size=${lineViews.size}"
    )

    /************遍歷集合中的所有View并顯示出來 */
    //表示一個(gè)View和父容器左邊的距離
    var mLeft = 0
    //表示View和父容器頂部的距離
    var mTop = 0
    for (i in 0 until mAllViews.size) {
        //獲得每一行的所有View
        lineViews = mAllViews[i]
        lineMaxHeight = mPerLineMaxHeight[i]
        for (j in lineViews.indices) {
            val childView: View = lineViews[j]
            val lp = childView.layoutParams as MarginLayoutParams
            val leftChild = mLeft + lp.leftMargin
            val topChild = mTop + lp.topMargin
            val rightChild: Int = leftChild + childView.measuredWidth
            val bottomChild: Int = topChild + childView.measuredHeight
            //四個(gè)參數(shù)分別表示View的左上角和右下角
            childView.layout(leftChild, topChild, rightChild, bottomChild)
            mLeft += lp.leftMargin + childView.measuredWidth + lp.rightMargin
        }
        mLeft = 0
        mTop += lineMaxHeight
    }
}

以上onLayout方法中做了兩件事情如下:

1盲镶、通過循環(huán)并通過totalLineWidth + childWidth > width進(jìn)行寬度比較將我們的子View存儲(chǔ)到lineViews中,也就是一列能裝幾個(gè)子View
同樣通過循環(huán)將每一行顯示的子View的lineViews存儲(chǔ)到MAllViews中蝌诡,mAllViews中存儲(chǔ)了n行l(wèi)ineViews列(每列的個(gè)數(shù)可能不一致)組成的數(shù)組
2溉贿、通過遍歷mAllViews和lineViews得到子View并通過childView.layout(leftChild, topChild, rightChild, bottomChild)擺放到合適的位置
FlowLayout調(diào)用FlowActivity.kt如下代碼:

class FlowActivity :Activity() {
//    private val list = mutableListOf("阿迪達(dá)斯", "李林", "耐克", "361", "海藍(lán)之迷面霜", "coach", "fendi", "亞歷山大短靴", "二手中古", "Ariete阿麗亞特", "ASH", "阿瑪尼牛仔")
    private val list = mutableListOf("阿迪達(dá)斯", "李林", "耐克", "361", "海藍(lán)之迷面霜", "coach", "fendi")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flow)
        addView()
    }

    private fun addView() {
        flowLayout.removeAllViews()
        list.forEach {
            val view = LayoutInflater.from(this).inflate(R.layout.item_flow, flowLayout, false) as TextView
            view.text = it
            flowLayout.addView(view)
        }
    }
}

activity_flow.xml

<com.eegets.measureview.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/flowLayout"
    android:background="#bbbbbb">

</com.eegets.measureview.FlowLayout>

item_flow.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/itemFlow"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff00ff"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:layout_marginTop="5dp"
    android:layout_marginBottom="5dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="12dp"/>

最終界面展示如下圖:


WeChat Image_20201119103121.png

至此我們自定義基本上就研究明白了

源碼已上傳至Github https://github.com/eegets/MeasureViewTest

最后感謝 大神姜新星的Android進(jìn)階

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市浦旱,隨后出現(xiàn)的幾起案子宇色,更是在濱河造成了極大的恐慌,老刑警劉巖颁湖,帶你破解...
    沈念sama閱讀 211,423評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宣蠕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡甥捺,警方通過查閱死者的電腦和手機(jī)抢蚀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涎永,“玉大人思币,你說我怎么就攤上這事鹿响∠畚ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 157,019評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵惶我,是天一觀的道長(zhǎng)妈倔。 經(jīng)常有香客問我,道長(zhǎng)绸贡,這世上最難降的妖魔是什么盯蝴? 我笑而不...
    開封第一講書人閱讀 56,443評(píng)論 1 283
  • 正文 為了忘掉前任毅哗,我火速辦了婚禮,結(jié)果婚禮上捧挺,老公的妹妹穿的比我還像新娘虑绵。我一直安慰自己,他們只是感情好闽烙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評(píng)論 6 385
  • 文/花漫 我一把揭開白布翅睛。 她就那樣靜靜地躺著,像睡著了一般黑竞。 火紅的嫁衣襯著肌膚如雪捕发。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,798評(píng)論 1 290
  • 那天很魂,我揣著相機(jī)與錄音扎酷,去河邊找鬼。 笑死遏匆,一個(gè)胖子當(dāng)著我的面吹牛法挨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拉岁,決...
    沈念sama閱讀 38,941評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼坷剧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了喊暖?” 一聲冷哼從身側(cè)響起惫企,我...
    開封第一講書人閱讀 37,704評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陵叽,沒想到半個(gè)月后狞尔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,152評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巩掺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評(píng)論 2 327
  • 正文 我和宋清朗相戀三年偏序,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胖替。...
    茶點(diǎn)故事閱讀 38,629評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡研儒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出独令,到底是詐尸還是另有隱情端朵,我是刑警寧澤,帶...
    沈念sama閱讀 34,295評(píng)論 4 329
  • 正文 年R本政府宣布燃箭,位于F島的核電站冲呢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏招狸。R本人自食惡果不足惜敬拓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評(píng)論 3 313
  • 文/蒙蒙 一邻薯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乘凸,春花似錦厕诡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冀偶,卻和暖如春醒第,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背进鸠。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評(píng)論 1 266
  • 我被黑心中介騙來泰國打工稠曼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人客年。 一個(gè)月前我還...
    沈念sama閱讀 46,333評(píng)論 2 360
  • 正文 我出身青樓霞幅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親量瓜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子司恳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容