关于做者html
郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工做,欢迎交流技术方面的问题,能够去个人Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。java
文章目录android
第一次阅览本系列文章,请参见导读,更多文章请参见文章目录。git
文章源码程序员
本文还提供了三个综合性的完整实例来辅助理解。github
第一次阅览本系列文章,请参见导读,更多文章请参见文章目录。算法
本篇文章咱们来分析View绘制方面的实践。canvas
一个简单的自定义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);复制代码
位图
@NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
@Nullable Paint 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,奇偶原则,对于平面上的任意一点,向任意方向射出一条射线,这条射线与图形相交(不是相切)的次数为奇数则说明这个点在图形内部,则进行染色。若为偶数则认为在图形外部,不进行染色。
这是一中交叉染色的状况。