1.不用傳入Context引數的DP轉PX,在安卓中進行繪製最後顯示都是以PX為單位的,所以我們一般需要用將設計圖上的DP轉為PX。
public static float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
複製程式碼
2.三角函式獲取座標值 通用程式碼
我們在繪製自定義View的過程中不可避免的經常會接觸到三角函式,現提供一個通用的獲取X,Y點的程式碼, 要注意的是我們繪製的時候0°是三點鐘方向而非傳統認知的12點鐘方向,畢竟View的座標系預設是以左上角為原點的。
據圖所示我們最終的X,Y點其實就是(cos * 半徑,sin * 半徑),化為程式碼就為:float cos = (float) Math.cos(Math.toRadians(angle));
float sin = (float) Math.sin(Math.toRadians(angle));
複製程式碼
這裡注意我們的角度都以預設0°為起始點進行相加,如果畫線的話則為:
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);
複製程式碼
3.Xfermode的使用
可以運用Xfermode繪製出多種重疊,交集等效果 如圖:
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之後的效果:
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); 複製程式碼
-
設定離屏緩衝時還可以指定裁取的大小,防止效能的浪費。
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圖片的內容),具體的效果圖可以參考下圖:
4.文字的繪製
中心點的確定:
需要注意的是文字的X,Y起始點並不是左上角而是左下角。
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而改變,防止因為文字改變可能出現的跳躍問題。
5.文字的左對齊
需要減去左邊的文字預設間距,如下:
// 繪製文字左對齊
paint.setTextAlign(Paint.Align.LEFT);
Rect rect = new Rect();
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
canvas.drawText("abcd", 0 - rect.left, 300, paint);
複製程式碼
6.文字的多行繪製
-
如果是僅僅多行繪製那麼非常簡單,直接使用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的使用:
- paint.breakText();
- canvas.drawText();
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);
}
}
複製程式碼
如上,此例我們總體的思路就是判斷當前文字是否在圖片的顯示高度之類,如果在圖片高度範圍之內則做擷取字元的處理,在判斷是否超過高度的時候可以根據自己的邏輯來設定,這裡只是提供一種思路,實際情況可以根據自己的需求計算處理。
7.canvas的裁剪和變換
canvas的裁剪主要有4個API:
- canvas.clipRect();
- canvas.clipPath();
- canvas.clipOutPath();
- canvas.clipOutRect();
當進行裁剪之後你繪製的部分只能是在你繪製的部分中被顯示出來,如下所示:
@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的變換:
-
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);
}
複製程式碼
可以看到最後繪製是在0,0點繪製的bitmap圖片,也就是說我們每次對座標系的操作其實都是操作的左邊系的原點。
Tips:需要注意的一點是通常進行canvas的變換或裁剪之前都需要呼叫canvas.save()去儲存canvas狀態,繪製完成之後呼叫canvas.restore()去還原canvas的座標,如果不還原的話之後的繪製都會以你改變後的原點為基礎進行繪製。