當(dāng)Android SDK中提供的系統(tǒng)UI控件無法滿足業(yè)務(wù)需求時(shí)精置,我們就需要考慮自己實(shí)現(xiàn)UI控件姐直。
自定義UI控件有2種方式:
- 繼承自系統(tǒng)提供的成熟控件(比如LinearLayout、RelativeLayout葛家、ImageView等)
- 直接繼承自系統(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è)問題:
- 如何根據(jù)相應(yīng)的屬性將UI元素繪制到界面
- 如何自定義控件大小边败,也就是測(cè)量布局的寬高
- 如果是ViewGroup,該如何安排其內(nèi)部子View的擺放位置
以上3個(gè)問題依次在如下3個(gè)方法中可以得到解決:
- onDraw
- onMeasure
- 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操作方法,如下:
從上圖可以看出辐益,Canvas每次繪制都需要傳入一個(gè)Paint對(duì)象断傲,Paint就相當(dāng)于一個(gè)畫筆,我們可以通過畫筆的各種屬性智政,來實(shí)現(xiàn)不同的繪制效果:
我們通過一個(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é)果如下圖:
輸出一下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)行顯示效果如下:
另外我們也看看此時(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è)量過程股缸,如下圖
所有工作都是在 onMeasure 方法中完成,方法定義如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
可以看出吱雏,該方法會(huì)傳入2個(gè)參數(shù)widthMeasureSpec
和heightMeasureSpec
敦姻。這兩個(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所占大小赞厕,然后才能確定自己的大小蜈项。比如以下代碼:
上圖可以看出LinearLayout的最終寬度是由其內(nèi)部最大的子View寬度決定的。
當(dāng)我們自定義一個(gè)ViewGroup時(shí)晨雳,也需要在onMeasure中綜合考慮子View的寬度行瑞。比如要實(shí)現(xiàn)一個(gè)流式布局FlowLayout,效果如下:
在大多數(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"/>
最終界面展示如下圖:
至此我們自定義基本上就研究明白了
源碼已上傳至Github https://github.com/eegets/MeasureViewTest
最后感謝 大神姜新星的Android進(jìn)階