Android 自定义View:绘制轮盘扇形区并加入扇形区点击事件

本文介绍了一个使用Kotlin实现的轮盘视图,详细解释了如何利用Canvas和Path绘制旋转轮盘,并处理点击事件以识别点击的扇形区域。作者分享了在处理旋转和点击匹配时遇到的误区及最终的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


I. 前言

还记得是五六年前写的demo,用的 canvas.drawArc() 及 旋转画布等实现了,绘制轮盘,当初不会path,不知道怎么搞 扇形区的点击事件… 强行搁置了… 后来学了Path后,也没去改它。这两天用 kotlin 重写了下,path玩了起来,然而写点击的扇形区域匹配时,且在有旋转角度后,先入为主的就走入了误区,

val bounds = RectF()
path.computeBounds(bounds, true)

想着,用

matrix.postRoate(angle) 
matrix.mapRect(bounds)

得到旋转后的矩形,去构建 Region ,去匹配 旋转后的 扇形点击区。从而走上了不归路…
最后发现按中心点旋转后,原始矩形 R,会变得倾斜,这时原始的left坐标,目测看来,可能是left/right/top/bottom,恩,是的,(因角度的不同)都可能;且通过debug,发现matrix的values数组中,出现了scale值;所以,看到这种结果,就想,应该是在矩阵旋转后,原始R它此时所在的外矩形,被写回了 R,所以会有scale缩放值…

最终的解决方案反而很简单,就是在 path.addArc() 中的 startAngle 值,加上旋转的角度就可以了。


II. 效果图

roulette view

III. 源码

package com.stone.stoneviewskt.ui.roulette

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.util.getRandomColor
import com.stone.stoneviewskt.util.loge
import java.util.*
import kotlin.math.min

/**
 * desc   :
 * author : stone
 * email  : aa86799@163.com
 * time   : 09/06/2017 23 28
 */
