前言
这段时间工作中遇到了这样的需求 :
- 需要选中一个色值出现个拖动条,且该拖动条从黑色往该色值渐变,再由该色值从白色渐变
- 该拖动条上的指示球要位移到该色值在拖动条的位置
- 拖动指示球可以进行实时获取该位置上的色值
具体效果如下图所示
分析问题
这样看来,这个小需求是不是很简单,然而就是这样的一个比较简单的需求,我花了挺久的时间才弄懂,最终还是在大佬的帮助下写出来的,我分析了下,主要有以下难点(我认为的)
-
如何让传入的色值在该拖动条中显示正确的位置呢? 因为考虑到传入不同的色值,在渐变拖动条中的位置是不一样的,比较极端的就是白色和黑色,这样指示球会显示在两端
-
手指滑动指示球的时候,如何记录获取下当前位置的色值呢?
以上,带着问题和菜鸟本人一起看看整体的实现思路吧
实现思路
对于这个需求,显而易见,需要我们自定义一个view来实现相应效果
获取指示球当前位置
- 这里就是我上面所考虑的第一个难点了,我们要获取当前色值在当前拖动条的位置,直接拿到该色值的宽度是不太现实的,我开始就是这么考虑的但是踌躇了很久也没发完全实现;在求助大佬后,可以得到一个简单的实现方案,将这个指示球先看成一个点,两边分别是0-1,定义下当前进度变量mProgress,经过思考,可以使用HSL算法来进行解决,考虑到两边的色值是不会变,可变的只有中间传入的色值,因此我们可以定义
companion object {
const val DEF_WHITE = Color.WHITE
const val DEF_BLACK = Color.BLACK
}
mColorSeeds = if (currentColor == DEF_WHITE || currentColor == DEF_BLACK) {
intArrayOf(
DEF_BLACK,
DEF_WHITE
)
} else {
intArrayOf(
DEF_BLACK,
currentColor,
DEF_WHITE,
)
}
复制代码
需要注意的是,如果传入的颜色是黑色或者白色,那么整个拖动条就是由黑色到白色渐变
- 接着创建一个size为3的float数组,这个数组是干什么用的呢?,相信各位小伙伴已经看出来了,这正是存放HSL的值,通过颜色转化的计算(重点),我们就可以拿到当前色值在0-1的变化值
val index = FloatArray(3)
ColorUtils.colorParse(currentColor, index)
mProgress = index[2]
if (BuildConfig.LOG_ENABLE) {
Log.d(TAG, "setColor() current color: ${currentColor}, progress:${mProgress}")
}
resetBgColor()
invalidate()
复制代码
颜色RGB进行HSL计算
- 首先,将当前传入的色值int,进行rgb转化
public static void colorParse(int currentColor, float[] index) {
//将当前传入的色值进行rgb转化
mix(Color.red(currentColor),Color.green(currentColor),Color.blue(currentColor),index);
}
复制代码
- 由于基本所有颜色都是在rgb的0-255范围内,但是由于这个范围不太好操作,我们可以用0.0-1.0范围描述0-255范围,其中0.0表示0(0x00),1.0表示255(0xFF),这样是不是直观许多
float f;
float f2;
//将传入三个rgb转化的值分别都除以255,
float f3 = ((float) color1) / 255f;
float f4 = ((float) color2) / 255f;
float f5 = ((float) color3) / 255f;
float max = Math.max(f3,Math.max(f4,f5)); //找到f3,f4,f5的最大值
float min = Math.min(f3, Math.max(f4,f5)); //找到f3,f4,f5的最大值
复制代码
- 主要使用了HSL公式色度/饱和度/明度,简单来说就是设 (r,g,b)分别是一个颜色的红、绿和蓝坐标,它们的值是在 0 到 1 之间的实数,设 max 等价于 r, g 和 b 中的最大者。设 min 等于这些值中的最小者。要找到在 HSL 空间中的 (h, s, l) 值,这里的 h ∈ [0, 360)是角度的色相角,而 s, l ∈ [0,1] 是饱和度和亮度通过查阅资料,可以简单说明下它们之间的定义
- H表示的是颜色范围,取值范围是0°到360°的圆心角,每个角度都可以代表一种颜色
- S表示的是色彩的饱和度,用0%到100%的值表示了相同色相、明度下色彩纯度的变化,简单来说呢,就是颜色里面的灰色越少,它的色彩越鲜艳
- L表示的是色彩的明度,这个就是控制色彩明亮的变化,同样使用了0%到100%的范围变化,数值越大,色彩越暗,越接近于黑色,色彩越亮,越接近与白色,看到这里豁然开朗,我所需要的值不就是这个色彩明度么,这样第一个难点不就解决了嘛
float f6 = max - min; //之间的范围
// 计算L(明度):L=(max(R,G,B) + min(R,G,B))/2
float f7 = (max + min) / 2.0f; //中间值
if(max == min) { //相当于最大的和最小的是一样的,所以表示范围就是只有0
start = 0.0f;
end = 0.0f;
} else {
//计算明度
end = max == f3 ? ((f4 - f5) / f6) % 6.0f : max == f4 ? ((f5 - f3) / f6) + 2.0f : 4.0f + ((f3 - f4) / f6);
//计算饱和度
start = f6 / (1.0f - Math.abs((2.0f * f7) - 1.0f));
}
float f8 = (end * 60.0f) %360.f;
if (f8 <0.0f) {
f8 += 360.0f;
}
//H值
index[0] = isColorProgress(f8,0.0f,360.f);
//S值
index[1] = isColorProgress(start,0.0f,1.0f);
//L值
index[2] = isColorProgress(end,0.0f,1.0f);
private static float isColorProgress(float f, float f2, float f3) {
return f < f2 ? f2: f>f3 ? f3 : f;
}
复制代码
- 通过hsl数值计算,我们可以拿到当前rgb色值的HSL值,然后需求需要的是色彩明亮的变化,也就是index[2],至此,通过HSL计算我们就拿到了该色值在0-1的明亮变化值
绘制相关图形
接着画一个指示球和圆角矩形,基本没什么难度,直接上代码
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.also { c ->
c.save()
mRectPaint.shader = mBgColorGradient
c.drawRoundRect(mBgRectf, mBgRectf.height() / 2f, mBgRectf.height() / 2f, mRectPaint)
c.drawCircle(
(mBgRectf.width() - mThumbRadius).coerceAtMost(
mThumbRadius.coerceAtLeast(mProgress * mBgRectf.width())
), mThumbRadius, mThumbRadius, mThumbBorderPaint
)
c.restore()
}
}
复制代码
此时可以拿到之前穿过来的进度值,那么它的位置就是拿到的当前进度mProgress x 圆角矩形的宽度
onSizeChange
当view的第一次分配大小或以后大小改变时的产生的时候,这时候判断圆角矩形宽高,设置指示器的radius大小以及设置线性渐变,将当前的colorSeeds数组传入进来
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mBgRectf.set(0f, 0f, width.toFloat(), height.toFloat())
mBgColorGradient =
LinearGradient(0f, 0f, w.toFloat(), 0f, mColorSeeds, null, Shader.TileMode.CLAMP)
mThumbRadius = mBgRectf.height() / 2f - mStokeWidth / 2
resetBackground(mInitColor)
}
复制代码
触摸touch事件逻辑
这里就需要考虑第二个难点了,如何在拖动的时候获取当前色值参数呢,很显然就是在手指移动的时候进行监听,所以我们需要重载onTouchEvent方法
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
event.x.let {
mProgress = it / width
invalidate()
}
mColorChangeListener?.also { callback ->
getColor().also {
callback.onColorChangeListener(
it,
ColorHelper.formatColor(it)
)
}
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
parent.requestDisallowInterceptTouchEvent(false)
}
}
return true
}
复制代码
-
当手指按下的时候,设置当前requestDisallowInterceptTouchEvent去阻止父view拦截点击事件
-
当手指松开的时候,或者没有触摸的时候,设置当前requestDisallowInterceptTouchEvent不阻止父view拦截点击事件
-
当手指移动的时候,将当前x除以宽度的值赋予给mProgress变量,获取到最新的进度值,传入颜色计算工具类进行转化,从而获取当前进度颜色
@ColorInt
private fun getColor(): Int {
return ColorUtils.getColor(mInitColor, mProgress)
}
复制代码
- 获取到当前的颜色色值int,这样也是通过HSL转化,当前的色值转化成HSL值,拿到L值活,将当前的HSL->RGB,HSL和上述实现方式一致
public static final int getColor(int i, float f) {
float[] fArr = new float[3];
//拿到当前hsl值
ColorUtils.colorParse(i,fArr);
//拿到当前的L值,赋值给传入的mProgress
fArr[2] = f;
return ColorUtils.hslToRgb(fArr);
}
复制代码
- 下面来看看HSL是如何转化成RGB的
public static int hslToRgb(float[] fArr) {
int i;
int i2;
int i3;
//当前的H值
float f = fArr[0];
//当前的S值
float f2 = fArr[1];
//当前的L值
float f3 = fArr[2];
float abs = (1.0f - Math.abs((f3 * 2.0f) - 1.0f)) * f2;
float f4 = f3 - (0.5f * abs);
float abs2 = (1.0f - Math.abs(((f / 60.0f) % 2.0f) - 1.0f)) * abs;
switch (((int) f) / 60) {
case 0:
i3 = Math.round((abs + f4) * 255.0f);
i2 = Math.round((abs2 + f4) * 255.0f);
i = Math.round(f4 * 255.0f);
break;
case 1:
i3 = Math.round((abs2 + f4) * 255.0f);
i2 = Math.round((abs + f4) * 255.0f);
i = Math.round(f4 * 255.0f);
break;
case 2:
i3 = Math.round(f4 * 255.0f);
i2 = Math.round((abs + f4) * 255.0f);
i = Math.round((abs2 + f4) * 255.0f);
break;
case 3:
i3 = Math.round(f4 * 255.0f);
i2 = Math.round((abs2 + f4) * 255.0f);
i = Math.round((abs + f4) * 255.0f);
break;
case 4:
i3 = Math.round((abs2 + f4) * 255.0f);
i2 = Math.round(f4 * 255.0f);
i = Math.round((abs + f4) * 255.0f);
break;
case 5:
case 6:
i3 = Math.round((abs + f4) * 255.0f);
i2 = Math.round(f4 * 255.0f);
i = Math.round((abs2 + f4) * 255.0f);
break;
default:
i = 0;
i3 = 0;
i2 = 0;
break;
}
return Color.rgb(colorSet(i3, 0, 255), colorSet(i2, 0, 255), colorSet(i, 0, 255));
}
复制代码
- 对于H值,它就是0°-360°进行范围变化的, H值分成0~6区域。RGB颜色空间是一个立方体而HSL颜色空间是两个六角形锥体,其中的 H(色调)是RGB立方体的主对角线。因此,RGB立方体的顶点:红、黄、绿、青、蓝和 品红就成为HSL六角形的顶点,而数值0~6就告诉我们H(色调)在哪个部分
- 既然可以拿到HSL值数值,那么我就可以通过它来拿到RGB数值,详细的可以了解下HSL公式计算和HSL->RGB的互相转化,这里就不过多解释了
- 传入当前传入的色值和进度,通过上述计算我们就可以拿到当前位置的rgb色值
ColorUtils.getColor(mInitColor, mProgress)
复制代码
提供色值变化的接口,供外部调用
接着调用色值变化的接口,将当前的色值传入进来,这里就不多说,主要是将当前变化的色值传出去,给外部调用,直接上代码
mColorChangeListener?.also { callback ->
getColor().also {
callback.onColorChangeListener(
it,
ColorHelper.formatColor(it)
)
}
}
复制代码
fun setOnColorChangeListener(onColorChangeListener: OnColorChangeListener) {
this.mColorChangeListener = onColorChangeListener
}
interface OnColorChangeListener {
fun onColorChangeListener(@ColorInt color: Int, colorHex: String)
}
复制代码
- 可以看下最终的效果如下
结语
以上就是整个颜色选中自定义view的绘制,主要还是对于颜色色值转化算法的理解,我是Android菜鸟级程序员 欢迎各位大佬鞭挞蹂躏,持续学习技术ing....
the end