本文提供了三个综合性的完整实例来辅助理解。
本篇文章我们来分析View绘制方面的实践。
一个简单的自定义View
public class DrawView extends View {
Paint paint = new Paint();
public DrawView(Context context) {
super(context);
}
public DrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.BLACK);
canvas.drawCircle(150, 150, 150, paint);
}
}
它在屏幕上绘制了一个圆形,如图:
在处理绘制的时候有以下几个关键点:
我们分别来看看这个关键的角色。
我们讨论的第一个问题就是View/ViewGroup的绘制顺序问题,绘制在View.draw()方法里调用的,具体的执行顺序是:
我们先从个小例子开始。
我们如果继承View来实现自定义View。View类的onDraw()是空实现,所以我们的绘制代码写在super.onDraw(canvas)的前面或者后面都没有关系,如下所示:
public class DrawView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制代码,写在super.onDraw(canvas)前后均可
}
}
但是如果我们继承特定的控件,例如TextView。我们就需要去考虑TextView的绘制逻辑。
public class DrawView extends TextView {
@Override
protected void onDraw(Canvas canvas) {
//写在前面,DrawView的绘制会先于TextView的绘制,TextView绘制的内容可以会覆盖DrawView
super.onDraw(canvas);
//写在后面,DrawView的绘制会晚于TextView的绘制,DrawView绘制的内容可以会覆盖TextView
}
}
具体怎么做取决于你实际的需求,例如你如果想给TextView加个背景,就写在super.onDraw(canvas)前面,想给TextView前面加些点缀,就 写在super.onDraw(canvas)后面。
我们来写个例子理解下。
举例
public class LabelImageView extends AppCompatImageView {
/**
* 梯形距离左上角的长度
*/
private static final int LABEL_LENGTH = 100;
/**
* 梯形斜边的长度
*/
private static final int LABEL_HYPOTENUSE_LENGTH = 100;
private Paint textPaint;
private Paint backgroundPaint;
private Path pathText;
private Path pathBackground;
public LabelImageView(Context context) {
super(context);
init();
}
public LabelImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//计算路径
calculatePath(getMeasuredWidth(), getMeasuredHeight());
canvas.drawPath(pathBackground, backgroundPaint);
canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
}
@Override
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
}
/**
* 计算路径 x1 x2
* ................................ distance(标签离右上角的垂直距离)
* . . . .
* . . .. y1
* . . .
* . . .
* . . y2 height(标签垂直高度)
* . .
* ................................
*/
private void calculatePath(int measuredWidth, int measuredHeight) {
int top = 185;
int right = measuredWidth;
float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
float x2 = right - LABEL_HYPOTENUSE_LENGTH;
float y1 = top + LABEL_LENGTH;
float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;
pathText.reset();
pathText.moveTo(x1, top);
pathText.lineTo(right, y2);
pathText.close();
pathBackground.reset();
pathBackground.moveTo(x1, top);
pathBackground.lineTo(x2, top);
pathBackground.lineTo(right, y1);
pathBackground.lineTo(right, y2);
pathBackground.close();
}
private void init() {
pathText = new Path();
pathBackground = new Path();
textPaint = new Paint();
textPaint.setTextSize(50);
textPaint.setFakeBoldText(true);
textPaint.setColor(Color.WHITE);
backgroundPaint = new Paint();
backgroundPaint.setColor(Color.RED);
backgroundPaint.setStyle(Paint.Style.FILL);
}
}
所以你可以看到,当我们继承了一个View,根据需求的不同可以选择性重写我们需要的方法,在super前插入代码和在super后插入代码,效果是不一样的。
Paint:顾名思义,画笔,通过Paint可以对绘制行为进行控制。
Paint有三种构造方法
public class Paint {
//空的构造方法
public Paint() {
this(0);
}
//传入flags来构造Paint,flags用来控制Paint的行为,例如:抗锯齿等
public Paint(int flags) {
mNativePaint = nInit();
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
// TODO: Turning off hinting has undesirable side effects, we need to
// revisit hinting once we add support for subpixel positioning
// setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
// ? HINTING_OFF : HINTING_ON);
mCompatScaling = mInvCompatScaling = 1;
setTextLocales(LocaleList.getAdjustedDefault());
}
//传入另外一个Paint来构造新的Paint
public Paint(Paint paint) {
mNativePaint = nInitWithPaint(paint.getNativeInstance());
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
setClassVariablesFrom(paint);
}
}
在Paint类中,处理颜色主要有三个方法。
着色器是图像领域的一个通用概念,它提供的是一套着色规则。
public Shader setShader(Shader shader)
着色器具体由Shader的子类实现:
LinearGradient - 线性渐变
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
举例
//线性渐变
Shader shader1 = new LinearGradient(0, 100, 200, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
Shader shader2 = new LinearGradient(0, 600, 200, 600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);
Shader shader3 = new LinearGradient(0, 1100, 200, 1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);
canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);
canvas.drawRect(0, 1100, 1000, 1500, paint3);
SweepGradient - 辐射渐变
public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode)
举例
//辐射渐变
Shader shader1 = new RadialGradient(0, 100, 200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
Shader shader2 = new RadialGradient(0, 600, 200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);
Shader shader3 = new RadialGradient(0, 1100, 200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);
canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);
BitmapShader - 位图着色
使用位图的像素来填充图形或者文字。
public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
举例
BitmapShader是一个很有用的类,可以利用该类做各种各样的图片裁剪。
//位图着色
Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
//绘制圆形
canvas.drawCircle(500, 500, 300, paint1);
ComposeShader - 组合Shader
ComposeShader可以将连个Shader组合在一起。
public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
PorterDuff.Mode用来指定两个Shader叠加时颜色的绘制策略,它有很多种策略,也就是以一种怎样的模式来与原图像进行合成,具体如下:
蓝色矩形为原图像,红色圆形为目标图像。
更多细节可以参见PorterDuff.Mode官方文档。
颜色过滤器可以将颜色按照一定的规则输出,常见于各种滤镜效果。
public ColorFilter setColorFilter(ColorFilter filter)
我们通常使用的是ColorFilter的三个子类:
LightingColorFilter - 模拟光照效果
public LightingColorFilter(int mul, int add)
mul 和 add 都是和颜色值格式相同的 int 值,其中 mul 用来和目标像素相乘,add 用来和目标像素相加。
举例
//颜色过滤器
ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
paint2.setColorFilter(colorFilter1);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);
PorterDuffColorFilter - 模拟颜色混合效果
public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)
PorterDuffColorFilter指定一种颜色和PorterDuff.Mode来与源图像就行合成,也就是以一种怎样的模式来与原图像进行合成,我们在上面已经讲过这个内容。
举例
//我们在使用Xfermode的时候也是使用它的子类PorterDuffXfermode
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方
paint.setXfermode(xfermode); // 设置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆
paint.setXfermode(null); // 用完及时清除 Xfermode
ColorMatrixColorFilter - 颜色矩阵过滤
ColorMatrixColorFilter使用一个颜色矩阵ColorMatrix来对象图像进行处理。
public ColorMatrixColorFilter(ColorMatrix matrix)
ColorMatrix是一个4x5的矩阵
[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]
通过计算,ColorMatrix可以对要绘制的像素进行转换,如下:
R’ = a*R + b*G + c*B + d*A + e;
G’ = f*R + g*G + h*B + i*A + j;
B’ = k*R + l*G + m*B + n*A + o;
A’ = p*R + q*G + r*B + s*A + t;
利用ColorMatrixColorFilter(可以实现很多炫酷的滤镜效果。
Paint.setXfermode(Xfermode xfermode)方法,它也是一种混合图像的方法。
Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以View中已有的内 容作为目标图像,选取一个PorterDuff.Mode作为绘制内容的颜色处理方案。
小结
关于PorterDuff.Mode,我们已经提到
这三种以不同的方式来使用PorterDuff.Mode,但是原理都是一样的。
Paint里有大量方法来设置文字的绘制属性,事实上文字在Android底层是被当做图片来处理的。
设置抗锯齿,默认关闭,用来是图像的绘制更加圆润。我们还可以在初始化的时候设置Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);。
设置填充风格,
如果是划线模式,我们针对线条还可以有多种设置。
setStrokeWidth(float width) - 设置线条粗细
setStrokeCap(Paint.Cap cap) - 设置线头的形状,默认为 BUTT
setStrokeJoin(Paint.Join join) - 设置拐角的形状。默认为 MITER
setStrokeMiter(float miter)- 设置 MITER 型拐角的延长线的最大值
设置图像的抖动。
抖动是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。
当然这个效果旨在低位色的时候比较有用,例如,ARGB_4444 或者 RGB_565,不过现在Android默认的色彩深度都是32位的ARGB_8888,这个方法的效果没有那么明显。
设置是否使用双线性过滤来绘制 Bitmap 。
图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。
设置图形的轮廓效果。Android有六种PathEffect:
CornerPathEffect(float radius)
DiscretePathEffect(float segmentLength, float deviation)
DashPathEffect(float[] intervals, float phase)
PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
SumPathEffect(PathEffect first, PathEffect second)
ComposePathEffect(PathEffect outerpe, PathEffect innerpe)
举例
//图形轮廓效果
//绘制圆角
PathEffect cornerPathEffect = new CornerPathEffect(20);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(5);
paint1.setPathEffect(cornerPathEffect);
//绘制尖角
PathEffect discretePathEffect = new DiscretePathEffect(20, 5);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);
paint2.setPathEffect(discretePathEffect);
//绘制虚线
PathEffect dashPathEffect = new DashPathEffect(new float[]{20,10, 5, 10}, 0);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
paint3.setPathEffect(dashPathEffect);
//使用path来绘制虚线
Path path = new Path();//画一个三角来填充虚线
path.lineTo(40, 40);
path.lineTo(0, 40);
path.close();
PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40, 0, PathDashPathEffect.Style.TRANSLATE);
paint4.setStyle(Paint.Style.STROKE);
paint4.setStrokeWidth(5);
paint4.setPathEffect(pathDashPathEffect);
设置阴影图层,处于目标下层图层。
举例
paint1.setTextSize(200);
paint1.setShadowLayer(10, 0, 0, Color.RED);
canvas.drawText("Android", 80, 300 ,paint1);
注:在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己 的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。
设置图层遮罩层,处于目标上层图层。
MaskFilter有两个子类:
举例
模糊效果
分别为:
//设置遮罩图层,处于目标上层图层
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
paint2.setMaskFilter(blurMaskFilter);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);
注:在硬件加速开启的情况下, setMaskFilter(MaskFilter maskfilter)只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。关闭硬件加速可以调用 View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)或者在Activity标签里设置android:hardwareAccelerated="false"。
Canvas实现了Android 2D图形的绘制,底层基于Skia实现。
Canvas提供了丰富的对象绘制方法,一般都以drawXXX()打头,绘制的对象包括:
这里的方法大都很简单,我们来描述下期中比较复杂的方法。
弧线
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {
native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
useCenter, paint.getNativeInstance());
}
例如
paint.setStyle(Paint.Style.FILL);//填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint);
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint);
paint.setStyle(Paint.Style.STROKE);//画线模式
paint.setStrokeWidth(5);
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint);
位图
第一个方法很简单,就是在指定的坐标处开始绘制位图。我们着重来看看第二个方法,这个方法不是很常用(可能是计算比较复杂的锅😓),但这并不影响它强大的功能。
drawBitmapMesh()方法将位图分为若干网格,然后对每个网格进行扭曲处理。我们先来看看这个方法的参数:
我们来用drawBitmapMesh()方法实现一个水面涟漪效果。
举例
/**
* 利用Canvas.drawBitmapMeshC()方法对图像做扭曲处理,模拟水波效果。
* <p>
* For more information, you can visit https://github.com/guoxiaoxing or contact me by
* guoxiaoxingse@163.com
*
* @author guoxiaoxing
* @since 2017/9/12 下午3:44
*/
public class RippleLayout extends FrameLayout {
/**
* 图片横向、纵向的格树
*/
private final int MESH_WIDTH = 20;
private final int MESH_HEIGHT = 20;
/**
* 图片顶点数
*/
private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);
/**
* 原坐标数组
*/
private final float[] originVerts = new float[VERTS_COUNT * 2];
/**
* 转换后的坐标数组
*/
private final float[] targetVerts = new float[VERTS_COUNT * 2];
/**
* 当前空间的图像
*/
private Bitmap bitmap;
/**
* 水波宽度的一半
*/
private float rippleWidth = 100f;
/**
* 水波扩展的速度
*/
private float rippleRadius = 15f;
/**
* 水波半径
*/
private float rippleSpeed = 15f;
/**
* 水波动画是否在进行中
*/
private boolean isRippling;
public RippleLayout(@NonNull Context context) {
super(context);
}
public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (isRippling && bitmap != null) {
canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0, null, 0, null);
} else {
super.dispatchDraw(canvas);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
showRipple(ev.getX(), ev.getY());
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 显示水波动画
*
* @param originX 原点 x 坐标
* @param originY 原点 y 坐标
*/
public void showRipple(final float originX, final float originY) {
if (isRippling) {
return;
}
initData();
if (bitmap == null) {
return;
}
isRippling = true;
//循环次数,通过控件对角线距离计算,确保水波纹完全消失
int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
Observable.interval(0, 10, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.take(count + 1)
.subscribe(new Consumer<Long>() {
@Override
public void accept(@NonNull Long aLong) throws Exception {
rippleRadius = aLong * rippleSpeed;
warp(originX, originY);
if (aLong == count) {
isRippling = false;
}
}
});
}
/**
* 初始化 Bitmap 及对应数组
*/
private void initData() {
bitmap = getCacheBitmapFromView(this);
if (bitmap == null) {
return;
}
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
for (int height = 0; height <= MESH_HEIGHT; height++) {
float y = bitmapHeight * height / MESH_HEIGHT;
for (int width = 0; width <= MESH_WIDTH; width++) {
float x = bitmapWidth * width / MESH_WIDTH;
originVerts[index * 2] = targetVerts[index * 2] = x;
originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
index += 1;
}
}
}
/**
* 图片转换
*
* @param originX 原点 x 坐标
* @param originY 原点 y 坐标
*/
private void warp(float originX, float originY) {
for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
float staticX = originVerts[i];
float staticY = originVerts[i + 1];
float length = getLength(staticX - originX, staticY - originY);
if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
PointF point = getRipplePoint(originX, originY, staticX, staticY);
targetVerts[i] = point.x;
targetVerts[i + 1] = point.y;
} else {
//复原
targetVerts[i] = originVerts[i];
targetVerts[i + 1] = originVerts[i + 1];
}
}
invalidate();
}
/**
* 获取水波的偏移坐标
*
* @param originX 原点 x 坐标
* @param originY 原点 y 坐标
* @param staticX 待偏移顶点的原 x 坐标
* @param staticY 待偏移顶点的原 y 坐标
* @return 偏移后坐标
*/
private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
float length = getLength(staticX - originX, staticY - originY);
//偏移点与原点间的角度
float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
//计算偏移距离
float rate = (length - rippleRadius) / rippleWidth;
float offset = (float) Math.cos(rate) * 10f;
float offsetX = offset * (float) Math.cos(angle);
float offsetY = offset * (float) Math.sin(angle);
//计算偏移后的坐标
float targetX;
float targetY;
if (length < rippleRadius + rippleWidth && length > rippleRadius) {
//波峰外的偏移坐标
if (staticX > originX) {
targetX = staticX + offsetX;
} else {
targetX = staticX - offsetX;
}
if (staticY > originY) {
targetY = staticY + offsetY;
} else {
targetY = staticY - offsetY;
}
} else {
//波峰内的偏移坐标
if (staticX > originY) {
targetX = staticX - offsetX;
} else {
targetX = staticX + offsetX;
}
if (staticY > originY) {
targetY = staticY - offsetY;
} else {
targetY = staticY + offsetY;
}
}
return new PointF(targetX, targetY);
}
/**
* 根据宽高,获取对角线距离
*
* @param width 宽
* @param height 高
* @return 距离
*/
private float getLength(float width, float height) {
return (float) Math.sqrt(width * width + height * height);
}
/**
* 获取 View 的缓存视图
*
* @param view 对应的View
* @return 对应View的缓存视图
*/
private Bitmap getCacheBitmapFromView(View view) {
view.setDrawingCacheEnabled(true);
view.buildDrawingCache(true);
final Bitmap drawingCache = view.getDrawingCache();
Bitmap bitmap;
if (drawingCache != null) {
bitmap = Bitmap.createBitmap(drawingCache);
view.setDrawingCacheEnabled(false);
} else {
bitmap = null;
}
return bitmap;
}
}
路径
public void drawPath(@NonNull Path path, @NonNull Paint paint) {
if (path.isSimplePath && path.rects != null) {
native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
} else {
native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
}
}
drawPath()可以绘制自定义图形,图形的路径用Path对象来描述。
Path对象可以描述很多图形,具体说来:
Canvas里的范围裁切主要有两类方法:
举例
clipReact
clipPath
//范围裁切
canvas.save();//保存画布
canvas.clipRect(200, 200, 900, 900);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢复画布
canvas.save();//保存画布
path.addCircle(500, 500, 300, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢复画布
关于几何变换有三种实现方式:
Canvas还提供了对象的位置变换的方法,其中包括:
举例
canvas.save();//保存画布
canvas.skew(0, 0.5f);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢复画布
canvas.save();//保存画布
canvas.rotate(45, 750, 750);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢复画布
注:1 为了不影响其他绘制操作,在进行变换之前需要调用canvas.save()保存画布,变换完成以后再调用canvas.restore()来恢复画布。 2 Canvas几何变换的顺序是相反的,例如我们在代码写了:canvas.skew(0, 0.5f); canvas.rotate(45, 750, 750); 它的实际调用顺序是canvas.rotate(45, 750, 750); -> canvas.skew(0, 0.5f)
Matrix也实现了Canvas里的四种常规变换,它的实现流程如下:
Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。
举例
//Matrix几何变换
canvas.save();//保存画布
matrix.preSkew(0, 0.5f);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢复画布
canvas.save();//保存画布
matrix.reset();
matrix.preRotate(45, 750, 750);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢复画布
Matrix除了四种基本的几何变换,还可以自定义几何变换。
这两个方法都是通过多点的映射的方式来直接设置变换,把指定的点移动到给出的位置,从而发生形变。
举例
//Matrix几何变换
canvas.save();//保存画布
matrix.setPolyToPoly(src, 0, dst, 0, 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, 0, 0, paint1);
canvas.restore();//恢复画布
在讲解Camera的三维变换之前,我们需要先理解Camera的坐标系系统。
我们前面说过,Canvas使用的是二维坐标系。
而Camera使用的是三维坐标系,这里偷个懒😊,借用凯哥的图来描述一下。
关于Camera坐标系:
比如我们在Camera坐标系里做个X轴方向的旋转
Camera的三维变换包括:旋转、平移与移动相机。
旋转
平移
移动相机
举例
旋转
//Camera三维变换
canvas.save();//保存画布
camera.save();//保存camera
camera.rotateX(45);
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布
平移
//Camera三维变换
canvas.save();//保存画布
camera.save();//保存camera
camera.translate(500, 500, 500);
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布
移动相机
//Camera三维变换
canvas.save();//保存画布
camera.save();//保存camera
camera.setLocation(0, 0, - 1000);//相机往前移动,图像变小
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布
Path描述了绘制路径,用它可以完成很多复杂的图形绘制。
我们再来看看Path里的方法。
例如:addCircle(float x, float y, float radius, Direction dir)
public void addCircle(float x, float y, float radius, Direction dir) {
isSimplePath = false;
native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
}
该方法的参数含义:
其他的方法都是这个方法类似。
直线
//从当前位置,向目标位置画一条直线,该方法使用相对于原点的绝对坐标
public void lineTo(float x, float y) {
isSimplePath = false;
native_lineTo(mNativePath, x, y);
}
//从当前位置,向目标位置画一条直线,该方法使用相对于当前位置的相对坐标
public void rLineTo(float dx, float dy) {
isSimplePath = false;
native_rLineTo(mNativePath, dx, dy);
}
当前位置:当前位置指的是最后一次盗用Path的方法的终点位置,初始原点为(0, 0)
这里说到当前位置,我们再提一个方法Path.moveTo(float x, float y),它可以移动当前位置到一个新的位置。
举例
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
path.lineTo(300, 400);// 由当前位置 (0, 0) 向 (300, 400) 画一条直线
path.rLineTo(400, 0);// 由当前位置 (300, 400) 向正右方400像素的位置画一条直线
canvas.drawPath(path, paint);
贝塞尔曲线
贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。简单来说,贝塞尔曲线就是将任意一条曲线转换为精确的数学公式。
在贝塞尔曲线中,有两类点:
一阶贝塞尔曲线
B(t)为时间为t时的坐标,P0为起点,P1为终点。
二阶贝塞尔曲线
三阶贝塞尔曲线
贝塞尔曲线的模拟可以使用bezier-curve
我们再来看看Path类提供的关于贝塞尔曲线的方法。
//二阶贝塞尔曲线,绝对坐标,(x1, y1)表示控制点,(x2, y2)表示终点
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
native_quadTo(mNativePath, x1, y1, x2, y2);
}
//二阶贝塞尔曲线,相对坐标
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
isSimplePath = false;
native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}
//三阶贝塞尔曲线,绝对坐标,(x1, y1)、(x2, y2)表示控制点,(x3, y3)表示终点
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
//三阶贝塞尔曲线,相对坐标
public void rCubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
我们来用贝塞尔曲线实现一个杯中倒水效果。
举例
/**
* 控制点的X坐标不断左右移动,形成波浪效果。
* <p>
* For more information, you can visit https://github.com/guoxiaoxing or contact me by
* guoxiaoxingse@163.com
*
* @author guoxiaoxing
* @since 2017/9/11 下午6:11
*/
public class WaveView extends View {
private static final String TAG = "WaveView";
/**
* 波浪从屏幕外开始,在屏幕外结束,这样效果更真实
*/
private static final float EXTRA_DISTANCE = 200;
private Path mPath;
private Paint mPaint;
/**
* 控件宽高
*/
private int mWidth;
private int mHeight;
/**
* 控制点坐标
*/
private float mControlX;
private float mControlY;
/**
* 波浪峰值
*/
private float mWaveY;
/**
* 是否移动控制点
*/
private boolean mMoveControl = true;
public WaveView(Context context) {
super(context);
init();
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mControlY = mHeight - mHeight / 8;
mWaveY = mHeight - mHeight / 32;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//波浪从屏幕外开始,效果更真实
mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
//二阶贝塞尔曲线
mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
//闭合曲线
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mPaint);
//mControlX坐标在 -EXTRA_DISTANCE ~ mWidth + EXTRA_DISTANCE 范围内,先自增再自减,左右移动
//形成波浪效果
if (mControlX <= -EXTRA_DISTANCE) {
mMoveControl = true;
} else if (mControlX >= mWidth + EXTRA_DISTANCE) {
mMoveControl = false;
}
mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;
//水面不断上升
if (mControlY >= 0) {
mControlY -= 2;
mWaveY -= 2;
}
Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);
mPath.reset();
invalidate();
}
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.parseColor("#4CAF50"));
}
}
弧线
//画弧线
public void arcTo(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean forceMoveTo) {
isSimplePath = false;
native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}
我们来看看这个方法的参数:
注:可以发现,这个方法与同样用来画弧线的方法Canvas.drawArc()少了个boolean useCenter参数,这是因为arcTo()方法只用来画弧线。
public void setFillType(FillType ft) - 设置填充方式
方法用来设置填充方式,填充的方式有四种:
WINDING:non-zero winding rule,非零环绕数原则,该原则基于所有图形的绘制都有绘制方向(前面提到的Direction描述的顺时针与逆向时针),对于平面上的任意一点,向任意方向射出一条射线,射线遇到每个顺时针 的交点则加1,遇到逆时针的交点则减1,最后的结果如果不为0,则认为该点在图形内部,染色。如果结果为0,则认为该点在图形外部,不染色。
EVEN_ODD:even-odd rule,奇偶原则,对于平面上的任意一点,向任意方向射出一条射线,这条射线与图形相交(不是相切)的次数为奇数则说明这个点在图形内部,则进行染色。若为偶数则认为在图形外部,不进行染色。 这是一中交叉染色的情况。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8