Android自定義視圖二:如何繪制內(nèi)容

這個系列是老外寫的,干貨栽烂!翻譯出來一起學(xué)習(xí)躏仇。如有不妥恋脚,不吝賜教!

  1. Android自定義視圖一:擴展現(xiàn)有的視圖焰手,添加新的XML屬性
  2. Android自定義視圖二:如何繪制內(nèi)容
  3. Android自定義視圖三:給自定義視圖添加“流暢”的動畫
  4. Android自定義視圖四:定制onMeasure強制顯示為方形

有的時候自持?jǐn)U展一個標(biāo)準(zhǔn)的Android視圖是不夠的糟描。你需要在視圖上繪制你自己的內(nèi)容才行。本文將會講述如何使用Canvas類來繪制一個折線圖书妻,并會講述如何處理尺寸和padding船响。

如果你還沒有準(zhǔn)備好的話,你可能需要閱讀這個系列的前篇躲履。

繪制第一個像素

如果你打算在自定義視圖繪制自己的內(nèi)容的話见间,最好的辦法是繼承基類ViewView是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
}
  1. 把y值映射到視圖的坐標(biāo)系中。
  2. 反轉(zhuǎn)胜宇,如上文所述:視圖的坐標(biāo)系和用戶看到的坐標(biāo)系的y軸方向是反的耀怜。
  3. 加上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)于CanvasPaint的更多資料仿野。

下一篇铣减,我們要給折線圖添加動畫她君。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葫哗,隨后出現(xiàn)的幾起案子缔刹,更是在濱河造成了極大的恐慌,老刑警劉巖劣针,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件校镐,死亡現(xiàn)場離奇詭異,居然都是意外死亡捺典,警方通過查閱死者的電腦和手機鸟廓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來襟己,“玉大人引谜,你說我怎么就攤上這事∏嬖。” “怎么了员咽?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贮预。 經(jīng)常有香客問我贝室,道長,這世上最難降的妖魔是什么仿吞? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任公壤,我火速辦了婚禮察迟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己吞歼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般枣申。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上看杭,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天忠藤,我揣著相機與錄音,去河邊找鬼楼雹。 笑死模孩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贮缅。 我是一名探鬼主播榨咐,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谴供!你這毒婦竟也來了块茁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤桂肌,失蹤者是張志新(化名)和其女友劉穎数焊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崎场,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡佩耳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谭跨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片干厚。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖螃宙,靈堂內(nèi)的尸體忽然破棺而出蛮瞄,到底是詐尸還是另有隱情,我是刑警寧澤污呼,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布裕坊,位于F島的核電站,受9級特大地震影響燕酷,放射性物質(zhì)發(fā)生泄漏籍凝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一苗缩、第九天 我趴在偏房一處隱蔽的房頂上張望饵蒂。 院中可真熱鬧,春花似錦酱讶、人聲如沸退盯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渊迁。三九已至慰照,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琉朽,已是汗流浹背毒租。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留箱叁,地道東北人墅垮。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像耕漱,于是被迫代替她去往敵國和親算色。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內(nèi)容