Android實(shí)現(xiàn)太極圖、跑馬燈(自定義View)

ANDROID 自定義 VIEW
雖然常用的 view 能夠滿足大部分需求,但是通過自定義 view 通常能夠更加方便的達(dá)到相應(yīng)目的。比如通過自定義 TextView
來實(shí)現(xiàn)一些文本動畫,只需要重寫相應(yīng)的部分代碼即可達(dá)到預(yù)期,比通過各種 view
組合實(shí)現(xiàn)更加方便。本文章將通過自定義 TextView
實(shí)現(xiàn)一個(gè)文本動畫效果,以及實(shí)現(xiàn)旋轉(zhuǎn)太極動畫進(jìn)行自定義 View
介紹。

實(shí)現(xiàn)自定義 View 的方式
繼承
View
類繼承已實(shí)現(xiàn)的
View
,如:TextView、ImageView等

旋轉(zhuǎn)太極
通過旋轉(zhuǎn)太極圖的案例介紹 SurfaceView
的使用以及好處。
SurfaceView與View的區(qū)別在于:
View為主動更新視圖;SurfaceView為被動更新視圖。
View刷新在主線程中;SurfaceView則開啟子線程進(jìn)行刷新。
View在繪圖時(shí)沒有實(shí)現(xiàn)雙緩沖機(jī)制,SurfaceView在底層機(jī)制中就實(shí)現(xiàn)了雙緩沖機(jī)制。

實(shí)現(xiàn)過程
繼承
SurfaceView
和實(shí)現(xiàn)SurfaceHolder.Callback
接口
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
? ?override fun surfaceCreated(holder: SurfaceHolder) {
? ? ? ?// SurfaceView創(chuàng)建時(shí)候調(diào)用(僅一次)
? ?}
? ?override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
? ? ? ?// SurfaceView發(fā)生改變時(shí)調(diào)用
? ?}
? ?override fun surfaceDestroyed(holder: SurfaceHolder) {
? ? ? ?// SurfaceView發(fā)生銷毀時(shí)調(diào)用(僅一次)
? ?}
}
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
? ?private lateinit var mBlackPaint: Paint
? ?private lateinit var mWhitePaint: Paint
? ?private lateinit var mCanvas: Canvas
? ?private var mHolder: SurfaceHolder
? ?init {
? ? ? ?mBlackPaint = Paint().apply {
? ? ? ? ? ?isAntiAlias = true
? ? ? ? ? ?strokeWidth = 2f
? ? ? ? ? ?color = Color.BLACK
? ? ? ? ? ?style = Paint.Style.FILL
? ? ? ?}
? ? ? ?mWhitePaint = Paint().apply {
? ? ? ? ? ?isAntiAlias = true
? ? ? ? ? ?strokeWidth = 2f
? ? ? ? ? ?color = Color.WHITE
? ? ? ? ? ?style = Paint.Style.FILL
? ? ? ?}
? ? ? ?mHolder = holder.apply {
? ? ? ? ? ?addCallback(this@TaiChiView)
? ? ? ?}
? ?}
? ?// 省略的代碼見上一節(jié)
...
}
定義繪制函數(shù),并在
surfaceCreated
方法中調(diào)用
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
? ?...
? ?private fun draw() {
? ? ? ?// 獲取畫布
? ? ? ?mCanvas = mHolder.lockCanvas()
? ? ? ?mCanvas.drawColor(Color.LTGRAY)
? ? ? ?// 定義曲線繪制的矩形,這里將太極圖居中繪制,且大圓半徑為屏幕寬度一半
? ? ? ?val rectLeft = width / 4f
? ? ? ?val rectTop = height / 2f - rectLeft
? ? ? ?val rectRight = width * 3 / 4f
? ? ? ?val rectBottom = height / 2f + rectLeft
? ? ? ?// 繪制左右半圓
? ? ? ?mCanvas.drawArc(RectF(rectLeft, rectTop, rectRight, rectBottom), 270f, -180f, true, mBlackPaint)
? ? ? ?mCanvas.drawArc(RectF(rectLeft, rectTop, rectRight, rectBottom), 270f, 180f, true, mWhitePaint)
? ? ? ?// 在左右半圓里繪制新的半圓,達(dá)到太極圖的分割效果
? ? ? ?mCanvas.drawArc(
? ? ? ? ? ?RectF(width / 2f - rectLeft / 2f, rectTop, width / 2f + rectLeft / 2f, height / 2f),
? ? ? ? ? ?270f,
? ? ? ? ? ?-180f,
? ? ? ? ? ?true,
? ? ? ? ? ?mWhitePaint
? ? ? ?)
? ? ? ?mCanvas.drawArc(
? ? ? ? ? ?RectF(width / 2f - rectLeft / 2f, height / 2f, width / 2f + rectLeft / 2f, rectBottom),
? ? ? ? ? ?270f,
? ? ? ? ? ?180f,
? ? ? ? ? ?true,
? ? ? ? ? ?mBlackPaint
? ? ? ?)
? ? ? ?// 繪制太極圖的兩個(gè)小圓
? ? ? ?mCanvas.drawCircle(width / 2f, height / 2f - rectLeft / 2f, rectLeft / 4f, mBlackPaint)
? ? ? ?mCanvas.drawCircle(width / 2f, height / 2f + rectLeft / 2f, rectLeft / 4f, mWhitePaint)
? ? ? ?mHolder.unlockCanvasAndPost(mCanvas)
? ?}
? ?override fun surfaceCreated(holder: SurfaceHolder) {
? ? ? ?draw()
? ?}
? ?...
}
到此,一個(gè)太極圖便成功繪制,效果圖如下:


