カスタム TagViewGroup

最も基本的なTagGroupViewを実装する方法を見てみましょう。 ViewGroupとしてのこのビューの機能は、サブビューの自動行折り返しを実装して、サブビューが XML で渡された順序でインターフェイス上に比較的合理的に表示されるようにすることです。

注: 実際、Android テクノロジが非常に完璧になったため、ConstraintsLayoutのようなレイアウト ビューで基本的に日常の開発ニーズの **99.95%** を解決できるため、 ViewGroupをカスタマイズする必要はなくなりました。車輪の再発明は開発プロセスにおいて常に最大のタブーであり、オープンソース フレームワークは歴史と時代によってテストされてきたため、問題が発生しないとは言えませんが、ほとんどのシナリオで問題が発生しないとは言えます。 。

まず全体的なアイデアを整理しましょう。

  1. 仕事をうまくやり遂げたいなら、まずツールを磨く必要があります。まず、ランダムな幅、高さ、色を持つビューを実装しましょう。

  2. TagGroupViewを実装します

    • onMeasureでMeasureSpecWithMarginが呼び出され、サブビューのサイズを測定し、測定結果を保存します。
    • onMeasureの最後のsetMeasuredDimension は、 ViewGroupの測定結果を保存するために使用されます。
    • onLayout は、各サブビューのレイアウトメソッドを呼び出し、 onMeasureに格納された測定結果に基づいてサブビューのマッピング範囲を設定します。
  3. XMLファイルで定義したいくつかのサブビューをTagGroupViewに配置し、実装効果を確認します

基本的な考え方は説明されました。各ステップの実装方法を見てみましょう。

ステップ 1: ランダム ビューを定義する

毎回異なる幅、高さ、色を生成できるテキスト ビューの定義については、特に言うことはありませんが、唯一注意が必要なのは乱数の生成です。

  1. シードはランダムな方法で取得する必要があります。現在の CPU パフォーマンスは非常に高いため、現在のシステムのミリ秒時間をシードとして使用することはできません。この方法では、すべてのテキスト ビューが同じになります。
  2. **nextInt()** メソッドを継続的に呼び出して乱数を生成することが最善です。これにより、実際の効果がよりランダムになり、開発プロセス中に発生する問題を検出しやすくなります。
class RandomTextView(context: Context, attributeSet: AttributeSet) :
    AppCompatTextView(context, attributeSet) {
    
    
    private val random by lazy {
    
     Random(System.nanoTime()) }
    private val color by lazy {
    
     Color.rgb(nextInt(), nextInt(), nextInt()) }
    private val randomWidth by lazy {
    
     nextInt() }
    private val randomHeight by lazy {
    
     nextInt() }
    private val rect by lazy {
    
     Rect() }

    init {
    
    
        text = nextInt().toString()
        textAlignment = TEXT_ALIGNMENT_CENTER
        gravity = Gravity.CENTER
        setPadding(10.dip, 10.dip, 10.dip, 10.dip)
        setBackgroundColor(color)
    }

    private fun nextInt() = random.nextInt() % 50 + 100

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        paint.getTextBounds(text.toString(), 0, text.length, rect)
        setMeasuredDimension(
            randomWidth + rect.width() + paddingLeft * 2,
            randomHeight + rect.height() + paddingTop * 2
        )
    }
}

ステップ 2: GroupView をカスタマイズする

  • onMeasureでMeasureSpecWithMarginが呼び出され、サブビューのサイズを測定し、測定結果を保存します。

  • onMeasureの最後のsetMeasuredDimension は、 ViewGroupの測定結果を保存するために使用されます。

  • onLayout は、各サブビューのレイアウトメソッドを呼び出し、 onMeasureに格納された測定結果に基づいてサブビューのマッピング範囲を設定します。


import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import kotlin.math.max

class TagGroupView(context: Context, attributeSet: AttributeSet) :
    ViewGroup(context, attributeSet) {
    
    
    //我们为每个视图定义一个[ChildViewParam]类来存储该视图的位置信息,包括其宽度,高度,起始的X,Y坐标
    private val childViewLayouts by lazy {
    
    
        0.until(childCount).map {
    
     ChildViewParam(0, 0, 0, 0) }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //已经使用的宽度
        var widthUsed = 0
        //已经使用的高度
        var heightUsed = 0
        //当前遍历行最大高度
        var maxHeightOfLine = 0
        //目前遍历的所有行的最大宽度
        var maxWidthOfLine = 0
        0.until(childCount).map {
    
     getChildAt(it) }.forEachIndexed {
    
     index, it ->
            it.layoutParams = it.layoutParams.toMarginLayoutParams
            val childViewParam = childViewLayouts[index]

            //因为我们这里仅仅是Demo,所以采用这种丑陋的抽离方式,实际上我认为应该有更好的方式,但是那个方式虽然实现看起来非常简单,但是理解成本会比较高
            //测量子视图布局方法,判断当前行放入子视图是否符合我们的规则,另外,Demo中忽略子视图设置的Margin,因为如果考虑Margin,代码会显得比较复杂
            fun doMeasureChild(): Boolean {
    
    
                //测量子视图的尺寸
                measureChildWithMargins(
                    it,
                    widthMeasureSpec,
                    widthUsed,
                    heightMeasureSpec,
                    heightUsed
                )
                //如果当前行的尺寸容许足够放入当前遍历到的子视图,刷新各个状态值,并返回true
                if (it.measuredWidth + widthUsed <= MeasureSpec.getSize(widthMeasureSpec)) {
    
    
                    childViewParam.x = widthUsed
                    childViewParam.y = heightUsed
                    childViewParam.width = it.measuredWidth
                    childViewParam.height = it.measuredHeight
                    widthUsed += it.measuredWidth
                    maxWidthOfLine = max(maxWidthOfLine, widthUsed)
                    maxHeightOfLine = max(maxHeightOfLine, it.measuredHeight)
                    return true
                }
                //否则返回false
                return false
            }
            //如果当前行空间足够,继续遍历下一个子视图
            if (doMeasureChild()) return@forEachIndexed
            //当前行空间不够,换行,重置状态值,重新尝试放入子视图
            widthUsed = 0
            heightUsed += maxHeightOfLine
            maxHeightOfLine = 0
            doMeasureChild()
        }
        //设置当前ViewGroup的测量结果
        setMeasuredDimension(maxWidthOfLine, heightUsed + maxHeightOfLine)
    }

    /**
     * 根据[onMeasure]测量子视图的结果,为每个子视图分配布局位置
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    
    
        0.until(childCount).map {
    
     getChildAt(it) }.forEachIndexed {
    
     index, it ->
            val param = childViewLayouts[index]
            it.layout(param.x, param.y, param.x + param.width, param.y + param.height)
        }
    }

    private val LayoutParams.toMarginLayoutParams: MarginLayoutParams
        get() = MarginLayoutParams(this)
}

/**
 * 存储视图的起始X,Y值,宽度和高度
 */
data class ChildViewParam(var width: Int, var height: Int, var x: Int, var y: Int) {
    
    
    override fun hashCode(): Int {
    
    
        return x * x + y * y;
    }

    override fun equals(other: Any?): Boolean {
    
    
        if (other == null) return false;
        if (other is ChildViewParam)
            return x == (other as ChildViewParam).x
                    && y == (other as ChildViewParam).y;
        return false;
    }
}

ステップ 3: XML ファイル内の対応する実装効果を確認する

<?xml version="1.0" encoding="utf-8"?>
<com.mm.android.mobilecommon.TagGroupView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.mm.android.mobilecommon.RandomTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp" />
<!--    篇幅原因,省略若干个相同定义的RandomTextView-->
</com.mm.android.mobilecommon.TagGroupView>

効果を見てみますと、はい、基本的には要件を満たしており、自動で折り返すこともでき、高さも規格を満たしています。
ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/qq_31433709/article/details/131645161