自定义View之案列篇(四):颜色选择器

2016年的第四篇案例,比以往来得更晚一些...

博客的更新就像 2012 年的第一场雪一样,比以往来得更晚一些…

博主这段时间实在太忙了,本篇案例一直没有更新,实在对不住大家…

案列篇系列包含了自定义 View 许多的知识点,希望大家能够知其然知其所以然,灵活应用,写出属于自己心动的控件。

下面来看看今天登场的是:

ColorPicker(颜色选择器)

大家一定还记得 PhotoShop 中的颜色面板,It’s very fashion . 曾经几时,苦苦的思索它的实现过程 …

先来看看最终的效果图:

color

具有以下效果:

  • 空心小圆随着手指的移动而移动

  • 随着手指的移动更改背景颜色

涉及到的知识点:

  • Color.HSVToColor

  • Shader

  • interface

接下来就对涉及到的知识点以及效果进行逐一的讲解。

Color.HSVToColor

颜色是由 int 型的数表示,由 4 个字节组成,分别是 A R G B,这个 int 型的值是确定的,透明度的值只能存在 A 这个字节上,不能存在颜色的字节上。存储的方式为 (alpha << 24) | (red << 16) | (green << 8) | blue 每一部分的取值范围都是 0-255 ,0 表示没有,255 表示填满了。不透明的黑色的值是 0xff000000,不透明的白色的值是 0xffffffff

方法预览:

    public static int HSVToColor(@Size(3) float hsv[]) {
        return HSVToColor(0xFF, hsv);
    }

把 HSV 的内容转化成 color,其中 alpha 设置成 0xff,参数 hsv 有三个成员,hsv[0] 的范围是 [0,360) 表示色彩,hsv[1] 范围 [0,1] 表示饱和度,hsv[2] 范围 [0,1] 表示值,如果它们的值超出范围,那么它们会被截断成范围内的值。

相关链接 RGB to HSV color conversion

文字的描述是比较抽象的,下面来看看一个例子:

color

  • hsv[1] (饱和度)hsv[2] (值) 不变的情况下,hsv[0] 逐渐增大,圆的色彩也在不断的变化

  • hsv[0] (色彩)hsv[2] (值) 不变的情况下,hsv[1] 逐渐减小,圆的饱和度也随着减小 (效果类似透明度的变化)

  • hsv[0] (色彩)hsv[1] (饱和度) 不变的情况下,hsv[2] 逐渐减小,圆的值也随着减小 (逐渐转变成黑色)

看看绘制 onDraw 的方法:

canvas.drawCircle(getWidth()/2, getHeight()/2, 200, colorWheelPaint);

圆心设置为控件的中心点,半径为 200px 绘制圆。

监听 SeekBar 的进度改变:

    @Override
    public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
        switch (seekBar.getId()) {
            case R.id.sb_color:
                mColorPicker.setHSVColor(i);
                break;
        }
    }

动态设置色彩。接着看看 setHSVColor 方法:

重绘:

    /**
     * @param color 0~360
     */
    public void setHSVColor(int color) {
        colorHSV[0] = color;
        colorWheelPaint.setColor(Color.HSVToColor(colorHSV));
        postInvalidate();
    }

源码在文章的结尾处。

Shader

Shader 类专门用来渲染图像以及一些几何图形。Shader 类与是一个空类,它的功能的实现,主要是靠它的派生类来实现的。继承关系如下:

color

Shader 类包括了 5 个直接子类:

  • BitmapShader 用于图像渲染

  • LinearGradient 用于线性渲染

  • RadialGradient 用于环形渲染 (放射状)

  • SweepGradient 用于梯度渲染(扫描状)

  • ComposeShader 用于混合渲染

这里主要讲解后三种渲染,如果对前面两种渲染感兴趣请链接:

图像渲染(Shader)

RadialGradient

RadialGradient 放射渐变,即它会向一个放射源一样,向外放射。

构造函数:

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色渐变
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)
1、 两色渐变构造函数使用实例

下面我们来看一下两色渐变构造函数的使用方法:

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)

两色渐变的构造函数的各项参数意义如下:

  • centerX:渐变中心点X坐标

  • centerY:渐变中心点Y坐标

  • radius:渐变半径

  • centerColor:渐变的起始颜色,即渐变中心点的颜色,取值类型必须是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不会
    显示出颜色。

  • edgeColor:渐变结束时的颜色,即渐变圆边缘的颜色,同样,取值类型必须是八位的0xAARRGGBB色值!

  • TileMode:用于指定当控件区域大于指定的渐变区域时,空白区域的颜色填充方式。

