前语
上一篇做了一个简略的六边形评分控件,首要对paint的api了解了一下,原本还想对六边形控件加入扩大旋转的功用,可是paint的api内容够多了,就算了。今日把上一篇的扩大旋转功用加到了这篇文章的扇形图里边,对安卓的多点触控学习了一下。
需求
这儿便是用传入的数据画一个扇形图,可是画完扇形图后,能够支撑多点触控,运用一只手指滑动时触发旋转,两只手指时触发缩放,三指手指时触发移动,四只手指以上时触发重置。核心思想如下:
- 1、外面传入数据,完成扇形图展现
- 2、扇形图能够单指旋转、二指扩大、三指移动,四指以上一起按下进行复位
- 3、旋转、扩大、平移效果能够叠加
效果图
这儿效果图并不是很好,由于我这手机是淘宝买的屏幕自己换的,会断触。并且我发现多个手指这样合作功用上也是有抵触的,多个手指逐步脱离屏幕时会到不同的状况,任何微小的移动,都会触发MOVE事情,从而造成不正常的改动,不知道其他有这种功用的控件怎么处理的,等有时间得研讨研讨。
代码
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就行了。