Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android Custom View: BaGuaView #73

Open
yunshuipiao opened this issue Feb 15, 2020 · 0 comments
Open

Android Custom View: BaGuaView #73

yunshuipiao opened this issue Feb 15, 2020 · 0 comments

Comments

@yunshuipiao
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant