Android 自定义View制作彩虹调色圆环
- 前言
- 绘制彩虹圆环
-
- 原理
- 代码
- 获取触摸点颜色
-
- 原理分析
- 代码
- 完整代码
-
- 使用方法
- 完事
前言
在上一篇文章中,我做了个直接使用图片制作的彩虹调色圆环
Android 利用图片取色法巧妙制作彩虹调色圆环
这一篇就来介绍一下怎么自定义控件来达到这种效果
绘制彩虹圆环
原理
- 首先我们根据获取到的空间宽高,确定一个绘制的矩形空间
,矩形的左上角为坐标(0,0)点;RectF
-
的中心点就是我们画圆的中心点,半径就是RectF
的宽的一半减去描边宽度的一半;RectF
- 利用
对画笔SweepGradient
进行着色,把想要的颜色列表放进去;Paint
- 利用
方法进行绘制圆环;canvas.drawOval
关于SweepGradient;
关于Canvas;
代码
- 确定图形绘制的矩形框,把用户给的矩形大小往里缩描边宽度的一半就是了;
/** 绘制图形的矩形框 */
private var mRectF: RectF? = null
/** 控件大小改变 */
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 计算矩形框的偏移量
val offset = (mPaint?.strokeWidth?: 0f) / 2
mRectF = RectF(offset, offset, w.toFloat() - offset, h.toFloat() - offset)
}
-
搞定画笔样式,正常来说需要把圆环的颜色列表和圆环宽度作为可设置属性提供给用户,但我这里只作为最简单的基本逻辑,就不展开说了
有需要的朋友可以参考Android 自定义控件基本教程之自定义一个圆形ImageView可设置边框和阴影(demo带详细注释);
/** 绘制圆环的画笔 */
private var mPaint: Paint? = null
/** 圆环颜色列表 */
private val colorList = listOf(
0xFFFF0000.toInt(),
0xFFFF00FF.toInt(),
0xFF0000FF.toInt(),
0xFF00FFFF.toInt(),
0xFF00FF00.toInt(),
0xFFFFFF00.toInt(),
0xFFFF0000.toInt()
)
init {
// 初始化画笔
val paint = Paint().apply {
// 抗锯齿
isAntiAlias = true
// 设置仅描边
style = Paint.Style.STROKE
// 设置描边的宽度
strokeWidth = 100f
}
mPaint = paint
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 计算矩形框的偏移量
...
// 设置着色器
mPaint?.shader = SweepGradient(mRectF!!.centerX(), mRectF!!.centerY(), colorList.toIntArray(), null)
}
- 绘制圆环;
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 绘制圆环
if (mRectF != null && mPaint != null) {
canvas?.drawOval(mRectF!!, mPaint!!)
}
}
- 这时候可以把自定义控件添加到布局里,并编译一次项目,就能看到预览效果了;
获取触摸点颜色
只是看到圆环当然是不够的,更重要的是能够通过触摸控件得到触摸点的颜色;
原理分析
- 监听
事件,得到触摸点(x,y);onTouchEvent
- 判断触摸点(x,y)是否在圆环内,通过触摸点(x,y)到中心点(x0,y0)的距离是否在 控件宽度的一半减去描边宽度 到 控件宽度的一半的范围内来确定;
- 假设存在触摸点如图;
- 使用数学方法
能得到假设点到0度位置的周长angle(这时候的圆周长为2*PI)Math.atan2
- 将angle除以圆周长2*PI就能得到这个假设点所在圆的百分比位置;
- 同时,我们的圆环其实是分成了多份颜色的,每个颜色占据的位置是均匀的,所以我们也能换算出触摸点所在位置的开始颜色和结束颜色,例如从0度开始是红色,顺时针渐变到紫色,那么在这部分位置就是红色为开始颜色,紫色为结束颜色;
- 再根据偏移的位置计算开始颜色到结束颜色的比例,得到触摸点颜色;
代码
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) {
return super.onTouchEvent(event)
}
if (isTouchInOval(event.x, event.y)) {
val color = getTouchColor(event.x, event.y)
mColorListener?.change(color)
} else {
Log.d("测试", "点击不在范围内")
}
return true
}
/**
* 判断触摸位置是否在圆环上
* @param x Float
* @param y Float
* @return Boolean
*/
private fun isTouchInOval(x: Float, y: Float): Boolean {
if (mRectF == null) return false
val distance = sqrt(((mRectF!!.centerX() - x).toDouble().pow(2.0) + (mRectF!!.centerY() - y).toDouble().pow(2.0)))
return distance < mRectF!!.centerX() && distance > mRectF!!.centerX() - mPaint!!.strokeWidth
}
/**
* 获取触摸位置的颜色,根据与中心点的夹角计算
* @param x Float
* @param y Float
* @return Int
*/
private fun getTouchColor(x: Float, y: Float): Int {
if (mRectF == null) return 0
// 计算幅角,也就是夹角对应的周长,取值范围是-PI到PI
val angle = atan2((y - mRectF!!.centerY()).toDouble(), (x - mRectF!!.centerX()).toDouble())
// 计算幅角周长占据总周长的百分比
var unit = (angle / (2 * Math.PI)).toFloat()
// 百分比为负的,就+1,取反方向的百分比
if (unit < 0) unit += 1
// 根据颜色范围计算颜色
if (unit <= 0) return colorList[0]
if (unit >= 1) return colorList[colorList.size -1]
// 计算百分比落在哪个颜色区间
val pos = unit * (colorList.size -1)
// 获得整数部分为第几个颜色
val index = pos.toInt()
// 获得小数部分为颜色再往前渐变的百分比
val pre = pos - index
// 开始颜色
val c0 = colorList[index]
// 结束颜色
val c1 = colorList[index +1]
// 计算透明度
val a = ave(Color.alpha(c0), Color.alpha(c1), pre)
// 计算红色
val r = ave(Color.red(c0), Color.red(c1), pre)
// 计算绿色
val g = ave(Color.green(c0), Color.green(c1), pre)
// 计算蓝色
val b = ave(Color.blue(c0), Color.blue(c1), pre)
// 整合
return Color.argb(a, r, g, b)
}
/**
* 计算取值,start + p * (end - start)
* @param start Int 开始值
* @param end Int 结束值
* @param position Float 开始值到结束值之间的百分比
* @return Int 计算结果
*/
private fun ave(start: Int, end: Int, position: Float) : Int {
return (start + position * (end - start)).toInt()
}
完整代码
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.atan2
import kotlin.math.pow
import kotlin.math.sqrt
/**
* @author D10NG
* @date on 2020/10/10 2:54 PM
*/
class SweepGradientView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {
/** 绘制图形的矩形框 */
private var mRectF: RectF? = null
/** 绘制圆环的画笔 */
private var mPaint: Paint? = null
/** 圆环颜色列表 */
private val colorList = listOf(
0xFFFF0000.toInt(),
0xFFFF00FF.toInt(),
0xFF0000FF.toInt(),
0xFF00FFFF.toInt(),
0xFF00FF00.toInt(),
0xFFFFFF00.toInt(),
0xFFFF0000.toInt()
)
/** 监听器 */
private var mColorListener: OnColorChangeListener? = null
init {
// 初始化画笔
val paint = Paint().apply {
// 抗锯齿
isAntiAlias = true
// 设置仅描边
style = Paint.Style.STROKE
// 设置描边的宽度
strokeWidth = 100f
}
mPaint = paint
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 计算矩形框的偏移量
val offset = (mPaint?.strokeWidth?: 0f) / 2
mRectF = RectF(offset, offset, w.toFloat() - offset, h.toFloat() - offset)
// 设置着色器
mPaint?.shader = SweepGradient(mRectF!!.centerX(), mRectF!!.centerY(), colorList.toIntArray(), null)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 绘制圆环
if (mRectF != null && mPaint != null) {
canvas?.drawOval(mRectF!!, mPaint!!)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) {
return super.onTouchEvent(event)
}
if (isTouchInOval(event.x, event.y)) {
val color = getTouchColor(event.x, event.y)
mColorListener?.change(color)
} else {
Log.d("测试", "点击不在范围内")
}
return true
}
/**
* 判断触摸位置是否在圆环上
* @param x Float
* @param y Float
* @return Boolean
*/
private fun isTouchInOval(x: Float, y: Float): Boolean {
if (mRectF == null) return false
val distance = sqrt(((mRectF!!.centerX() - x).toDouble().pow(2.0) + (mRectF!!.centerY() - y).toDouble().pow(2.0)))
return distance < mRectF!!.centerX() && distance > mRectF!!.centerX() - mPaint!!.strokeWidth
}
/**
* 获取触摸位置的颜色,根据与中心点的夹角计算
* @param x Float
* @param y Float
* @return Int
*/
private fun getTouchColor(x: Float, y: Float): Int {
if (mRectF == null) return 0
// 计算幅角,也就是夹角对应的周长,取值范围是-PI到PI
val angle = atan2((y - mRectF!!.centerY()).toDouble(), (x - mRectF!!.centerX()).toDouble())
// 计算幅角周长占据总周长的百分比
var unit = (angle / (2 * Math.PI)).toFloat()
// 百分比为负的,就+1,取反方向的百分比
if (unit < 0) unit += 1
// 根据颜色范围计算颜色
if (unit <= 0) return colorList[0]
if (unit >= 1) return colorList[colorList.size -1]
// 计算百分比落在哪个颜色区间
val pos = unit * (colorList.size -1)
// 获得整数部分为第几个颜色
val index = pos.toInt()
// 获得小数部分为颜色再往前渐变的百分比
val pre = pos - index
// 开始颜色
val c0 = colorList[index]
// 结束颜色
val c1 = colorList[index +1]
// 计算透明度
val a = ave(Color.alpha(c0), Color.alpha(c1), pre)
// 计算红色
val r = ave(Color.red(c0), Color.red(c1), pre)
// 计算绿色
val g = ave(Color.green(c0), Color.green(c1), pre)
// 计算蓝色
val b = ave(Color.blue(c0), Color.blue(c1), pre)
// 整合
return Color.argb(a, r, g, b)
}
/**
* 计算取值,start + p * (end - start)
* @param start Int 开始值
* @param end Int 结束值
* @param position Float 开始值到结束值之间的百分比
* @return Int 计算结果
*/
private fun ave(start: Int, end: Int, position: Float) : Int {
return (start + position * (end - start)).toInt()
}
/**
* 设置监听器
* @param listener OnColorChangeListener
*/
fun setColorChangeListener(listener: OnColorChangeListener) {
this.mColorListener = listener
}
/**
* 颜色改变监听器
*/
interface OnColorChangeListener{
fun change(color: Int)
}
}
使用方法
布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.rd.yunmqtt.SweepGradientView
android:id="@+id/sg_view"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="颜色"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
活动
class TestActivity : AppCompatActivity() {
private lateinit var binding: ActivityTestBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_test)
binding.sgView.setColorChangeListener(object : SweepGradientView.OnColorChangeListener{
override fun change(color: Int) {
binding.textView.setTextColor(color)
}
})
}
}