2016年的第四篇案例,比以往来得更晚一些...
博客的更新就像 2012 年的第一场雪一样,比以往来得更晚一些...
博主这段时间实在太忙了,本篇案例一直没有更新,实在对不住大家...
案列篇系列包含了自定义 View 许多的知识点,希望大家能够知其然知其所以然,灵活应用,写出属于自己心动的控件。
下面来看看今天登场的是:
ColorPicker(颜色选择器)
大家一定还记得 PhotoShop 中的颜色面板,It's very fashion . 曾经几时,苦苦的思索它的实现过程 . . .
先来看看最终的效果图:
具有以下效果:
空心小圆随着手指的移动而移动
随着手指的移动更改背景颜色
涉及到的知识点:
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
文字的描述是比较抽象的,下面来看看一个例子:
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 类与是一个空类,它的功能的实现,主要是靠它的派生类来实现的。继承关系如下:
Shader 类包括了 5 个直接子类:
BitmapShader 用于图像渲染
LinearGradient 用于线性渲染
RadialGradient 用于环形渲染 (放射状)
SweepGradient 用于梯度渲染(扫描状)
ComposeShader 用于混合渲染
这里主要讲解后三种渲染,如果对前面两种渲染感兴趣请链接:
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,渐变的起始颜色为红色,渐变结束的颜色为黄色,填充方式为重复原图填充。注意我们画的圆的大小与所构造的放射渐变的大小是一样的,所以不存在空白区域的填充问题。
效果图如下:
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);
构造了四色颜色的数值,以及对应的位置百分比。效果图如下:
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);
}
各个参数的含义上面已经讲解了,来看看效果图:
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);
}
效果图一栏:
注意:尽量避免在 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);
}
这里的参数我就不再细讲了,来看看效果图:
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);
}
效果图:
随着手指的移动更改背景颜色,需要重写 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();
重绘空心小圆。
源码
如果本文有帮到你,记得加关注哦