Android自定義檢視四:定製onMeasure強制顯示為方形
這個系列是老外寫的,乾貨!翻譯出來一起學習。如有不妥,不吝賜教!
上一篇開發之後的效果如上圖。不過看著這張圖,需要注意的不是我們自定義檢視展示了什麼,而是這個檢視的大小和位置。你會看到這個折線圖有一個特定的大小(size)。這個size是怎麼定的呢?現在的程式碼是使用了一個豎直方向的LinearLayout
,折線圖和他下面的TextView
使用layout_weight
屬性平分了他們所在的LinearLayout
的高度。那麼如果我們刪掉了TextView
和全部的layout_weight
,並把折線圖的高度設定為wrap_content
會發生什麼呢?
是的,以上修改之後整個的圖就變成了這樣。雖然使用了wrap_content
LinearLayout
。這就是View的預設佈局行為,但是,如果我們要改變一下呢?
View的Layout
自定義檢視顯示在螢幕上一共分三步:measure(測量),layout(佈局),draw(繪製)。基本上一個自定義檢視在測量這一步計算大小,之後可以通過getMeasureWidth
和getMeasureHeight
得到View的寬度和高度。在佈局計算這個自定義檢視的左上和右下座標以及實際的寬度和高度,最後根據以上layout步驟獲得的資料呼叫onDraw
方法把View繪製在螢幕上。
所以要修改size,也就是自定義檢視中修改預設的行為,就需要override
onMeasure()
方法。一般的通用做法是:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
// set default width and height values you defined
setMeasuredDimension(mDefaultWidth, mDefaultHeight)
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
// set default width value and calculated `heightSpecSize` as height
setMeasuredDimension(mDefaultWidth, heightSpecSize)
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
// set calculated `widthSpecSize` as width and default height
setMeasuredDimension(widthSpecSize, mDefaultHeight)
}
}
簡單理解MeasureSpec.AT_MOST
就是你給折線圖的layout_width
或者layout_height
設定了wrap_content
,系統不知道精確的寬度、高度是多少的時候的一個標記。如果有具體的50dp, 100dp的時候,這個標記的值為MeasureSpec.EXACTLY
。一般,一個view的寬度、高度只有這兩種標記。
View的寬、高度測量分別處理三種情況:
1. 如果寬、高度都是AT_MOST
的時候,寬度和高度設定為預設值。
2. 寬度為AT_MOST
高度不是的時候,寬度設定為預設值,高度設定為測量的值heightSpecSize
。
3. 寬度為精確值,高度為AT_MOST
的時候,寬度設定為widthSpecSize
,高度設定為預設值。
而我們這裡則是意外的簡單。因為要設定為正方形,所以使用寬度和高度中相對較小的那個值來作為寬、高度共同的值就可以了:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var size = 0
var width = getMeasuredWidth()
var height = getMeasuredHeight()
if (width > height) {
size = height
} else {
size = width
}
setMeasuredDimension(size, size)
}
如前文所說,我們要把折線圖這個自定義檢視設定為正方形。所以,不管測量的Mode是如何的,只要用寬、高中的最小值就可以了。
而上面說到的layout部分,對於有子view的`View`比較有用,
也就是說對於繼承自`ViewGroup`的`View`來說比較有用。
我們的折線圖知識一個單純的不能更單純的`View`。
我們前面在onDraw
方法使用的getWidth()
和getHeight()
在onMeasure()
方法中都是不可用的。因為這個時候正在計算寬、高度。在這個方法裡只能取到getMeasuredWidth()
和getMeasuredHeight()
。
override onMeasure
方法就一定要在最後呼叫setMeasuredDimension
方法。呼叫setMeasuredDimension
方法是告訴父view當前view的測量高度是多少。如果不呼叫這個方法的話會拋異常。
修改之後的佈局,寬度match_parent
,高度wrap_content
。不必要的屬性都略掉了。
<RelativeLayout>
<LinearLayout>
<Button android:text="walking" />
<Button android:text="Running" />
<Button android:text="Cycling" />
</LinearLayout>
<demo.customview.customviewdemo.Views.SquareLineChartView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
效果:
支援任意寬高比例
一個正方形的View已經非常實用了。比如繼承ImageView
之後像上面一樣override了onMeasure()
方法就可以得到一個一直都是正方形顯示的View。那麼,既然我們已經支援了寬、高1:1了,為什麼不支援任意的寬高比呢。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var width = getMeasuredWidth()
var height = getMeasuredHeight()
var widthWithoutPadding = width - paddingLeft - paddingRight
var heightWithoutPadding = height - paddingTop - paddingBottom
var maxWidth = (heightWithoutPadding * RATIO).toInt()
var maxHeight = (widthWithoutPadding / RATIO).toInt()
if (widthWithoutPadding > maxWidth) {
width = maxWidth + paddingLeft + paddingRight
} else {
height = maxHeight + paddingTop + paddingBottom
}
setMeasuredDimension(width, height)
}
上面的程式碼就可以支援任意的寬高比了。看看效果(比例7:3):