public static float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
复制代码
咱们在绘制自定义View的过程当中不可避免的常常会接触到三角函数,现提供一个通用的获取X,Y点的代码, 要注意的是咱们绘制的时候0°是三点钟方向而非传统认知的12点钟方向,毕竟View的坐标系默认是以左上角为原点的。 android
float cos = (float) Math.cos(Math.toRadians(angle));
float sin = (float) Math.sin(Math.toRadians(angle));
复制代码
这里注意咱们的角度都以默认0°为起始点进行相加,若是画线的话则为:canvas
canvas.drawLine(getWidth()/2,getHeight() / 2,
(float) Math.cos(Math.toRadians(angle)) * RADIUS, //RADIUS为半径,angle为角度,图示角度为90+90+90+60=240
(float) Math.sin(Math.toRadians(angle)) * RADIUS,
paint);
复制代码
能够运用Xfermode绘制出多种重叠,交集等效果 如图: app
public class XfermodeView extends View {
private static final float RADIUS = DisplayUtil.dp2px(100);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap;
public XfermodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
bitmap = getBitmap((int) RADIUS * 2);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.parseColor("#3F51B5"));
canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);
canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);
}
Bitmap getBitmap(int width) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
options.inJustDecodeBounds = false;
options.inDensity = options.outWidth;
options.inTargetDensity = width;
return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
}
}
复制代码
使用Xfermode以后的效果: ide
public class XfermodeView extends View {
private static final float RADIUS = DisplayUtil.dp2px(100);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
RectF savedArea = new RectF();
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
Bitmap bitmap;
public XfermodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
bitmap = getBitmap((int) RADIUS * 2);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.parseColor("#3F51B5"));
int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存状态 开启离屏缓冲
canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);//DST
paint.setXfermode(xfermode);
canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);//SRC
paint.setXfermode(null);
canvas.restoreToCount(saved);
}
Bitmap getBitmap(int width) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
options.inJustDecodeBounds = false;
options.inDensity = options.outWidth;
options.inTargetDensity = width;
return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
}
}
复制代码
能够看到模式设置为了SRC_IN,则最终效果为取SRC和DST的交集同时显示SRC的交集部分。函数
使用Xfermode须要注意的点:绘制以前使用离屏缓冲保存画布状态,绘制以后还原。开启离屏缓冲的缘由是若是不开启那么是Xfermode是没效果的,由于默认的DST蒙版将会被认为是整个View。性能
int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存状态 开启离屏缓冲
...
canvas.restoreToCount(saved);
复制代码
设置离屏缓冲时还能够指定裁取的大小,防止性能的浪费。ui
RectF savedArea = new RectF();
savedArea.set(left, top, right, bottom);
int saved = canvas.saveLayer(savedArea, paint);
复制代码
Tips:设置setXfermode以前绘制的是DST,后绘制的是SRC(图例SRC是图片,DST是绘制的圆形,采用SRC_IN,最终取交集而且显示SRC图片的内容),具体的效果图能够参考下图:spa
中心点的肯定:设计
须要注意的是文字的X,Y起始点并非左上角而是左下角。3d
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 , paint);
复制代码
咱们能够经过:
paint.setTextAlign(Paint.Align.CENTER);
复制代码
来设置文字的中心点,如此设置以后X的起始点就为你所定义的位置了。 可是绘制事后文字是会偏上的,由于默认的点为BaseLine,咱们须要将文字下移偏移量才是一个真正的中点值。
paint.setTextSize(DisplayUtil.dp2px(50));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
float offset = (rect.top + rect.bottom) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
复制代码
这样减去偏移量文字将会下移为真正的中点,可是注意这种方法是基于BaseLine的,因此当文字肯定不会改变的时候用这种方式比较合适。
会改变的文字用:
Paint.FontMetrics fontMetrics = new Paint.FontMetrics();
paint.getFontMetrics(fontMetrics);
paint.setTextSize(DisplayUtil.dp2px(100));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
复制代码
这种方式不会随着文字的BaseLine而改变,防止由于文字改变可能出现的跳跃问题。
须要减去左边的文字默认间距,以下:
// 绘制文字左对齐
paint.setTextAlign(Paint.Align.LEFT);
Rect rect = new Rect();
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
canvas.drawText("abcd", 0 - rect.left, 300, paint);
复制代码
若是是仅仅多行绘制那么很是简单,直接使用StaticLayout就能够了:
{
staticLayout = new StaticLayout(text, textPaint, 600, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
//StaticLayout 并非一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,
// 它是纯粹用来绘制文字的。 StaticLayout 支持换行,它既能够为文字设置宽度上限来让文字自动换行,也会在 \\n 处主动换行。
//参数说明
//width 是文字区域的宽度,文字到达这个宽度后就会自动换行;
//align 是文字的对齐方向;
//spacingmult 是行间距的倍数,一般状况下填 1 就好;
//spacingadd 是行间距的额外增长值,一般状况下填 0 就好;
//includeadd 是指是否在文字上下添加额外的空间,来避免某些太高的字符的绘制出现越界。
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 使用 StaticLayout 代替 Canvas.drawText() 来绘制文字,
// 以绘制出带有换行的文字
canvas.save();
canvas.translate(50, 40);
staticLayout.draw(canvas);
canvas.restore();
}
复制代码
文字的精确折行
通常用于跟图片相交的需求使用: 主要是两个API的使用:
int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
//截取字符串。
//参数为绘制的文字,开始字符截取的字符,终止字符,是否顺时,截取的宽度,保存截取的宽度
canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
//第二和第三个参数为字符串的起始点和终止点
复制代码
咱们的中心思想就是每次用breakText计算出当次的起点文字到终点文字的长度,根据长度计算出文字的终止点是哪里,而后用drawText根据起止点和终止点截取文字并绘制。 下边以一个实例来讲明,上代码:
public class ImageTextView extends View {
private static final float IMAGE_WIDTH = DisplayUtil.dp2px(120);
private static final float IMAGE_Y = DisplayUtil.dp2px(50);
private boolean isLeft = true;
private boolean isInImage = false;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap;
Paint.FontMetrics fontMetrics = new Paint.FontMetrics();
String text = "This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text.";
float[] cutWidth = new float[1];
public ImageTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
bitmap = getAvatar((int) IMAGE_WIDTH);
paint.setTextSize(DisplayUtil.dp2px(14));
paint.getFontMetrics(fontMetrics);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制文字
canvas.drawBitmap(bitmap, getWidth() / 2 - IMAGE_WIDTH / 2, IMAGE_Y, paint);
int length = text.length();
float verticalOffset = -fontMetrics.top;
for (int start = 0; start < length; ) {
int maxWidth;
float textTop = verticalOffset + fontMetrics.top;
float textBottom = verticalOffset + fontMetrics.bottom;
//判断是否在图片区域内
if (textTop > IMAGE_Y && textTop < IMAGE_Y + IMAGE_WIDTH
|| textBottom > IMAGE_Y && textBottom < IMAGE_Y + IMAGE_WIDTH) {
// 文字和图片在同一行,减去图片的宽度
isInImage = true;
maxWidth = (int) (getWidth() / 2 - IMAGE_WIDTH / 2);
} else {
isInImage = false;
// 文字和图片不在同一行
maxWidth = getWidth();
}
int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
if (isInImage) {//若是是图片显示区域内
if (isLeft) { //在图片左边
isLeft = false;
canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
} else { //在图片右边
isLeft = true;
canvas.drawText(text, start, start + count, getWidth() / 2 + IMAGE_WIDTH / 2, verticalOffset, paint);
verticalOffset += paint.getFontSpacing(); //再右边才换行
}
} else {
canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
verticalOffset += paint.getFontSpacing(); //换行
}
start += count;
}
}
Bitmap getAvatar(int width) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
options.inJustDecodeBounds = false;
options.inDensity = options.outWidth;
options.inTargetDensity = width;
return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
}
}
复制代码
如上,此例咱们整体的思路就是判断当前文字是否在图片的显示高度之类,若是在图片高度范围以内则作截取字符的处理,在判断是否超太高度的时候能够根据本身的逻辑来设置,这里只是提供一种思路,实际状况能够根据本身的需求计算处理。
当进行裁剪以后你绘制的部分只能是在你绘制的部分中被显示出来,以下所示:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.clipRect(0, 0, 100, 100);
canvas.drawBitmap(bitmap, 0, 0, paint);
}
复制代码
能够看到只绘制了被切割矩形的部分。
Tips:这里还须要注意的一点是当进行了clipPath操做以后画笔的抗锯齿效果就会无效了,画出的东西颇有多是带有毛边的,好比用clipPath切割一个圆形而后绘制一个圆形的头像,在这种状况下就能够考虑Xfermode而非clipPath了。
canvas.rotate(degree);
canvas.translate(x,y);
canvas.scale(x,y);
canvas.skew(x,y);
这里只须要注意一点,canvas的变换是改变的坐标系起始点也就是左边系的原点,好比当我调用tranlate以后在调用rotate的状况是这样的:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
bitmap = getAvatar(100);
canvas.translate(200,200);
canvas.rotate(45);
canvas.drawBitmap(bitmap, 0, 0, paint);
}
复制代码
Tips:须要注意的一点是一般进行canvas的变换或裁剪以前都须要调用canvas.save()去保存canvas状态,绘制完成以后调用canvas.restore()去还原canvas的坐标,若是不还原的话以后的绘制都会以你改变后的原点为基础进行绘制。