本文内容为从0-1开发一个颜色选择器,先上效果图
相关源码文件,将会在文末附上。
效果如图所示。本文将讲述如何自定义一个这样的控件,代码在文末。
思考
(一)实现方式
(二)传递给业务层的数据格式
(三)实现细节
上面三个点,是需要我们去思考的。
(一),这里选择的实现方法,推荐是自定义view,因为这种布局,一般普通的控件,是不支持的。
(二)对于业务层来说,应该做的是,传递颜色数据进入我们的控件,然后控件选择了颜色后,把颜色数据原封不动的传回给业务层,这里选择了String格式作为色值传递,如“#ffffff”。
(三)实现中,使用canvas如何正确绘制圆弧,如何计算真实的角度,象限区间的数据转换,以及坐标数据的转换,点击事件的坐标计算,都是值得思考的问题。
好了,思考完以后,就开始干活。
实现过程
#####(一)定义类
做类似的控件,都是需要有一个大纲思维,就是我这个控件库,给外部暴露了什么方法,我内部又需要如何处理外部传入的数据。所以,要先定义好抽象方法:
(1)控件抽象类:
interface LibColorCirclePickerApi {
fun setColor(color: Array<Array<String>>)
fun setLibColorCirclePickerListener(listener: LibColorCirclePickerListener)
fun removeLibColorCirclePickerListener()
}
(2)外部监听类:
interface LibColorCirclePickerListener {
fun selInfo(info: LibColorSelInfo)
}
定义好这些方法,就可以通过继承,具体实现了。
#####(二)自定义view实现绘制色值
这里涉及到canvas绘制的知识,请提前了解。
而如图所示,绘制分为两个层级。一个就是中心圆,另外一个就是色块。
在绘制之前,需要确定以下的变量,方便后续的绘制:
(一)绘制的中心坐标点,x,y点。这里取x/2,y/2。
(二)绘制的半径,这里取最短边的一般作为绘制的半径。
(三)绘制中心圆的半径,通过一定的比率和(二)相乘,得出中心圆的半径。
(四)除中心圆外,剩余的半径长度,这里后续会做平均划分,计算除每层色块的半径长度。
(五)初始化相关中间变量值(如色块坐标集合)
初始相关变量后后,就开始绘制:
绘制中心圆
对于中心圆,这里直接调用canvas的画圆圈方法就行,不再啰嗦
绘制色块
对于色块,首先要明确,你的色块有多少层,这里就决定了每层色块的半径长度。这里的多少层,是通过外部传入的二维数组决定的。对于传入的二维数组,一维决定层级,二维就是一维下的色块数组。
通过上述的了解,就可以通过 “剩余半径” 除以 “色块层数” 即可得出 “每层色块半径长度”。
再通过 360 除以 “每层色块数量”,即可得出每个色块所占的角度。
最后通过设置画笔的strokeWidth,就可以进行绘制。
最后,把绘制过程中产生的“半径范围值”,“角度范围值”,“色值”通过中间变量保存,供以后续的触摸事件调用。
ps:
这里有个实现细节,就是关于Paint的strokeWidth。实际是基于绘制坐标两端扩展的。例如,绘制一条直线,那就会在Y坐标两端拓展,如 (x,y)(0,10),strokeWidth是10,那么直角边宽度就会基于(0,5)-(0,15)中间。
所以,设置strokeWidth后绘制,需要预留绘制实现中的位置。
触摸事件
对于触摸事件,需要在onTouchEvent中进行处理,父类方法重新如下:
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP -> {
val currentX = event.x
val currentY = event.y
dealWithUpEvent(currentX, currentY)
}
MotionEvent.ACTION_CANCEL -> {
}
}
return true
}
这里通过return true,把所有事件都拦截到本view进行处理了。
设想一下,有了中心坐标,和点击时候的x,y坐标。是不是可以得出一个“象限区间”的逻辑?然后再通过数学公式tan方法,即可得出对应的角度。
角度有了,象限有了,就可以换算出最后的真正点击角度,还有点击坐标点的半径。核心代码如下:
/**
* 处理手势抬起事件
* */
private fun dealWithUpEvent(currentX: Float, currentY: Float) {
//判断象限
val trainX = (currentX - mCenterX)
val trainY = (mCenterY - currentY)
//求半径长度
val x = abs(abs(currentX) - abs(mCenterX)).toDouble()
val y = abs(abs(mCenterY) - abs(currentY)).toDouble()
//半径
val trainRadius = sqrt((x.pow(2.0) + y.pow(2.0)))
//角度
val angle = atan(abs(x) / abs(y)) * 180 / PI
//计算后,再根据象限,转换最终的角度
var trainAngle = 0f
if (trainX <= 0 && trainY >= 0) {
trainAngle = (90 - angle + 270).toFloat()
} else if (trainX >= 0 && trainY >= 0) {
trainAngle = angle.toFloat()
} else if (trainX <= 0 && trainY <= 0) {
trainAngle = (angle + 180).toFloat()
} else if (trainX >= 0 && trainY <= 0) {
trainAngle = (90 - angle + 90).toFloat()
} else {
return
}
// Log.d("dealWithUpEvent", "angle $angle + 转换角度 $trainAngle 半径: $trainRadius")
//通过象限,角度,半径,三个条件,确定具体的位置
val filterList: MutableList<LibColorInnerPosInfo> = ArrayList()
this.mColorPosInfo.filter {
it.startAngle <= trainAngle && it.endAngle >= trainAngle
}.filter {
it.startRadius >= trainRadius && it.endRadius <= trainRadius
}.filter {
!it.color.isNullOrBlank()
}.toCollection(filterList)
if (filterList.size != 1) {
return
}
mListener?.selInfo(LibColorSelInfo().apply {
this.colorStr = filterList.first().color
})
}
至此,全部流程已经说完了,还有不懂的地方,请下载源码了解:
源码
提取码: naf2
that’s all--------------------------------------------------------------