本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。
转载请标明出处:
http://blog.csdn.net/qian520ao/article/details/61421857
本文出自凶残的程序员的博客
一、前言
Shape标签详解
自定义view中画笔Paint有设置着色器方法paint.setShader()
,我们来看一下这个Shader的子类
二、SweepGradient
[雷达实战]
效果图
使用方式
<qdx.radarview.RadarView
android:layout_width="300dp"
android:layout_height="300dp"
qdx:bgColor="#000" //设置雷达背景色,默认黑色
qdx:circleCount="4" //设置雷达圆圈个数,默认4个
qdx:endColor="#aaff0000"
qdx:startColor="#0000ff00" //设置雷达扫描的颜色,gif的绿色
qdx:lineColor="#00ff00" //雷达线段绘制的颜色
/>
启动雷达 | 关闭雷达 扫描
startScan();
stopScan();
SweepGradient【梯度渐变】
上图我们可以看到SweepGradient有两个构造方法,分别是多色渐变和两色渐变,我介绍一下各自的参数。
- float cx : 中心点x坐标
- float cy : 中心点y坐标
- int[] colors : 渐变的颜色数组
- float[] position : 取值范围[0,1],对应colors[i]的颜色占比
… - int color1 : 这个是两色渐变的起始颜色
int color2 : 两色渐变的终止颜色
绘制雷达
有了这个SweepGradient
,绘制雷达就轻松多了,我们只要把雷达的基线绘制好,然后通过paint处理的着色器绘制渐变的圆进行旋转就是我们所要的雷达效果了。
- 首先我们在values目录下面创建attrs.xml文件,设置自定义view的参数供布局使用
<declare-styleable name="RadarView">
<attr name="startColor" format="color" /> //起始颜色
<attr name="endColor" format="color" /> //终止颜色
<attr name="bgColor" format="color" /> //雷达背景颜色
<attr name="circleCount" format="integer" />//圆圈数量
<attr name="lineColor" format="color" /> //绘制圆圈和基线的颜色
</declare-styleable>
- 在代码中我们获取布局中定义的参数
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
startColor = ta.getColor(R.styleable.RadarView_startColor, startColor);
endColor = ta.getColor(R.styleable.RadarView_endColor, endColor);
mRadarBgColor = ta.getColor(R.styleable.RadarView_bgColor, mRadarBgColor);
mRadarLineColor = ta.getColor(R.styleable.RadarView_lineColor, mRadarLineColor);
radarCircleCount = ta.getInteger(R.styleable.RadarView_circleCount, radarCircleCount);
ta.recycle();
- onMeasure的时候,我们根据用户的不同宽高设置,设置对应的布局大小
int width = measureSize(1, DEFAULT_WIDTH, widthMeasureSpec);
int height = measureSize(0, DEFAULT_HEIGHT, heightMeasureSpec);
int measureSize = Math.max(width, height); //取最大的 宽|高
setMeasuredDimension(measureSize, measureSize);//重新设置布局measure需要调用
/**
* 测绘measure
* @param specType 1为宽, 其他为高
* @param contentSize 默认值
*/
private int measureSize(int specType, int contentSize, int measureSpec) {
int result;
//获取测量的模式和Size
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = Math.max(contentSize, specSize);
} else {
result = contentSize;
if (specType == 1) {
// 根据传人方式计算宽
result += (getPaddingLeft() + getPaddingRight());
} else {
// 根据传人方式计算高
result += (getPaddingTop() + getPaddingBottom());
}
}
return result;
}
- 上述热身运动做完之后我们开始绘制雷达,首先我们将画板canvas移动到屏幕的中心点,然后根据用户设定圆圈个数来绘制圆圈,最后在用设置着色器的paint来绘制过渡颜色的圆,最后进行旋转即可。
canvas.translate(mRadarRadius, mRadarRadius); //将画板移动到屏幕的中心点
mRadarBg.setShader(null);
canvas.drawCircle(0, 0, mRadarRadius, mRadarBg); //绘制底色(默认为黑色),可以使雷达的线看起来更清晰
for (int i = 1; i <= radarCircleCount; i++) { //根据用户设定的圆个数进行绘制
canvas.drawCircle(0, 0, (float) (i * 1.0 / radarCircleCount * mRadarRadius), mRadarPaint); //画圆圈
}
canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint); //绘制雷达基线 x轴
canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint); //绘制雷达基线 y轴
//设置颜色渐变从透明到不透明
mRadarBg.setShader(radarShader);
canvas.concat(matrix);
canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
初始化paint和shader
mRadarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //设置抗锯齿
mRadarPaint.setColor(mRadarLineColor); //画笔颜色
mRadarPaint.setStyle(Paint.Style.STROKE); //设置空心的画笔,只画圆边
mRadarPaint.setStrokeWidth(2); //画笔宽度
mRadarBg = new Paint(Paint.ANTI_ALIAS_FLAG); //设置抗锯齿
mRadarBg.setColor(mRadarBgColor); //画笔颜色
mRadarBg.setStyle(Paint.Style.FILL); //设置空心的画笔,只画圆边
radarShader = new SweepGradient(0, 0, startColor, endColor);
matrix = new Matrix();
我们通过Handler让雷达动起来。
雷达旋转的方法是旋转canvas,这其中有两种实现方式(Matrix详解)
canvas.rotate(degress);
canvas.concat(matrix);
matrix.preRotate(rotateAngel, 0, 0);
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
rotateAngel += 3;
postInvalidate();
matrix.reset(); //重置matrix
matrix.preRotate(rotateAngel, 0, 0);
mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
}
};
三、BitmapShader
[多边形图片实战]
从上图可以看出BitmapShader
只有一个构造方法,
- bitmap : 设置图片
- TileModeX | Y : X,Y 轴填充模式
Shader.TileMode里有三种模式:CLAMP(拉伸)、MIRROR(镜像)、REPETA(重复)
上面的这张德莱文X轴是CLAMP模式,Y轴是REPETA模式,那么问题来了,激光强还是斧头强。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dlw);
BitmapShader bmpShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.REPEAT);
Paintm Paint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setShader(bmpShader);
canvas.drawRect(bmpRect, mPaint); //rect为整个屏幕的大小
- 这时候我们如果在onDraw的时候,在中心位置绘制一个圆,那么是不是就能够相当于以上图为背景,扣出一个圆呢。
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, mPaint); //中心画一个半径为宽的圆
那么就好理解了,paint设置了BitmapShader
相当于绘制了底层的图片背景,你可以设置了BitmapShader
之后,canvas用这枝画笔去绘制任何图像,相当于从背景图扣出对应的形状。
有一点要注意的是BitmapShader通过构造函数初始化设置bitmap时候,默认这个着色背景为当前bitmap的大小,可以通过setLocalMatrix
去重新设置着色背景的形状/范围
- 绘制多边形
效果图
那么我们撸起袖子来通过这个BitmapShader
来绘制多边形,我们首先画一个五角形
思路:
首先我们将中心点移动到五角形的中心,以r为半径,我们睁眼说瞎话的设A角度为0°,那么就可以计算B,C,D,E各自的角度,然后按照我们上片自定义菜单得出的公式
x=Math.sin(2∗PI/360∗angle)∗r
y=Math.cos(2∗PI/360∗angle)∗r
x=Math.sin(Math.toRadians(angle))∗r
y=Math.cos(Math.toRadians(angle))∗r
现在我们就能够求出各个点的坐标了,下面我们看一下绘制流程。
我们的绘制流程是
0 - 2 - 4 - 1 - 3 - 0
A - C - E - B - D - A
int angleCount=5; //angleCount五角星,五个角
int partOfAngle = 360 / angleCount; //每个部分的角度
int[] angles = new int[angleCount];
for (int i = 0; i < angleCount; i++) {
angles[i] = currentAngle + partOfAngle * i;
float x = (float) (Math.sin(Math.toRadians(angles[i])) * startRadius);//获取各个点的x轴坐标
float y = (float) (Math.cos(Math.toRadians(angles[i])) * startRadius);
pointFList.add(new PointF(x, y)); //存入集合,方便使用
}
我们通过比较简单的方法来验证一下我们的计算
canvas.translate(mWidth / 2, mHeight / 2); //将画板移动到中心
mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
mPath.lineTo(pointFList.get(2).x, pointFList.get(2).y);
mPath.lineTo(pointFList.get(4).x, pointFList.get(4).y);
mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
mPath.lineTo(pointFList.get(3).x, pointFList.get(3).y);
canvas.drawPath(mPath, mPaint); //mPaint的stype->Paint.Style.FILL
根据上面这个点的规律,再加上计算(奇/偶数角度计算方式有点区别),下面我直接po出最后的计算方式,虽然感觉不是很干练[小于4的情况 : Go home, You are drunk]
canvas.translate(mWidth / 2, mHeight / 2);
mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
for (int i = 2; i < angleCount; i++) {
if (i % 2 == 0) {// 除以二取余数,余数为0则为偶数,否则奇数
mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
}
}
if (angleCount % 2 == 0) { //如果是偶数,moveTo
mPath.moveTo(pointFList.get(1).x, pointFList.get(1).y);
} else { //奇数,lineTo
mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
}
for (int i = 3; i < angleCount; i++) {
if (i % 2 != 0) {
mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
}
}
canvas.drawPath(mPath, mPaint);
三十角形
[凶残的德莱文]
四、LinearGradient
[霓虹灯文字实战/图片倒影实战]
- float x0 : 起始点渐变 x0 的坐标 float y0 : 起始点渐变 yo 的坐标
- float x1 : 结束渐变点 x1 的坐标 float y1 : 结束渐变点 y1 的坐标
- int color(),int color1 分别代表起始颜色和结束颜色,16进制的颜色 [0xAARRGGBB]
- TileMode : 和bitmapShader一样都是CLAMP(拉伸)、MIRROR(镜像)、REPETA(重复)
- 另外的参数和SweepGradient参数作用是一样的。
不多说了,我们先上代码
mSdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLgShader = new LinearGradient(0, 0, 0, 300, new int[]{
0xFFFF0000,
0xffFF7F00,
0xffFFFF00,
0xff00FF00,
0xff00FFFF,
0xff0000FF,
0xff8B00FF}, null, Shader.TileMode.CLAMP); //position为null则均匀分配
mSdPaint.setShader(mLgShader);
rect = new Rect(0, 0, 300, 300);
结合图文我们可以很清晰的看出,我们从左上角到左下角着色LinearGradient
,所以当着色背景LinearGradient
设置的大小大于或者等于当前显示的范围(rect),那么设置TileMode作用就不大了。那么我们来试试当着色背景LinearGradient
的高度为当前形状1/3的时候,设置不同的TileMode的效果。
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//用来画分割线
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(2);
mSdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLgShader = new LinearGradient(200, 200, 200, 300, new int[]{
0xFFFF0000, //红
0xFFFF7F00, //橙
0xFFFFFF00, //黄
0xFF00FF00, //绿
0xFF00FFFF, //青
0xFF0000FF, //蓝
0xFF8B00FF //紫
}, null, Shader.TileMode.CLAMP);//position为null则均匀分配
mSdPaint.setShader(mLgShader);
rect = new Rect(0, 0, 300, 300);
onDraw
canvas.drawRect(rect, mSdPaint);
canvas.drawLine(0,100,300,100,mPaint); //画2条分割线,方便理解
canvas.drawLine(0,200,300,200,mPaint);
下面分别是CLAMP [拉伸] MIRROR [镜像] REPEAT [重复]
下面我们用LinearGradient
开始实战霓虹文字
先放出代码过个瘾
matrix = new Matrix(); //用来移动Shader的matrix
mPaint = getPaint(); //获取控件本身的paint,这个很重要,获取控件绘制的paint
//我们设置一个LgShader,从左边[距离文字显示2倍宽度的距离]
mShader = new LinearGradient(-2 * w, 0, -w, 0, new int[]{getCurrentTextColor(), Color.RED, Color.YELLOW, Color.BLUE, getCurrentTextColor()}, null, Shader.TileMode.CLAMP);
mPaint.setShader(mShader);
ValueAnimator animator = ValueAnimator.ofInt(0, width * 3); //我们设置value的值为0-getMeasureWidth的3 倍
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mVx = (Integer) animation.getAnimatedValue();
postInvalidate();
}
});
animator.setRepeatMode(ValueAnimator.RESTART); //重新播放
animator.setRepeatCount(-1); //无限循环
animator.setDuration(2000);
animator.start();
onDraw
matrix.reset(); //先重置,以免累加移动
matrix.preTranslate(mVx, 0);
mShader.setLocalMatrix(matrix);
xml代码
<qdx.radarview.shader.LinearGradientText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="塞纳河畔 左岸的咖啡\n我手一杯 品尝你的美"
android:textColor="#ff00ff"
android:textSize="40sp" />
解刨图
- PS:好气啊,昨晚编辑了这篇文章,写了蛮多的没有保存到线上草稿箱,手抽了一下把浏览器关了…T T,上图原本是有呕心沥血做出,后来被我清除了,大家凑合着看吧。。。。
- 回归正题,我们来分解一下上面这张图,图片做的是一个效果,但是实际上我理一下,当前我设置的文字颜色是#ff00ff即紫色
getCurrentTextColor(), Color.RED, Color.YELLOW, Color.BLUE, getCurrentTextColor()
- 所以排序应该是 紫紫紫 紫红黄蓝紫 紫紫紫 ,也就是说红色的左边和蓝色的右边填充的颜色应该是紫色(TileMode为CLAMP),所以我们开始translation的时候就能比较柔和的先显示TextView当前的颜色然后再进行过渡。
我们拿LinearGradient
来做一个图片倒影
- 先拿效果图精神精神
- 下面我来介绍一下这张图片,女主叫克拉拉,英国籍韩裔女演员,又名李成敏。2004年,获得韩国第一届网络美女照片竞赛第一名...不好意思扯远了首先我们讲一下思路,首先我们需要设置2张bitmap,一张是正序图片,另一张是倒影的图片(都为同张图片),关键是倒影图片设置完之后,我们需要调节一下图片的透明度,这里需要用到Xfermoder的知识,我们用LinearGradient
绘制出与bitmap相同大小的RectF(dst目标),然后用Xfermode的SRC_OUT模式绘制倒影的bitmap,再加上先前对LinearGradient
的颜色设定就能够做到如图的倒影效果了。
private Bitmap bitmap; //克拉拉bmp
private Bitmap bitmapShadow; //倒影的克拉拉bmp
private Paint mBmpShadowPaint; //绘制倒影RECTF的paint
private Paint mXfPaint; //设置Xfermode的paint
private Xfermode xfermode; //SRC_OUT Xfermode
private void init(){
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kll); //先从资源文件获取克拉拉bmp
mBmpShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mXfPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT); //设置XferModer
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int bmpW = bitmap.getWidth();
float ratioW = w * 1.0f / bmpW * 2 / 3;
//压缩克拉拉bmp ,我们设置图片的宽度为屏幕的 2/3
bitmap = BmpUtils.zoomImg(bitmap, (int) (bitmap.getWidth() * ratioW), (int) (bitmap.getHeight() * ratioW));
Matrix matrix = new Matrix();
matrix.setScale(1, -1); //X轴不变化,y轴旋转180°,即设置倒影的bmp
bitmapShadow = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); //克拉拉bmp旋转,即旋转的克拉拉bmp
//我们设置LinearGradient,首先旋转的克拉拉bmp 上面 10%的部分我们透明度设置稍微小一点,10%-30%过渡,最后渐进到透明
LinearGradient mLgShader = new LinearGradient(0, bitmap.getHeight(), 0, bitmap.getHeight() * 2, new int[]{0x00000000, 0x11000000, 0xaa000000, Color.WHITE}, new float[]{0, 0.1f, 0.4f, 0.6f}, Shader.TileMode.REPEAT);
mBmpShadowPaint.setShader(mLgShader);
shadowRectF = new RectF(0, bitmap.getHeight(), bitmap.getWidth(), bitmap.getHeight() * 2); //绘制在 克拉拉bmp图片下方。getHeight*2
}
private RectF shadowRectF;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth() / 6, getHeight() / 10);//首先我们将图片居中(因为图片是屏幕的2/3,所以我们往右 1/6)
canvas.drawBitmap(bitmap, 0, 0, null);//先绘制克拉拉
int layerID = canvas.saveLayer(shadowRectF, null, Canvas.ALL_SAVE_FLAG);//新建图层,用来进行XferMode手术
canvas.drawRect(shadowRectF, mBmpShadowPaint); //绘制dst图片
mXfPaint.setXfermode(xfermode); //SRC_OUT
canvas.drawBitmap(bitmapShadow, 0, bitmap.getHeight(), mXfPaint); //绘制src图片
mXfPaint.setXfermode(null);
canvas.restoreToCount(layerID);//还原。
}
五、总结
- 最后还剩下
RadialGradient
水波纹效果和ComposeShader
(Shader),有兴趣的可以捯饬捯饬。
通过上面三个实战,我们可以发现无论是哪种Shader,都是从左上角开始填充,然后利用Canvas去绘制具体某一区域(也就是说你canvas绘制的就是这相当于满屏的着色背景的一部分。),我们可以通过Shader和Xfermode绘制出比较奇幻的效果,例如RadialGradient
和Xfermode的SCREEN
可以绘制灯光遮罩效果,还有支付宝的咻一咻,等着大家慢慢发掘。如果有好的idea可以留言给我。