這個系列是老外寫的,干貨栽烂!翻譯出來一起學(xué)習(xí)躏仇。如有不妥恋脚,不吝賜教!
- Android自定義視圖一:擴展現(xiàn)有的視圖焰手,添加新的XML屬性
- Android自定義視圖二:如何繪制內(nèi)容
- Android自定義視圖三:給自定義視圖添加“流暢”的動畫
- Android自定義視圖四:定制onMeasure強制顯示為方形
有的時候自持?jǐn)U展一個標(biāo)準(zhǔn)的Android視圖是不夠的糟描。你需要在視圖上繪制你自己的內(nèi)容才行。本文將會講述如何使用Canvas
類來繪制一個折線圖书妻,并會講述如何處理尺寸和padding船响。
如果你還沒有準(zhǔn)備好的話,你可能需要閱讀這個系列的前篇躲履。
繪制第一個像素
如果你打算在自定義視圖繪制自己的內(nèi)容的話见间,最好的辦法是繼承基類View
。View
是UI繪制的最小單元工猜,同時各種功能齊備缤剧。所以我們從繼承View
開始。
要畫出第一個項目域慷,只需要override方法onDraw()
荒辕。在這個方法里我們可以獲得一個canvas(畫布),繪制就在這個canvas上進(jìn)行犹褒。沒有必要調(diào)用超類的onDraw()
實現(xiàn)抵窒,因為其實并沒有什么實現(xiàn)。在View
中這個方法是空的叠骑。
class LineChartView : View {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attributeSet: AttributeSet) : super(ctx, attributeSet) {
}
constructor(ctx: Context, attributeSet: AttributeSet, defStyle: Int) : super(ctx, attributeSet, defStyle) {
}
override fun onDraw(canvas: Canvas) {
val p = Paint()
p.style = Paint.Style.STROKE
p.color = resources.getColor(android.R.color.holo_orange_dark) // 0xFF33B5E5.toInt()
p.strokeWidth = 4f
canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), p)
}
}
布局:
<RelativeLayout>
<demo.customview.customviewdemo.Views.LineChartView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp" />
</RelativeLayout>
為了一個直觀的第一映像李皇,代碼全部貼出來,布局貼主要部分宙枷。Constructor部分可以直接忽略不計掉房。這個自定義視圖會在屏幕上以(0,0)為起點慰丛,以這個視圖的寬和高(width卓囚,height)為終點繪制一條桔色的線。Paint
類用來控制如何繪制诅病。paint對象可以實現(xiàn)很多酷炫的效果哪亿,不過這里只用來繪制一條線。
注意:繪制的坐標(biāo)系是這個自定義視圖贤笆。左上角為(0蝇棉,0)點,也就是坐標(biāo)原點芥永。x軸向右為正值篡殷,y軸向下為正值。
添加padding
我們繪制出了第一條線埋涧,這是一個很好的開始板辽。但是奇瘦,有的時候需要在內(nèi)容的展示上需要留白,也就是需要設(shè)置padding戳气。如果在布局中給剛剛創(chuàng)建的視圖設(shè)置padding,你會發(fā)現(xiàn)沒有什么效果巧鸭。
那是因為在繪制的時候我們并沒有把padding值計算在內(nèi)瓶您,而padding值是包含在視圖的寬度和高度之內(nèi)的。如果視圖的寬度是100像素纲仍,兩邊的padding是10像素那么在一個寬度上可用的值是80像素呀袱。getWidth()
方法返回的是整個視圖的寬度。padding的值可以用方法getPaddingWidth()
來獲得郑叠。當(dāng)視圖設(shè)置了padding后夜赵,正確的繪制方法請看下面的代碼:
override fun onDraw(canvas: Canvas) {
val p = Paint()
p.style = Paint.Style.STROKE
p.color = resources.getColor(android.R.color.holo_orange_dark)
p.strokeWidth = 4f
val left = paddingLeft
val top = paddingTop
val right = width - paddingRight
val bottom = height - paddingBottom
// canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), p)
canvas.drawLine(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), p)
}
在布局中給這個視圖的padding值設(shè)置為android:padding="20dp"
, 繪制結(jié)果如下所示:
雖然有些情況不一定需要自定義視圖支持padding。但是個人建議還是在一開始就把padding考慮進(jìn)來乡革,即使你覺得不需要padding寇僧。給一個有很多子view的自定義視圖添加padding支持非常棘手,但是如果從一開始就考慮padding的話事情會容易很多沸版。
繪制折線圖
一開始就說要繪制一個折線圖嘁傀,那么現(xiàn)在我們正式著手開始繪制。
第一步视粮,我們需要繪制用的數(shù)據(jù)细办。
private var _points: List<Float>? = null
var points: List<Float>
get() = if (_points == null) listOf<Float>() else _points!!
set(value) {
_points = value
}
在Kotlin里給屬性增加getter和setter要簡單很多。private var _points: List<Float>? = null
指定了一個back field蕾殴,用來存放賦值進(jìn)來的數(shù)據(jù)笑撞。在返回的時候,如果_points
為空則返回一個空的數(shù)組钓觉,否則返回原數(shù)組茴肥。在java里需要手動寫一個setter方法。
有了points
屬性荡灾,用戶就可以給我們的自定義視圖添加繪制需要的點數(shù)據(jù)炉爆。這里的points
屬性的值是二維平面的y值。在二維平面繪制總是需要兩個坐標(biāo)來定位繪制的點卧晓。這里假設(shè)x軸的坐標(biāo)值都是視圖的寬度平分得到的芬首,實際上也確實是這樣,那么我們就可以把給定的points
的index作為x值來使用逼裆。
下面我們來研究一下如何把給定的點映射到視圖的坐標(biāo)系內(nèi)郁稍。
fun getYPos(yValue: Float, maxValue: Float): Float {
var drawHeight = height - paddingTop - paddingBottom
var drawYValue = (yValue / maxValue) * drawHeight // 1
drawYValue = drawHeight - drawYValue // 2
drawYValue += paddingTop // 3
return drawYValue
}
- 把y值映射到視圖的坐標(biāo)系中。
- 反轉(zhuǎn)胜宇,如上文所述:視圖的坐標(biāo)系和用戶看到的坐標(biāo)系的y軸方向是反的耀怜。
- 加上padding的offset值
現(xiàn)在我們已經(jīng)可以繪制折線圖了恢着。我們可以通過在兩點之間連線的方式來繪制折線圖,但是還有一個更好的辦法财破。使用Path
£桑現(xiàn)在看來不會有太大的不同,不過隨著后面代碼的深入你會發(fā)現(xiàn)大有不同∽罅。現(xiàn)在的onDraw
方法看起來是這樣的:
override fun onDraw(canvas: Canvas?) {
var maxValue = getMax(this.points)
var path = Path()
path.moveTo(getXPos(0), getYPos(this.points[0], maxValue))
for (i: Int in 1..(points.count() - 1)) {
path.lineTo(getXPos(i), getYPos(points[i], maxValue))
}
var paint = Paint()
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
paint.color = resources.getColor(android.R.color.holo_orange_dark)
canvas?.drawPath(path, paint)
}
前面我們介紹了getYPos()
方法靡羡,上面的代碼中還用到了一個類似的getXPos()
方法。這個方法就是按照points
數(shù)組的元素個數(shù)平分X軸俊性,確定X軸的單元寬度有多少略步,并返回每一個index對應(yīng)的X軸的值。
第二部分基本上和前面的繪制方法一樣定页,只不過這里使用了drawPath()
方法而不是drawLine()
方法趟薄。
運行結(jié)果:
添加細(xì)節(jié)
首先要做的就是添加抗鋸齒。開啟這個功能之后典徊,圖像看起來更加順滑杭煎。抗鋸齒可以這樣開啟:
paint.isAntiAlias = true
另一個可以讓圖像看起來效果更好的功能是給線圖添加陰影:
paint.setShadowLayer(4f, 2f, 2f, resources.getColor(android.R.color.darker_gray))
現(xiàn)在是用paint
對象繪制的所有東西都會添加一個陰影卒落。不過岔帽,在上面這行代碼智商還需要添加一點佐料,否則的話上面的代碼會不聽使喚导绷。在方法的最后一個參數(shù)中指定的顏色不會起作用犀勒。添加如下代碼:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_SOFTWARE, paint)
}
setShadowLayer()
這個方法需要硬件加速關(guān)閉,但是在HONEYCOMB
和以上版本中默認(rèn)這個功能是開啟的妥曲。所以我們要讓他在LAYER_TYPE_SOFTWARE
條件下也能工作贾费。
最后回到setShadowLayer()
方法,第一個參數(shù)是模糊半徑檐盟,值越大模糊的半徑就越大褂萧,同時顏色就越模糊。就像一瓶墨水倒進(jìn)多少水里一樣葵萎,誰越多导犹,顏色越淺,就是這個道理羡忘。第二谎痢、三個參數(shù)是指定了陰影向右移2像素,向下移2像素卷雕。最后一個參數(shù)指定顏色节猿。
下面在背景添加橫向的線:
private fun drawBackground(canvas: Canvas) {
var maxValue = getMax(points)
var range = getLineDistance(maxValue)
paint.style = Paint.Style.STROKE
paint.strokeWidth = 2f
paint.color = resources.getColor(android.R.color.background_light)
for (i: Int in 0..maxValue.toInt() - 1 step range) {
var yPos = getYPos(i.toFloat(), maxValue)
canvas.drawLine(0f, yPos, width.toFloat(), yPos, paint)
}
}
這樣看起來就更像一個圖表了。
從這里開始,我們的折線圖可以添加更多的東西了滨嘱。這樣這個自定義的折線圖就會更加的美觀峰鄙。在Canvas
上,可以繪制線太雨、路徑吟榴,長方形和橢圓等。使用Path
囊扳,我們可以修改填充方式吩翻、顏色、填充寬度等各種效果宪拥。官方文檔中有關(guān)于Canvas和Paint的更多資料仿野。
下一篇铣减,我們要給折線圖添加動畫她君。