Android 自定义View之八卦图

1,201 阅读4分钟

Android Custom View: BaGuaView

[toc]

最近想熟悉一下自定义 view 的内容,于是写了下面这个八卦图,记录一下开发流程。

bagua

自定义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。

github

loading