其中 TileMode 的取值有:

  • TileMode.CLAMP 用边缘色彩填充多余空间
  • TileMode.REPEAT 重复原图像来填充多余空间
  • TileMode.MIRROR 重复使用镜像模式的图像来填充多余空间

来看个简单的例子:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mRadialGradient = new RadialGradient(getWidth() / 2, getHeight() / 2, 200, 0xffff0000, 0xffffff00, 
        Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, 200, mPaint);
    }

渐变的中心点为空间的中心点,渐变半径为 200px,渐变的起始颜色为红色,渐变结束的颜色为黄色,填充方式为重复原图填充。注意我们画的圆的大小与所构造的放射渐变的大小是一样的,所以不存在空白区域的填充问题。

效果图如下:

color

2、多色渐变构造函数使用实例

多色渐变的构造函数如下:

RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

这里与两色渐变不同的是两个参数:

  • int[] colors:表示所需要的渐变颜色数组,长度大于等于2。

  • float[] stops:表示每个渐变颜色所在的位置百分点,取值 0-1,数量必须与 colors 数组保持一致,不然直接 crash ,一般第一个数值取0,最后一个数值取 1;如果第一个数值和最后一个数值并没有取 0 和 1,比如我们这里取一个位置数组:{0.2,0.5,0.8},起始点是 0.2 百分比位置,结束点是 0.8 百分比位置,而 0-0.2 百分比位置和 0.8-1.0 百分比的位置都是没有指定颜色的。而这些位置的颜色就是根据我们指定的 TileMode 空白区域填充模式来自行填充!!有时效果我们是不可控的。所以为了方便起见,建议大家 stops 数组的起始和终止数值设为 0 和 1。

多色渐变的例子:

        int[]   colors = new int[]{0xffff0000,0xff00ff00,0xff00ffff,0xff0000ff};
        float[] stops  = new float[]{0f,0.3f,0.6f,1f};

        mRadialGradient = new RadialGradient(getWidth() / 2, getHeight() / 2, 200, colors, stops, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, 200, mPaint);

构造了四色颜色的数值,以及对应的位置百分比。效果图如下:

color

参考链接RadialGradient与水波纹按钮效果

SweepGradient

梯度渲染,扫描渐变,类似卫星扫描的效果。

构造函数预览:

 //两色
 public SweepGradient(float cx, float cy, int color0, int color1) 

 //多色
 public SweepGradient(float cx, float cy,
                         int colors[], float positions[])
1、 两色渐变构造函数使用实例
 //两色
 public SweepGradient(float cx, float cy, int color0, int color1) 

SweepGradient 与 RadialGradient 类似,下面来看看他的各项参数:

  • cx 渐变中心点X坐标

  • cy 渐变中心点Y坐标

  • color0:扫描开始的颜色,即中心点和水平最右点的连线颜色,取值类型必须是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不会显示出颜色。

  • color1:扫描结束的颜色,即中心点和水平最左点的连线颜色,取值类型必须是八位的0xAARRGGBB色值!

扫描的角度为360度,color0 ,color1平分360度,color0 顺时针扫描了 (0-180),color1 顺时针扫描了(180-360)。由于扫描的半径可以无限大,所以这里没有填充方式的参数。

来看个简单的例子:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2, 0xffff0000, 0xffffff00);
        mPaint.setShader(mSweepGradient);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, 200, mPaint);
    }

各个参数的含义上面已经讲解了,来看看效果图:

color

2、 多色渐变构造函数使用实例

方法预览:

 //多色
 public SweepGradient(float cx, float cy,
                         int colors[], float positions[])

这里与两色渐变不同的是两个参数:

  • int[] colors:表示所需要的渐变颜色数组,长度大于等于2。

  • float[] positions:表示每个渐变颜色所扫描的相对位置,取值 0-1,数量必须与 colors 数组保持一致,不然直接 crash ,一般第一个数值取0,最后一个数值取1;如果第一个数值和最后一个数值并没有取 0 和 1,绘图可能会产生意想不到的结果。可以为 null,渐变颜色间隔均匀。

修改一下上面的例子:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int [] colors=new int[]{0xffff0000, 0xffffff00,0xffff00ff};

        mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2, colors, null);
        mPaint.setShader(mSweepGradient);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, 200, mPaint);
    }

效果图一栏:

color

注意:尽量避免在 onDraw 方法中新建对象,我这里主要是为了演示方便。

