前语

上一篇做了一个简略的六边形评分控件,首要对paint的api了解了一下,原本还想对六边形控件加入扩大旋转的功用,可是paint的api内容够多了,就算了。今日把上一篇的扩大旋转功用加到了这篇文章的扇形图里边,对安卓的多点触控学习了一下。

需求

这儿便是用传入的数据画一个扇形图,可是画完扇形图后,能够支撑多点触控,运用一只手指滑动时触发旋转,两只手指时触发缩放,三指手指时触发移动,四只手指以上时触发重置。核心思想如下:

  • 1、外面传入数据,完成扇形图展现
  • 2、扇形图能够单指旋转、二指扩大、三指移动,四指以上一起按下进行复位
  • 3、旋转、扩大、平移效果能够叠加

效果图

这儿效果图并不是很好,由于我这手机是淘宝买的屏幕自己换的,会断触。并且我发现多个手指这样合作功用上也是有抵触的,多个手指逐步脱离屏幕时会到不同的状况,任何微小的移动,都会触发MOVE事情,从而造成不正常的改动,不知道其他有这种功用的控件怎么处理的,等有时间得研讨研讨。

自定义view实战(9):多点触控扇形图

代码

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import kotlin.math.*
/**
 * 多点触控饼状图
 *
 * @author silence
 * @date 2022-11-04
 */