class RouletteView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
    View(context, attrs, defStyleAttr), Runnable {

    private val mPaint: Paint
    private var mThread: Thread? = null
    private val mScreenWith: Int = resources.displayMetrics.widthPixels
    private val mScreenHeight: Int = resources.displayMetrics.heightPixels
    var mRadius: Int = 100
    var mPart: Int = 1 //等分数
        set(value) {
            field = value
            for (i in 0 until value) {
                if (i == 0)
                    mColorList.add(Color.RED)
                else
                    mColorList.add(getRandomColor())
                mPathList.clear()
                mPathList.add(Path())
                loge("color list " + mColorList.size)
            }
        }
    private var mMin: Int = 200
    private var mCenterX: Int = 300
    private var mCenterY: Int = 300
    private val outRectF = RectF()
    private var mAngle: Float = 0.toFloat()
    private val mColorList: MutableList<Int> = mutableListOf()
    private var mRunFlag: Boolean = false
    private var mRunEnd: Boolean = false
    private var mStartRunTime: Long = 0
    private var mEndRunTime: Long = 0
    private var mAcceleration = 5
    private var mMaxAcceleration = 50
    private var mPathList: MutableList<Path> = mutableListOf()
    private var mOnPartClickListener: ((index: Int) -> Unit)? = null

    interface OnPartClickListener {
        fun onPartClick(index: Int)
    }

    fun setOnPartClickListener(block: (index: Int) -> Unit) {
        this.mOnPartClickListener = block
    }

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context) : this(context, null)

    init {
        mMin = min(mScreenWith, mScreenHeight)
        mRadius = mMin / 2  //使用小的一边作为半径默认值

        this.mPaint = Paint()
        this.mPaint.isAntiAlias = true //设置画笔无锯齿
        this.mPaint.strokeWidth = 6f
        this.mPaint.style = Paint.Style.FILL
        this.mPaint.textSize = 30f

        isFocusable = true //设置焦点

        if (attrs != null) {
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                mRadius.toFloat(),
                resources.displayMetrics
            )
            val array = context.obtainStyledAttributes(attrs, R.styleable.RouletteView)
            mRadius = array.getDimension(R.styleable.RouletteView_radius, mRadius.toFloat()).toInt()
            mPart = array.getInt(R.styleable.RouletteView_part, mPart)
            array.recycle()
        }
        mPathList.clear()
        for (i in 0 until mPart) {
            if (i == 0)
                mColorList.add(Color.RED)
            else
                mColorList.add(getRandomColor())
            mPathList.add(Path())
            loge("color list " + mColorList.size)
        }
    }

    fun startRotate() {
        mAngle = 0f
        mRunFlag = true
        mRunEnd = false
        mThread = Thread(this)
        mThread?.start()
        mStartRunTime = System.currentTimeMillis()
    }

    fun stopRotate() {
        mRunEnd = true//要停止run
        mEndRunTime = System.currentTimeMillis()
    }

    override fun run() {
        while (mRunFlag) {
            val start = System.currentTimeMillis()
            logic()
            postInvalidate()
            val end = System.currentTimeMillis()
            try {
                if (end - start < 50) {
                    Thread.sleep(50 - (end - start))
                }
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }

        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(mMin, mMin)
    }

    override fun onDraw(canvas: Canvas) {
        //画布旋转
//        canvas.rotate(mAngle, mCenterX.toFloat(), mCenterY.toFloat())

//        canvas.drawPaint(this.mPaint)

        //绘制在横向居中 顶部 位置
        mCenterX = mMin / 2
        mCenterY = mRadius
        canvas.drawCircle(mCenterX.toFloat(), mCenterY.toFloat(), mRadius.toFloat(), this.mPaint)

        //整个圆的外矩形   原平稳的矩形旋转后,其所在范围的外矩形大可能会变大,因为屏幕上还是以 ltrb来 计算绘制
        outRectF.set(
            (mCenterX - mRadius).toFloat(),
            0f,
            (mCenterX + mRadius).toFloat(),
            (2 * mRadius).toFloat()
        )

        //等分圆 绘制扇形
//        for (i in 0 until mPart) {
//            this.mPaint.color = mColorList[i]
////            outRectF 穿过中心点的横轴,右方向是0度
////            canvas.drawArc(outRectF, (360 / mPart * i).toFloat(), (360 / mPart).toFloat(), false, this.mPaint)
//            canvas.drawArc(outRectF, (360 / mPart * i).toFloat(), (360 / mPart).toFloat(), true, this.mPaint)
//        }

        //使用 path等分圆,绘制扇形。 方便后续判断 点击的 part index.
        for (i in 0 until mPart) {
            mPathList[i].reset()
            this.mPaint.color = mColorList[i]
            mPathList[i].apply {
                addArc(
                    outRectF,
                    360 / mPart * i + mAngle % 360,
                    (360 / mPart).toFloat()
                )
            }
            mPathList[i].lineTo(mCenterX.toFloat(), mCenterY.toFloat())
            mPathList[i].close()
            canvas.drawPath(mPathList[i], mPaint)

//            canvas.drawText("$i", mPathList[i].)
        }
    }

    private fun logic() {
        if (!mRunEnd) {
            when {
                System.currentTimeMillis() - mStartRunTime > 1500 -> {
                    mAngle += mMaxAcceleration
                }
                System.currentTimeMillis() - mStartRunTime > 500 -> {
                    mAngle += mAcceleration * 2
                }
                System.currentTimeMillis() - mStartRunTime > 50 -> {
                    mAngle += mAcceleration
                }
            }
        } else {
            val random = Random()
            when {
                System.currentTimeMillis() - mEndRunTime > 1500 -> {
                    mAngle += 0f
                    mRunFlag = false
                }
                System.currentTimeMillis() - mEndRunTime > 1000 -> {
                    mAngle += mAcceleration * 2 - random.nextInt(5)
                }
                System.currentTimeMillis() - mEndRunTime > 500 -> {
                    mAngle += mAcceleration * 2 + random.nextInt(5)
                }
                System.currentTimeMillis() - mEndRunTime > 50 -> {
                    mAngle += mMaxAcceleration - 10
                }
            }
        }

    }

    fun getRunStats(): Boolean {
        return mRunFlag
    }

    fun setPart(part: Int) {
        this.mPart = part
        invalidate()
    }

    private fun getPointPart(x: Float, y: Float): Int {
        var resIndex: Int = -1
        mPathList.forEachIndexed { index, path ->
            if (pointIsInPath(x, y, path)) {
                resIndex = index
            }
        }
        return resIndex
    }

    private fun pointIsInPath(x: Float, y: Float, path: Path): Boolean {
        val bounds = RectF()
        path.computeBounds(bounds, true)
        val region = Region()
        region.setPath(
            path,
            Region(
                Rect(
                    bounds.left.toInt(),
                    bounds.top.toInt(),
                    bounds.right.toInt(),
                    bounds.bottom.toInt()
                )
            )
        )

        return region.contains(x.toInt(), y.toInt())
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                stopRotate()
                parent?.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_UP -> {
                val partIndex = getPointPart(event.x, event.y)
//                when (partIndex) {
//                    -1 -> {
//                        showShort("未点击扇形区")
//                    }
//                    else -> {
//                        showShort("扇形区索引=$partIndex")
//                    }
//                }
                mOnPartClickListener?.invoke(partIndex)
            }
        }
        return true
    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值