ComposeShader(组合渲染)

构造方法预览:

 public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

参数含义:

  • shaderA 目标渲染器

  • shaderB 源渲染器

  • mode 渲染器组合的模式

mode 具体参考 自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)

我们将放射渲染器以及扫描渲染器组合在一起:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int [] colors=new int[]{0xffff0000, 0xffffff00,0xffff00ff};

        SweepGradient sweepGradient = new SweepGradient(getWidth()/2, getHeight()/2, colors, null);
        RadialGradient radialGradient = new RadialGradient(getWidth()/2, getHeight()/2,
                radius, 0xFFFFFFFF, 0x00FFFFFF, Shader.TileMode.CLAMP);

        ComposeShader composeShader = new ComposeShader(sweepGradient, radialGradient, PorterDuff.Mode.SRC_OVER);

        mPaint.setShader(composeShader);

        canvas.drawCircle(getWidth()/2,getHeight()/2,200,mPaint);

    }

这里的参数我就不再细讲了,来看看效果图:

color

ColorPicker的具体实现

如果对自定义 View 大体流程还不是很熟悉的话。请链接 自定义View之绘图篇(一):基础图形的绘制 系列的文章。

onMeasure 方法略过 . . .

onSizeChanged 方法:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / 2;
        centerY = h / 2;
        radius = Math.min(centerX, centerY);
        //生成色轮
        createColorWheel();
    }

赋值中心点坐标,半径为宽,高一半的最小值。具体来看看 createColorWheel 方法:

    private void createColorWheel() {
        int colorCount = 12;
        int colorAngleStep = 360 / 12;
        int colors[] = new int[colorCount];
        float hsv[] = new float[]{0f, 1f, 1f};
        for (int i = 0; i < colors.length; i++) {
            hsv[0] = (i * colorAngleStep + 180) % 360;
            colors[i] = Color.HSVToColor(hsv);
        }

        SweepGradient sweepGradient = new SweepGradient(centerX, centerY, colors, null);
        RadialGradient radialGradient = new RadialGradient(centerX, centerY,
                radius, 0xFFFFFFFF, 0x00FFFFFF, Shader.TileMode.CLAMP);
        ComposeShader composeShader = new ComposeShader(sweepGradient, radialGradient, PorterDuff.Mode.SRC_OVER);

        colorWheelPaint.setShader(composeShader);
    }

主要是把色轮分成 12 等份,求出每份的色彩,并且每份的饱和度和值都为 1,然后生成大小为 12 间隔均匀的扫描渲染器;新建不透明到透明半径为 radius 的放射渲染器;通过扫描渲染器作为目标渲染器,放射渲染器作为源渲染器生成组合渲染器,并设置给 Paint 。接着进行绘制:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(centerX, centerY, 200, colorWheelPaint);
    }

绘制大小为 radius 的圆:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(centerX, centerY, radius, colorWheelPaint);
    }

效果图:

color

随着手指的移动更改背景颜色,需要重写 onTouchEvent 方法:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ViewParent parent = getParent();
        if (parent != null)
            parent.requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                int cx = x - centerX;
                int cy = y - centerY;
                double d = Math.sqrt(cx * cx + cy * cy);

                if (d <= radius) {
                    colorHSV[0] = (float) (Math.toDegrees(Math.atan2(cy, cx)) + 180f);
                    colorHSV[1] = Math.max(0f, Math.min(1f, (float) (d / radius)));
                    if (onSeekColorListener != null) {
                        touchCircleY = y;
                        touchCircleX = x;
                        onSeekColorListener.onSeekColorListener(getColor());
                        postInvalidate();
                    }
                }

                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

根据 X,Y 轴的偏移量,可以计算出当前触摸点与中心点连线与水平方向的角度:

Math.toDegrees(Math.atan2(cy, cx)

并把角度赋值给 HSV 数组的色彩值。

通过当前触摸点到中心点的距离/半径 获取到饱和度:

HSV 饱和度 = Math.max(0f, Math.min(1f, (float) (d / radius)))

通过赋值当前触摸点坐标,绘制触摸的空心小圆:

   touchCircleY = y;
   touchCircleX = x;

通过依赖倒转原则(接口),把获取到的颜色值公开:

onSeekColorListener.onSeekColorListener(getColor());

最后调用:

postInvalidate();

重绘空心小圆。

源码

如果本文有帮到你,记得加关注哦

源码地址

猜你喜欢

转载自blog.csdn.net/u012551350/article/details/53170891