사용자 정의 TagViewGroup

가장 기본적인 TagGroupView 구현 방법을 살펴보겠습니다 . ViewGroup 으로서 이 뷰의 기능은 하위 뷰가 xml에 전달된 순서에 따라 상대적으로 합리적으로 인터페이스에 표시될 수 있도록 하위 뷰의 자동 줄 바꿈을 구현하는 것입니다.

참고: 실제로 이제 Android 기술이 매우 완벽해졌으므로 더 이상 ViewGroup을 맞춤설정할 필요가 없습니다. 왜냐하면 ConstraintsLayout 과 같은 레이아웃 보기는 기본적으로 일일 개발 요구 사항의 **99.95%**를 해결할 수 있기 때문입니다. 바퀴를 재발명하는 것은 항상 개발 과정에서 가장 큰 금기 사항이며, 오픈 소스 프레임워크는 역사와 시간에 따라 테스트되었습니다. 문제가 발생하지 않는다고 말할 수는 없지만 대부분의 시나리오에서 문제가 발생하지 않는다고 말할 수 있습니다. .

먼저 전체적인 아이디어를 정리해 보겠습니다.

  1. 작업을 잘하려면 먼저 도구를 갈고닦아야 합니다.먼저 너비, 높이, 색상이 임의인 뷰를 구현해 보겠습니다.

  2. TagGroupView 구현 :

    • MeasureSpecWithMargin은 하위 뷰의 크기를 측정하고 측정 결과를 저장하기 위해 onMeasure 에서 호출됩니다 .
    • 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 사용자 정의

  • MeasureSpecWithMargin은 하위 뷰의 크기를 측정하고 측정 결과를 저장하기 위해 onMeasure 에서 호출됩니다 .

  • 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>

달성된 효과를 살펴보겠습니다. 예, 기본적으로 요구 사항을 충족합니다. 자동으로 줄을 감쌀 수 있고 높이도 표준을 충족합니다.
여기에 이미지 설명을 삽입하세요.

Acho que você gosta

Origin blog.csdn.net/qq_31433709/article/details/131645161
Recomendado
Clasificación