Android实现太极图、跑马灯(自定义View)

ANDROID 自定义 VIEW
虽然常用的 view 能够满足大部分需求,但是通过自定义 view 通常能够更加方便的达到相应目的。比如通过自定义 TextView
来实现一些文本动画,只需要重写相应的部分代码即可达到预期,比通过各种 view
组合实现更加方便。本文章将通过自定义 TextView
实现一个文本动画效果,以及实现旋转太极动画进行自定义 View
介绍。

实现自定义 View 的方式
继承
View
类继承已实现的
View
,如:TextView、ImageView等

旋转太极
通过旋转太极图的案例介绍 SurfaceView
的使用以及好处。
SurfaceView与View的区别在于:
View为主动更新视图;SurfaceView为被动更新视图。
View刷新在主线程中;SurfaceView则开启子线程进行刷新。
View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中就实现了双缓冲机制。

实现过程
继承
SurfaceView
和实现SurfaceHolder.Callback
接口
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// SurfaceView创建时候调用(仅一次)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
// SurfaceView发生改变时调用
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// SurfaceView发生销毁时调用(仅一次)
}
}
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)
}
}
// 省略的代码见上一节
...
}
定义绘制函数,并在
surfaceCreated
方法中调用
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)
// 在左右半圆里绘制新的半圆,达到太极图的分割效果
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
)
// 绘制太极图的两个小圆
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()
}
...
}
到此,一个太极图便成功绘制,效果图如下:


接下来给绘制的太极图添加旋转动画,新增角度变量声明和动画播放开关。
class TaiChiView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
...
// 添加动画
private var mDegrees = 0f
private var isRunning = true
...
}
修改图形绘制方法,让画布围绕图形中旋转。
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()
// 设置旋转角度
mCanvas.rotate(mDegrees, width / 2f, height/2f)
// 绘制太极图的代码路基
...
// restore方法和save方法数量一致
mCanvas.restore()
mHolder.unlockCanvasAndPost(mCanvas)
}
override fun surfaceCreated(holder: SurfaceHolder) {
// 启用协程,不断绘制太极图,改变旋转角度
CoroutineScope(Dispatchers.Default).launch {
while (isRunning) {
draw()
mDegrees++
}
}
}
...
override fun surfaceDestroyed(holder: SurfaceHolder) {
// SurfaceView销毁则停止动画
isRunning = false
}
}
最后,一个会动的太极图就成功绘制完成,效果如图:


文本跑马灯
使用 TextView
实现跑马灯存在一个焦点的问题,如果不设置焦点,那么跑马灯无法运行起来。通过自定义 TextView
方式重写 isFocused
方法即可解决问题。
class MarqueeText(context: Context) : TextView(context) {
init {
// 初始化以下textview已提供的跑马灯属性
ellipsize = TextUtils.TruncateAt.MARQUEE
isSingleLine = true
// 永久循环
marqueeRepeatLimit = -1
isSelected = true
}
// 重写isFocused方法,让textview失踪获取焦点
override fun isFocused(): Boolean = true
}
注意:TextView
跑马灯需要文本足够长才能够动起来。
效果如图:

项目地址:https://github.com/laoheix/lao_hei_dome.git
使用Android Compose显示,存在SurfaceView跳转后Canvas为空的异常,后续修复。