最も基本的なTagGroupViewを実装する方法を見てみましょう。 ViewGroupとしてのこのビューの機能は、サブビューの自動行折り返しを実装して、サブビューが XML で渡された順序でインターフェイス上に比較的合理的に表示されるようにすることです。
注: 実際、Android テクノロジが非常に完璧になったため、ConstraintsLayoutのようなレイアウト ビューで基本的に日常の開発ニーズの **99.95%** を解決できるため、 ViewGroupをカスタマイズする必要はなくなりました。車輪の再発明は開発プロセスにおいて常に最大のタブーであり、オープンソース フレームワークは歴史と時代によってテストされてきたため、問題が発生しないとは言えませんが、ほとんどのシナリオで問題が発生しないとは言えます。 。
まず全体的なアイデアを整理しましょう。
-
仕事をうまくやり遂げたいなら、まずツールを磨く必要があります。まず、ランダムな幅、高さ、色を持つビューを実装しましょう。
-
TagGroupViewを実装します。
- onMeasureでMeasureSpecWithMarginが呼び出され、サブビューのサイズを測定し、測定結果を保存します。
- onMeasureの最後のsetMeasuredDimension は、 ViewGroupの測定結果を保存するために使用されます。
- onLayout は、各サブビューのレイアウトメソッドを呼び出し、 onMeasureに格納された測定結果に基づいてサブビューのマッピング範囲を設定します。
-
XMLファイルで定義したいくつかのサブビューをTagGroupViewに配置し、実装効果を確認します
基本的な考え方は説明されました。各ステップの実装方法を見てみましょう。
ステップ 1: ランダム ビューを定義する
毎回異なる幅、高さ、色を生成できるテキスト ビューの定義については、特に言うことはありませんが、唯一注意が必要なのは乱数の生成です。
- シードはランダムな方法で取得する必要があります。現在の CPU パフォーマンスは非常に高いため、現在のシステムのミリ秒時間をシードとして使用することはできません。この方法では、すべてのテキスト ビューが同じになります。
- **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>
効果を見てみますと、はい、基本的には要件を満たしており、自動で折り返すこともでき、高さも規格を満たしています。