接下來給繪制的太極圖添加旋轉(zhuǎn)動畫,新增角度變量聲明和動畫播放開關(guān)。
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
? ?...
? ?
? ?// 添加動畫
? ?private var mDegrees = 0f
? ?private var isRunning = true
? ?...
}
修改圖形繪制方法,讓畫布圍繞圖形中旋轉(zhuǎn)。
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
? ?...
? ?private fun draw() {
? ? ? ?mCanvas = mHolder.lockCanvas()
? ? ? ?mCanvas.drawColor(Color.LTGRAY)
? ? ? ?val rectLeft = width / 4f
? ? ? ?val rectTop = height / 2f - rectLeft
? ? ? ?val rectRight = width * 3 / 4f
? ? ? ?val rectBottom = height / 2f + rectLeft
? ? ? ?mCanvas.save()
? ? ? ?// 設(shè)置旋轉(zhuǎn)角度
? ? ? ?mCanvas.rotate(mDegrees, width / 2f, height/2f)
? ? ? ?// 繪制太極圖的代碼路基
? ? ? ?...
? ? ? ?// restore方法和save方法數(shù)量一致
? ? ? ?mCanvas.restore()
? ? ? ?mHolder.unlockCanvasAndPost(mCanvas)
? ?}
? ?override fun surfaceCreated(holder: SurfaceHolder) {
? ? ? ?// 啟用協(xié)程,不斷繪制太極圖,改變旋轉(zhuǎn)角度
? ? ? ?CoroutineScope(Dispatchers.Default).launch {
? ? ? ? ? ?while (isRunning) {
? ? ? ? ? ? ? ?draw()
? ? ? ? ? ? ? ?mDegrees++
? ? ? ? ? ?}
? ? ? ?}
? ?}
...
? ?override fun surfaceDestroyed(holder: SurfaceHolder) {
? ? ? ?// SurfaceView銷毀則停止動畫
? ? ? ?isRunning = false
? ?}
}
最后,一個(gè)會動的太極圖就成功繪制完成,效果如圖:


文本跑馬燈
使用 TextView
實(shí)現(xiàn)跑馬燈存在一個(gè)焦點(diǎn)的問題,如果不設(shè)置焦點(diǎn),那么跑馬燈無法運(yùn)行起來。通過自定義 TextView
方式重寫 isFocused
方法即可解決問題。
class MarqueeText(context: Context) : TextView(context) {
? ?init {
? ? ? ?// 初始化以下textview已提供的跑馬燈屬性
? ? ? ?ellipsize = TextUtils.TruncateAt.MARQUEE
? ? ? ?isSingleLine = true
? ? ? ?// 永久循環(huán)
? ? ? ?marqueeRepeatLimit = -1
? ? ? ?isSelected = true
? ?}
? ?// 重寫isFocused方法,讓textview失蹤獲取焦點(diǎn)
? ?override fun isFocused(): Boolean = true
}
注意:TextView
跑馬燈需要文本足夠長才能夠動起來。
效果如圖:

項(xiàng)目地址:https://github.com/laoheix/lao_hei_dome.git
使用Android Compose顯示,存在SurfaceView跳轉(zhuǎn)后Canvas為空的異常,后續(xù)修復(fù)。