自定义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();

重绘空心小圆。

源码

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

源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  • 系列文章之 Android中自定义View(一)系列文章之 Android中自定义View(二)系列文章之 And...
    YoungerDev阅读 2,152评论 0 4
  • 通过之前的详细分析,我们知道:在measure中测量了View的大小,在layout阶段确定了View的位置。 完...
    SnowDragonYY阅读 914评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 今日重阳,泡茶写词。天与秋光,红叶待赏。 金黄饱满的柿子挂满枝头 孩子们翘起了小脚儿 田间忙收的父母 满脸堆满了丰...
    赵小建阅读 144评论 0 0