Potato
Potato copied to clipboard
Android Custom View: BaGuaView
Android Custom View: BaGuaView
[toc]
最近想熟悉一下自定义 view 的内容,于是写了下面这个八卦图,记录一下开发流程。
自定义View的通用步骤
这个自定义 view 主要分为两步,测量和绘制。
其中测量部分几乎是所有自定义 view 的通用步骤。
新建 BaguaView,并实现 onMeasure() 方法。
class Bagua2View @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
下面主要来看测量方法。
相关的内容这里不再多说,如果 测量模式是 AT_MOST, 表示子View具体大小没有尺寸限制,为 wrap_content,但是存在上限,上限一般为父View大小;那么设置一个默认值,大小为屏幕宽度或者高度中的较小值。代码如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 获取宽的测量模式
val wSpecMode = MeasureSpec.getMode(widthMeasureSpec)
// 获取控件提供的 view 宽的最大值
val wSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val hSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val hSpecSize = MeasureSpec.getSize(heightMeasureSpec)
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE)
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, hSpecSize)
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, DEFAULT_SIZE)
}
}
val DEFAULT_SIZE = if (getWinWidth() < getWinHeight()) getWinWidth() else getWinHeight()
中间的太极部分
整个图像来说,中间的太极图宽高是view 宽高的一半。
下面开始画中间的太极部分。
// left表示view到父view左边的距离,因此 xCenter 为 view 中心的x坐标
val xCenter = (right - left) / 2f
val yCenter = (bottom - top) / 2f
paint.color = getColor(R.color.white)
// 先画一个白色的圆形
canvas?.drawCircle(xCenter, yCenter, (size - STROKE_WIDTH) / 2, paint)
中间的太极图可以分为几部分,首先是左右对称的半圆,可以用画圆弧的方法画出来。
paint.color = getColor(R.color.black)
canvas?.drawArc(
0f + size * 0.25f,
0f + size * 0.25f,
size.toFloat() * 0.75f,
size.toFloat() * 0.75f,
-90f,
180f,
false,
paint
)
在白色圆的前提下,画出右边的黑色半圆。前四个参数定义一个矩形,限制圆弧的大小。
后两个参数显示圆弧的长度。从 -90度画到 180度。
接下来分别画出上下两个小一点的圆形,创建出太极图的基本形状。
代码如下:
// 画中间大小的两个圆形
canvas?.drawCircle(xCenter, yCenter * 1.25f, size / 8f, paint)
paint.color = getColor(R.color.white)
canvas?.drawCircle(xCenter, yCenter * 0.75f, size / 8f, paint)
canvas?.drawCircle(xCenter, yCenter * 1.25f, size / 32f, paint)
paint.color = getColor(R.color.black)
其中前两个参数是圆形的圆心坐标,第三个参数是圆形的半径。
下面就是如何让这个太极图转起来。
思路就是通过旋转画布,重新绘制整个太极图,造成一种旋转太极的效果。
canvas?.rotate(startAngle * direction, xCenter, yCenter)
第一个参数表示旋转的角度。
private var startAngle: Float = 0f
// 控制顺时针和逆时针
private var direction = 1
val anim = ValueAnimator.ofFloat(0f, 360f)
.apply {
duration = 2000
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener {
startAngle = it.animatedValue as Float
invalidate()
}
}
fun doAnim() {
if (!anim.isStarted) {
anim.start()
return
}
if (anim.isPaused) {
anim.resume()
return
}
anim.pause()
}
通过 ValueAnimator 创建连续的旋转角度,并不断调用 invalidate() 进行重新绘制,是太极图旋转。
外层的部分
思路:先在正上方画出三个黑色矩形,并每次旋转画布45度。重复执行,画出8个卦象。
接着通过变量去控制在每个卦象中间是否再画一个白色的矩形。
代码如下:
private val threeLineList = arrayListOf<List<Boolean>>().apply {
add(arrayListOf(false, false, false))
add(arrayListOf(true, false, false))
add(arrayListOf(true, false, true))
add(arrayListOf(true, true, false))
add(arrayListOf(true, true, true))
add(arrayListOf(false, true, true))
add(arrayListOf(false, true, false))
add(arrayListOf(false, false, true))
}
threeLineList.forEachIndexed { index, list ->
canvas?.rotate(if (index == 0) 0f else 45f, xCenter, yCenter)
drawThreeLine(canvas, list)
}
上面的8个列表来控制卦象的形状,true 表示中间有白色矩形。
fun drawThreeLine(
canvas: Canvas?,
list: List<Boolean>
) {
val firstTop = size * 0.2f - reactHeight / 2
list.forEachIndexed { index, b ->
paint.color = getColor(R.color.black)
val left = size * 0.5f - reactWidth / 2
val top = firstTop - (index * 1.8f) * reactHeight
val right = left + reactWidth
val bottom = top + reactHeight
canvas?.drawRect(left, top, right, bottom, paint)
if (b) {
// 表示地
paint.color = getColor(R.color.white)
val l = left + reactWidth * 0.4f
val t = top - 2
val r = l + reactWidth * 0.2f
val b = bottom + 2
canvas?.drawRect(l, t, r, b, paint)
}
}
}
这里矩形的具体位置可以稍微调整一下。
目前来说,整个图像都可以一起旋转。但怎么做到内外两部分的旋转方向不同。
最开始的想法是自定义内外两个 view,放在 viewgroup 里面,利用属性动画去实现不同方向的旋转。
但其实可以借助上面太极旋转的经验。
首先太极图顺时针旋转 1度,画出太极;外层逆时针旋转2;其实就是在原位置逆时针旋转1度。
不断旋转进行绘制,即可实现一个自定义view里面,不用部分分别做动画的效果。
总结
在上面知识的基础上,也可以画出下面甚至更多的自定义 view。