class PieChartView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): View(context, attributeSet, defStyleAttr) {
    companion object{
        // 没有手指按下状况
        const val STATE_NORMAL = 0
        // 单指按下,进行旋转
        const val STATE_ROTATING = 1
        // 双指按下,进行缩放
        const val STATE_SCALING = 2
        // 三指按下,进行移动
        const val STATE_MOVING = 3
        // 三指以上按下,进行复位
        const val STATE_RESETTING = 4
    }
    /**
     * 数据,所占份额依据单位占总数核算
     */
    var data: MutableList<Int>? = null
    set(value) {
        field = value
        calculatePercent()
    }
    // 用于图表制作的数据
    private val mPieData: MutableList<Triple<Int, Float, Int>> = ArrayList()
    // 半径占最小边框的份额
    private val mRadiusPercent = 0.8f
    // 画笔
    private val mPaint: Paint = Paint().apply {
        // 色彩
        color = Color.BLACK
        // 粗细,设置为0时不管怎么扩大 都是1像素
        strokeWidth = 5f
        // 抗锯齿
        flags = Paint.ANTI_ALIAS_FLAG
        // 填充形式
        style = Paint.Style.FILL
    }
    // 矩形, 制作弧形需要用到
    private var mRectF: RectF = RectF()
    // 中点坐标
    private var mCenterX: Int = 0
    private var mCenterY: Int = 0
    // 圆的半径
    private var mRadius: Float = 0f
    // 状况
    private var mState = STATE_NORMAL
    // 单指状况
    // 开始坐标
    private var mLastX = 0f
    private var mLastY = 0f
    private var mDegree = 0f
    private var mCountDegree = 0f
    // 双指状况
    // 第二个手指按下时两指间的间隔,即初始间隔,扩大份额以此为基准
    private var mFirstDistance = 1f
    private var mCurrentDistance = 1f
    // 三指状况
    // 上一次三点中心
    private var mLastTripleCenterX = 0f
    private var mLastTripleCenterY = 0f
    // 本次移动值
    private var mMoveX = 0f
    private var mMoveY = 0f
    // 累计移动值
    private var mCountMoveX = 0f
    private var mCountMoveY = 0f
    // 核算份额
    private fun calculatePercent() {
        var count = 0
        val colors = arrayOf(Color.BLUE, Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN)
        // 核算总数
        for (i in data!!) {
            count += i
        }
        // 填入份额
        var color = colors[0]
        var lastColor = colors[1]
        for (i in data!!) {
            // 避免相邻色彩相同
            while (color == lastColor) {
                color = colors[(Math.random() * colors.size).toInt()]
            }
            mPieData.add(Triple(i, i / count.toFloat(), color))
            lastColor = color
        }
        Log.e("TAG", "calculatePercent: mPieData=$mPieData")
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 自定义view要设置好默认大小
        val width = getDefaultSize(100, widthMeasureSpec)
        val height = getDefaultSize(100, heightMeasureSpec)
        // 由控件宽高取得中心点坐标
        mCenterX = width / 2
        mCenterY = height / 2
        // 半径,设置为最小宽度的80%
        mRadius = (if (mCenterX < mCenterY) mCenterX else mCenterY) * mRadiusPercent
        // 制作的矩形
        mRectF.set(mCenterX - mRadius, mCenterY - mRadius,
            mCenterX + mRadius, mCenterY + mRadius)
        setMeasuredDimension(width, height)
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent): Boolean {
        when(ev.actionMasked) {
            // 第一个点按下
            MotionEvent.ACTION_DOWN -> {
                //Log.e("TAG", "ACTION_DOWN --> 1")
                // 一只手指时旋转
                mState = STATE_ROTATING
                mLastX = ev.x
                mLastY = ev.y
            }
            // 第二个或许以上的点按下
            MotionEvent.ACTION_POINTER_DOWN -> {
                when (ev.pointerCount) {
                    2 -> {
                        //Log.e("TAG", "ACTION_POINTER_DOWN --> 2")
                        mState = STATE_SCALING
                        // 两指时核算初始间隔
                        mFirstDistance = getDistance(
                            ev.getX(0), ev.getY(0),
                            ev.getX(1), ev.getY(1))
                        mCurrentDistance = mFirstDistance
                    }
                    3 -> {
                        Log.e("TAG", "ACTION_POINTER_DOWN --> 3")
                        mState = STATE_MOVING
                        // 三指时核算三点的中心
                        val pair = getTripleCenter(
                            ev.getX(0), ev.getY(0),
                            ev.getX(1), ev.getY(1),
                            ev.getX(2), ev.getY(2))
                        mLastTripleCenterX = pair.first
                        mLastTripleCenterY = pair.second
                        Log.e("TAG", "ACTION_POINTER_DOWN --> ($mLastTripleCenterX, $mLastTripleCenterY)")
                    }
                    else -> {
                        //Log.e("TAG", "ACTION_POINTER_DOWN --> 4+")
                        mState = STATE_RESETTING
                        // 更改状况,刷新即可
                        invalidate()
                    }
                }
            }
            // 所有的点移动
            MotionEvent.ACTION_MOVE -> {
                // 移动时处理,假如需要盯梢手指,需要用到actionIndex、PointerId、PointerIndex
                if (ev.pointerCount == 1) {
                    //Log.e("TAG", "ACTION_MOVE --> 1")
                    // 当时点和上一次的点核算视点
                    mDegree = getDegree(mLastX, mLastY, ev.x, ev.y).toFloat()
                    mLastX = ev.x
                    mLastY = ev.y
                    invalidate()
                }else if (ev.pointerCount == 2) {
                    //Log.e("TAG", "ACTION_MOVE --> 2")
                    val newDistance = getDistance(
                        ev.getX(0), ev.getY(0),
                        ev.getX(1), ev.getY(1))
                    // 缩小扩大,限制些规模削减调用,不必touchSlop值太小了
                    if (abs(mCurrentDistance - newDistance) > 5) {
                        // 更新
                        mCurrentDistance = newDistance
                        // 不运用scaleX和scaleY,直接onDraw里边自己处理
                        invalidate()
                    }
                }else if(ev.pointerCount == 3) {
                    //Log.e("TAG", "ACTION_MOVE --> 3")
                    // 三指时核算三点的中心
                    val pair = getTripleCenter(
                        ev.getX(0), ev.getY(0),
                        ev.getX(1), ev.getY(1),
                        ev.getX(2), ev.getY(2))
                    mMoveX = pair.first - mLastTripleCenterX
                    mMoveY = pair.second - mLastTripleCenterY
                    // 限制移动规模
                    val maxLengthX = width / 2f - mRadius
                    val maxLengthY = height / 2f - mRadius
                    if ((mCountMoveX + mMoveX) >= -maxLengthX ||
                        (mCountMoveX + mMoveX) <= maxLengthX ||
                        (mCountMoveY + mMoveY) >= -maxLengthY ||
                        (mCountMoveY + mMoveY) <= maxLengthY) {
                        mLastTripleCenterX = pair.first
                        mLastTripleCenterY = pair.second
                        //Log.e("TAG", "ACTION_POINTER_DOWN --> ($mLastTripleCenterX, $mLastTripleCenterY)")
                        invalidate()
                    }else{
                        mMoveX = 0f
                        mMoveY = 0f
                    }
                }
            }
            // 非最后一个点抬起
            MotionEvent.ACTION_POINTER_UP -> {
                //Log.e("TAG", "ACTION_POINTER_UP --> ${ev.pointerCount}")
                // 经过测试,手指抬起时还未更改pointerCount
                when (ev.pointerCount) {
                    2 -> mState = STATE_ROTATING
                    3 -> mState = STATE_SCALING
                    4 -> mState = STATE_MOVING
                }
            }
            // 最后一个点抬起
            MotionEvent.ACTION_UP -> {
                //Log.e("TAG", "ACTION_UP --> 1")
                mState = STATE_NORMAL
            }
        }
        // 注意阻拦事情,至少阻拦ACTION_DOWN
        return true
    }
    private fun getDistance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        // 平方和公式
        @Suppress("ReplaceJavaStaticMethodWithKotlinAnalog")
        return sqrt((Math.pow((x1 - x2).toDouble(), 2.0)
                + Math.pow((y1 - y2).toDouble(), 2.0)).toFloat())
    }
    private fun getTripleCenter(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float)
        : Pair<Float, Float> {
        val x = (x1 + x2 + x3) / 3
        val y = (y1 + y2 + y3) / 3
        return Pair(x, y)
    }
    private fun getDegree(x1: Float, y1: Float, x2: Float, y2: Float): Double {
        // 带上移动偏移量的圆心
        val centerX = mCenterX + mCountMoveX
        val centerY = mCenterY + mCountMoveY
        // 起点视点
        val radians1 = atan2(y1 - centerY, x1 - centerX).toDouble()
        // 终点视点
        val radians2 = atan2(y2 - centerY, x2 - centerX).toDouble()
        // 从弧度转换成视点
        // Log.e("TAG", "getDegree: $degree")
        return Math.toDegrees(radians2 - radians1)
    }
    override fun onDraw(canvas: Canvas) {
        // 混合效果
        if (mState != STATE_RESETTING) {
            // 对canvas进行旋转,注意每次canvas都会复位,所以要用累加值
            if (mState == STATE_ROTATING) {
                mCountDegree += mDegree
            }
            canvas.rotate(mCountDegree, mCenterX.toFloat() + mCountMoveX,
                mCenterY.toFloat() + mCountMoveY)
            // 对canvas进行缩放
            val scale = mCurrentDistance / mFirstDistance
            canvas.scale(scale, scale, mCenterX.toFloat(), mCenterY.toFloat())
            // 对canvas进行移动
            if(mState == STATE_MOVING) {
                mCountMoveX += mMoveX
                mCountMoveY += mMoveY
            }
            canvas.translate(mCountMoveX, mCountMoveY)
        }else {
            // 重置
            mCountMoveX = 0f
            mCurrentDistance = mFirstDistance
            mCountMoveX = 0f
            mCountMoveY = 0f
        }
        // 制作圆弧
        var angleCount = 0f
        for (peer in mPieData) {
            val angle = 360 * peer.second
            mPaint.color = peer.third
            canvas.drawArc(mRectF, angleCount, angle, true, mPaint)
            angleCount += angle
        }
    }
}

