实现可展示当前选中颜色进度的自定义view

1,436 阅读8分钟

前言


这段时间工作中遇到了这样的需求 :

  1. 需要选中一个色值出现个拖动条,且该拖动条从黑色往该色值渐变,再由该色值从白色渐变
  2. 该拖动条上的指示球要位移到该色值在拖动条的位置
  3. 拖动指示球可以进行实时获取该位置上的色值 具体效果如下图所示

dd486915-9cd8-4f9b-b72a-a4a841232016.png

分析问题

这样看来,这个小需求是不是很简单,然而就是这样的一个比较简单的需求,我花了挺久的时间才弄懂,最终还是在大佬的帮助下写出来的,我分析了下,主要有以下难点(我认为的)

  • 如何让传入的色值在该拖动条中显示正确的位置呢? 因为考虑到传入不同的色值,在渐变拖动条中的位置是不一样的,比较极端的就是白色和黑色,这样指示球会显示在两端

  • 手指滑动指示球的时候,如何记录获取下当前位置的色值呢?

以上,带着问题和菜鸟本人一起看看整体的实现思路吧

实现思路

对于这个需求,显而易见,需要我们自定义一个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计算

  1. 首先,将当前传入的色值int,进行rgb转化
    public static void colorParse(int currentColor, float[] index) {
        //将当前传入的色值进行rgb转化
        mix(Color.red(currentColor),Color.green(currentColor),Color.blue(currentColor),index);
    }
  1. 由于基本所有颜色都是在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的最大值
  1. 主要使用了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));
    }
  1. 对于H值,它就是0°-360°进行范围变化的, H值分成0~6区域。RGB颜色空间是一个立方体而HSL颜色空间是两个六角形锥体,其中的 H(色调)是RGB立方体的主对角线。因此,RGB立方体的顶点:红、黄、绿、青、蓝和 品红就成为HSL六角形的顶点,而数值0~6就告诉我们H(色调)在哪个部分
  2. 既然可以拿到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)
    }
  • 可以看下最终的效果如下

IMG_20220103_114027.jpg

结语

以上就是整个颜色选中自定义view的绘制,主要还是对于颜色色值转化算法的理解,我是Android菜鸟级程序员 欢迎各位大佬鞭挞蹂躏,持续学习技术ing....

the end