文章目录
一、概念
画笔,保存了绘制几何图形、文本和位图的样式和颜色信息
二、常用API
通过ALT+7查看Paint类结构图,发现有大量的get、set方法。里面还存有很多的native方法, 实际上当我们调用Paint类中的方法时,实际上是间接调用了native方法。以setSubpixelText方法为例它就直接调用native方法nSetSubpixelText():
public void setSubpixelText(boolean subpixelText) {
nSetSubpixelText(mNativePaint, subpixelText);
}
nSetSubpixelText()就是一个native方法:
@CriticalNative
private static native void nSetSubpixelText(long paintPtr, boolean subpixelText);
这需要我们掌握Paint类中常用的一些API方法:
mPaint = new Paint();//初始化
mPaint.setColor(Color.RED);//设置颜色
mPaint.setARGB(255,255,255,0);//设置paint对象颜色,范围0~255
mPaint.setAlpha(200);//设置alpha透明度,范围0~255
mPaint.setAntiAlias(true);//抗锯齿
mPaint.setStyle(Paint.Style.STROKE);//描边效果:FILL填充效果;STROKE:描边;FILL_AND_STROKE:同时作用
mPaint.setStrokeWidth(4);//描边宽度
mPaint.setStrokeCap(Paint.Cap.ROUND);//圆角效果:BUTT默认/ROUND圆角/SQUARE方角
mPaint.setStrokeJoin(Paint.Join.MITER);//拐角风格:MITER默认,尖角/ROUND/BEBEL切除尖角
mPaint.setShader(new SweepGradient(200,200,Color.BLUE,Color.RED));//设置环形渲染器,加上圆环效果
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));//设置图层混合模式
mPaint.setColorFilter(new LightingColorFilter(0x00ffff,0x000000));//设置颜色过滤器
mPaint.setFilterBitmap(true);//设置双线性过滤
mPaint.setMaskFilter(new BlurMaskFilter(10,BlurMaskFilter.Blur.NORMAL));//设置画笔遮罩滤镜,传入度数和样式
mPaint.setTextScaleX(2);//设置文本缩放倍数
mPaint.setTextSize(38);//设置文字大小
mPaint.setTextAlign(Paint.Align.LEFT);//对其方式
mPaint.setUnderlineText(true);//设置下划线
String str="Android高级开发工程师";
Rect rect = new Rect();
mPaint.getTextBounds(str,0,str.length(),rect);//测量文本大小,将文本大小信息存放在rect中
mPaint.measureText(str);//获取文本的宽
mPaint.getFontMetrics();//获取文体度量对象
- 1.setColor(int Color)参数具体的颜色纸,16进制数值,0xFFFF0000
- 2.setARGB(int a,int r,int g,int b)参数:分别透明度、红、绿、蓝。0-255数值
- 3.setShader(Shader shader)参数着色器对象,一般使用shader的几个子类
- LinearGradient:线性渲染
- RadialGradient:环形渲染
- SweepGradient:扫描渲染
- BitmapShader:位图渲染
- ComposeShader:组合渲染,只能2个组合。例如LinearGradient+BitmapShader
- 4.setColorFilter(ColorFilter colorFilter)设置颜色过滤。一般使用ColorFilter三个子类:
- PorterDuffColorFilter:指定一个颜色和一种PorterDuff.Mode与绘制对象进行合成
- ColorMatrixColorFilter:使用一个ColorMatrix来对颜色进行处理
- LightingColorFilter光照效果
接下来重点看一下渲染器。
三、渲染器
3.1 LinearGradient线性渲染
构造方法:
public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
@Nullable float positions[], @NonNull TileMode tile)
参数:
- (x0,y0):渐变起始点坐标
- (x1,y1):渐变结束点坐标
- color0:渐变开始点颜色,16进制的颜色表示,必须要带有透明度
- color1:渐变结束颜色的颜色
- colors:渐变数组
- positions:浮点型数组,position的取值范围为【0,1】,作用是指定某个位置的颜色值。
- title:用于指定控件区域大于指定的渐变区域时,空白区域的颜色填充方法。端点范围之外的着色规则,类型是TitleMode
对于position,new float[]{0.5f,1} 影响了渐变的效果,是相对位置。即50%~100%才开始渐变 。可为null,null就表示渐变为线性变化:
//如果positions不为空且其长度与颜色长度不相等,就抛出异常
if (positions != null && colors.length != positions.length) {
throw new IllegalArgumentException("color and position arrays must be of equal length");
}
使用:
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
mPaint = new Paint();//初始化
mPaint.setAntiAlias(true);//抗锯齿
mPaint.setStyle(Paint.Style.FILL);
//1.线性渲染
mShader = new LinearGradient(0,0,500,500,new int[]{Color.RED,Color.BLUE},new float[]{0.5f,1}, Shader.TileMode.CLAMP);
mPaint.setShader(mShader);
canvas.drawCircle(250,250,250,mPaint);
效果:
由红向蓝渐变,如果圆不明显,可以改成矩形
3.2 环形渲染RadialGradient
构造方法:
public RadialGradient(float centerX, float centerY, float radius,
@NonNull @ColorInt int colors[], @Nullable float stops[],
@NonNull TileMode tileMode)
参数:
- centerX,CenterY:辐射中心的坐标
- radius:辐射半径
- centerColor:辐射中心的颜色
- edgeColor:辐射边缘的颜色
- colors:渐变颜色数组
- stops:渐变位置数组,类似扫描渐变的position数组,取值【0,1】中心点为0,半径到达位置为1.0f
- tileMode:辐射范围之外的着色规则,类型是TileMode。shader表示未覆盖以外的 填充方式
使用:
//2.环形渲染
mPaint.setShader(new RadialGradient(250,250,500,new int[]{Color.RED,Color.BLUE,Color.YELLOW},null, Shader.TileMode.CLAMP));
canvas.drawCircle(250,250,250,mPaint);
效果:
由圆心开始向外环形渲染
3.3 扫描渲染SweepGradient
构造方法:
public SweepGradient(float cx, float cy, @ColorInt int color0, @ColorInt int color1)
参数:
- cx,cy:扫描的中心
- color0:扫描的起始位置
- color1:扫描的终止位置
使用:
//3.扫描渲染
mPaint.setShader(new SweepGradient(250,250,Color.RED,Color.BLUE));
canvas.drawCircle(250,250,250,mPaint);
效果:
从x轴正方向,沿着顺时针方向选择360°
3.4 位图渲染BitmapShader
构造方法:
public BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)
参数:
- bitmap:用来做模板的Bitmap对象
- tileX:X轴方向的着色规则,类型是TileMode
- tileY:Y轴方向的着色规则,类型是TileMode
使用:
//4.位图渲染
mPaint.setShader(new BitmapShader(mBitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
// canvas.drawRect(0,0,mBitmap.getWidth(),mBitmap.getHeight(),mPaint);
canvas.drawCircle(250,250,250,mPaint);
效果:
在水平、垂直方向的最后一个像素,有一个拉伸填充效果。这个效果源自参数TileMode:
- CLAMP 当绘制的区域超出图片自身的区域,会以最后一个像素进行拉伸、填充
- REPEAT 绘制区域超出渲染区域的部分,重复排版–平铺效果,也就是copy
- MIRROR 绘制区域超出渲染区域的部分,镜像填充
3.5 组合渲染ComposeShader
构造方法:
public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB,
@NonNull PorterDuff.Mode mode)
public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode)
参数:
- shaderA,shaderB:要混合的两种shader
- xfermode mode:组合两种shader颜色的模式
- porterDuff.Mode mode:组合两种shader颜色的模式
使用:
//5.组合渲染
mPaint.setShader(new ComposeShader(new BitmapShader(mBitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT),
new LinearGradient(0,0,1000,1600,new int[]{Color.RED,Color.GREEN,Color.BLUE},null,Shader.TileMode.CLAMP),
PorterDuff.Mode.MULTIPLY ));
canvas.drawCircle(250,250,250,mPaint);
效果:
绘制区域远大于位图,重复排版,同时线性渲染也加持了。
PorterDuff.Mode.MULTIPLY:两个渲染器进行渲染的图层混合规则
四、PorterDuff.Mode图层混合模式
就是将所绘制图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值。
一共有18种模式:
- CLEAR
- SRC
- DST
- SRC_OVER
- DST_OVER
- SRC_IN
- DST_IN
- SRC_OUT
- DST_OUT
- SRC_ATOP
- DST_ATOP
- XOR
- DARKEN
- LIGHTEN
- MULTIPLY
- SCREEN
- ADD
- OVERLAY
每一种模式都代表一个规则,图层混合后的效果,是通过规则计算alpha通道值和颜色值C。src:原图像;dst:目标图像
4.1 离屏绘制
画笔Paint.setXfermode(),首先要禁止硬件加速(因为图层混合有些api是不支持硬件加速的,但是系统默认开启)
关于离屏绘制,首先开启离屏绘制:
//禁止硬件加速,图层混合有些api不支持,但是系统默认开启
setLayerType(View.LAYER_TYPE_SOFTWARE,null);
setBackgroundColor(Color.GRAY);//给自定义view加一个背景
//离屏绘制
int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
//目标图
canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
//设置混合模式
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//源图,重叠区域右下角部分
canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
//清除混合模式
mPaint.setXfermode(null);
//进行图层恢复
canvas.restoreToCount(layerId);
然后关闭离屏绘制:
//禁止硬件加速,图层混合有些api不支持,但是系统默认开启
setLayerType(View.LAYER_TYPE_SOFTWARE,null);
setBackgroundColor(Color.GRAY);//给自定义view加一个背景
//离屏绘制
// int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
//目标图
canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
//设置混合模式
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//源图,重叠区域右下角部分
canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
//清除混合模式
mPaint.setXfermode(null);
//进行图层恢复
// canvas.restoreToCount(layerId);
发现灰色背景不见了,它直接参与了图层混合的计算。不使用离屏绘制,直接考虑canvas的绘制,导致计算结果不正确。
使用离屏绘制(或者叫离屏缓冲),先创建一个图层,将两个图层混合后的结果绘制到画布上。 通过使用离屏绘制,把要绘制的内容单独绘制在缓冲层,保证Xfermode的使用不会出现任何错误。
4.2 使用离屏绘制
4.2.1 Canvas.saveLayer()
Canvas.saveLayer()可以做短时的离屏缓冲,在绘制之前保存,绘制之后恢复
int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
//画方
canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
//设置Xfermode
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//画圆
canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
//用完及时清除Xfermode
mPaint.setXfermode(null);
//进行图层恢复
canvas.restoreToCount(layerId);
4.2.2 View.setLayerType()
View.setLayerType()直接把整个View都绘制在离屏缓冲中
//使用GPU来缓冲
setLayerType(LAYER_TYPE_HARDWARE);
//使用一个Bitmap来缓冲
setLayerType(LAYER_TYPE_SOFTWARE);
图层混合模式只作用于src源图像区域。每一种模式都有对应的公式来计算各自的alpha通道值、颜色值。以DARKEN为例:
//alpha通道值:源图像的alpha通道值+目标图像的alpha通道值-源图像的alpha通道值*目标图像的alpha通道值
<p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
//颜色的计算方式:(1-目标图像的alpha通道值)* 源图像的alpha颜色值 + (1-源图像的alpha通道值)* 目标图像的颜色值 + min(源图像的颜色值,目标图像的颜色值)
<p>\(C_{out} = (1 - \alpha_{dst}) * C_{src} + (1 - \alpha_{src}) * C_{dst} + min(C_{src}, C_{dst})\)</p>
4.3 案例-幸运刮刮卡
这里用到了触摸,在onTouchEvent方法中处理,绘制贝塞尔曲线,最后调用invalidate()方法,绘是的onDraw方法被调用。
onDraw方法中首先绘制结果,然后进行离屏绘制,将我们手势的二阶 贝塞尔曲线绘制到bitmap上,然后再绘制目标图像。之后设置图层混合模式为SRC_OUT(清除相交的区域),最后绘制源图像、清除图层混合模式、图层恢复。
package com.example.admin.uimaster.xfermode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.example.admin.uimaster.R;
public class XfermodeEraserView extends View {
private Paint mPaint;
private Bitmap mDstBmp, mSrcBmp, mTxtBmp;
private Path mPath;
public XfermodeEraserView(Context context) {
this(context, null);
}
public XfermodeEraserView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XfermodeEraserView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//初始化画笔
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(80);
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
//初始化图片对象
mTxtBmp = BitmapFactory.decodeResource(getResources(), R.drawable.result);
mSrcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.eraser);
mDstBmp = Bitmap.createBitmap(mSrcBmp.getWidth(), mSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
//路径(贝塞尔曲线)
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制刮奖结果
canvas.drawBitmap(mTxtBmp, 0, 0, mPaint);
//使用离屏绘制
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
//先将路径绘制到 bitmap上
Canvas dstCanvas = new Canvas(mDstBmp);
dstCanvas.drawPath(mPath, mPaint);
//绘制 目标图像
canvas.drawBitmap(mDstBmp, 0, 0, mPaint);
//设置 模式 为 SRC_OUT, 擦橡皮区域为交集区域需要清掉像素
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
//绘制源图像
canvas.drawBitmap(mSrcBmp, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerID);
}
private float mEventX, mEventY;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mEventX = event.getX();
mEventY = event.getY();
mPath.moveTo(mEventX, mEventY);
break;
case MotionEvent.ACTION_MOVE:
float endX = (event.getX() - mEventX) / 2 + mEventX;
float endY = (event.getY() - mEventY) / 2 + mEventY;
//画二阶贝塞尔曲线
mPath.quadTo(mEventX, mEventY, endX, endY);
mEventX = event.getX();
mEventY = event.getY();
break;
}
invalidate();
return true; //消费事件
}
}