首要问题

扇形图的功用就没什么好说的了,下面首要讲讲多点触控以及制作的问题。

MotionEvent的action和actionMasked

在处理TouchEvent的时分,咱们一般时经过MotionEvent的action来判别事情类型,可是运用这种方法取得的事情类型只要DOWN、MOVE、UP三种事情,当需要多点触控的时分就不能用action来判别了,而是应该用actionMasked。运用actionMasked才干拿到ACTION_POINTER_DOWN和ACTION_POINTER_UP事情。

下面是两种方法get的源码

    public final int getAction() {
        return nativeGetAction(mNativePtr);
    }
    public final int getActionMasked() {
        return nativeGetAction(mNativePtr) & ACTION_MASK;
    }
ACTION_DOWN和ACTION_POINTER_DOWN

在运用actionMasked后,咱们就能拿到ACTION_POINTER_DOWN事情,这个事情是在第二个或许以上的手指按下的时分才触发。可是第一个手指触发仍是在ACTION_DOWN事情,所以处理多个手指时,还得在两个事情中都要处理下。

ACTION_MOVE

多点触控的按下事情和抬起事情都会别的处理,可是ACTION_MOVE是不会别的处理的,所有的MOVE事情都会在ACTION_MOVE中触发。

MotionEvent的pointerCount

由于上面ACTION_MOVE的原因,咱们怎么在其中判别是几个手指呢?这儿就要用到pointerCount了,它能够取得当时按下手指的个数,所以当你不需要盯梢手指,只需要判别有几个的问题时,用它就能够了,假如还要盯梢手指那就要看下面的几个参数了。

actionIndex、PointerId、PointerIndex

这儿能够看看我上一篇转载的文章:Android多点触控详解,里边写的还不错。

我这就简略讲讲,actionIndex便是一个会复用的action的下标,还会自动改动值坚持顺序(中间抬起,紧挨着的后边补上),可是没法在ACTION_MOVE中运用。PointerId是唯一的,每一个手指事情序列的PointerId是不会变的(actionIndex是会改动的),可是新增的PointerId会填充前面空缺的方位。PointerIndex的效果便是在ACTION_MOVE中代替actionIndex,由于在ACTION_MOVE中actionIndex一直是0,但仍是要有一个按下手指的下标的,那便是PointerIndex了。

所以假如要盯梢手指的移动状况,就需要用到pointerId和pointerIndex了,合作下面两个方法就能处理了。

ev.findPointerIndex(pointerId)
ev.getPointerId(pointerIndex)
一指旋转

上面多点触控讲的差不多了,要判别一指的移动事情,只需要在ACTION_MOVE处理pointerCount等于1时的状况。旋转最重要的便是旋转视点,这儿只需要知道起始两点相对于X坐标的夹角就行,两个视点一减便是旋转视点了。算的视点后,运用invalidate()触发更新,在onDraw里对canvas rotate就行了。

这儿要注意下每次canvas都会复位,所以要自己统计一个累加值。

双指扩大

和旋转相似,只不过要在ACTION_POINTER_DOWN的pointerCount等于2时取得初始的间隔,之后再ACTION_MOVE中再取得新的间隔,并触发更新,在onDraw里对canvas扩大就行。

三指移动

这个也相似双指扩大,在ACTION_POINTER_DOWN中取得三个点时的中心,在ACTION_MOVE中更新中心,触发移动,在onDraw里对canvas进行translate。(ps. 我发现规模限制没有用,不知道怎么回事)

四指以上重置

原本我认为重置会很复杂的,后边发现每次canvas都会复位,所以只要在onDraw中判别下是否是STATE_RESETTING事情,是的话把累加值悉数置零就能够了。

多种状况累加

在多种状况累加的时分,状况这种东西就有用了。在onDraw中,当归于自己状况的时分累加值,当不是自己状况时,按旧的累加值处理canvas就行了。