Android绘图(一)基础篇
目錄
- 一、繪圖入門
- 二、繪制位圖
- 三、繪制點
- 四、繪制直線
- 五、繪制矩形
- 六、繪制圓
- 七、繪制路徑
-
- 7.1 往 Path 中添加線條
- 7.2 往 Path 中添加矩形、橢圓、弧
- 7.3 往 Path 中添加曲線和貝塞爾曲線
- 7.4 將 Path 中的圖形進行運算
- 7.5 繪制文字
-
- 7.5.1 在指定位置繪制文本
- 八、Paint的FontMetrics使用
-
- 8.1 行距
- 8.2 計算fontPadding
- 8.3文本在控件水平方向居中顯示
- 8.4 文本在控件垂直方向居中顯示
- 8.5 Paint的setTextAlign使用
- 九、繪制驗證碼
一、繪圖入門
Canvas 翻譯為“畫布”,可以理解成畫家作畫時的宣紙。Canvas 供了若干方法用于繪制各種圖形圖案——點、線、圓等等。Paint 翻譯為“畫筆”,為繪圖定義各種參數——顏色、線條樣式、圖案樣式等等。通常的繪圖思路是先定義 Paint 對象,指定繪圖參數,再通過 Canvas 對象進行圖 形繪制,繪圖的結果因 Paint 的不同而不同。Paint 類用于定義繪圖時的參數,主要包含顏色、文本、圖形樣式、位圖模式、濾鏡等幾個方面。
顏色是指繪圖時使用的顏色,在 Android 中顏色可以指定透明度,使用 16 進制來表示顏色時,格式通常為#AARRGGBB,其中,AA 表示透明度、RR 表示紅色、GG 表示綠色、BB 表示藍色,Color 類定義了顏色信息,內置了常用顏色的 int 型常量,比如 Color.RED 是紅色,Color.BLUE 是藍色……如果您習慣了 16 進制的顏色,Color 類的靜態方法 parseColor(String colorString)可以將 16進制顏色轉換成 Color 類型。
先來看看簡單的使用
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ImageView iv = findViewById(R.id.iv1);// 創建空白的bitmapBitmap bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888);// 新建畫布,關聯bitmapCanvas canvas = new Canvas(bitmap);// 繪制黑色背景canvas.drawColor(Color.BLACK);// 定義畫筆Paint paint = new Paint();// 設置畫筆顏色paint.setColor(Color.WHITE);paint.setAntiAlias(true);// 畫筆實心paint.setStyle(Paint.Style.FILL);// 文本水平居左對齊paint.setTextAlign(Paint.Align.LEFT);// 字體大小設置paint.setTextSize(32f);paint.setTextSkewX(0.5f); // 設置文本傾斜度,取值0~1,正負表示傾斜方向paint.setUnderlineText(true);// 設置下劃線paint.setFakeBoldText(true); // 設置粗體// 繪制文字canvas.drawText("hello 你好啊!", 10, 100, paint);// 繪制圖形// 設置畫筆空心樣式paint.setStyle(Paint.Style.STROKE);paint.setColor(Color.RED);// 設置畫筆描邊的邊框寬度paint.setStrokeWidth(20);// 設置畫筆連接處的形狀paint.setStrokeJoin(Paint.Join.BEVEL);// 繪制矩形canvas.drawRect(new Rect(20, 200, 350, 350), paint);//顯示bitmap到ImageView中iv.setImageBitmap(bitmap);
}
效果圖:
二、繪制位圖
常用方法
// bitmap 繪制在畫布上,同時指定位圖左上角相對于畫布的坐標,大小與原位置相同,不進行任何縮放。
public void drawBitmap( Bitmap bitmap, float left, float top, Paint paint)
// 下面兩個方法從源bitmap中摳出一塊大小區域為src的圖片并繪制到canvas的dst處。src和ds 的大小與比例關系影響到最終的繪制效果,這個過程是自動縮放以適應dest區域的
public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
ps:繪制位圖時,除非需要進行位圖運算,否則,并不需要指定 paint 對象,直接傳遞null 即可。
案例-使用drawBitmap方法在ImageView上繪制2個位圖,一個按原始大小繪制,另一個則按2倍大小繪制
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ImageView iv = findViewById(R.id.iv);// 創建空白位圖Bitmap bitmap = Bitmap.createBitmap(500, 800, Bitmap.Config.ARGB_8888);// 創建畫布并關聯位圖Canvas canvas = new Canvas(bitmap);// 繪制背景色,便以區分canvas.drawColor(Color.BLACK);// 獲取資源目錄下的原圖Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);// 繪制原圖canvas.drawBitmap(sourceBitmap, 0, 0, null);// 獲取原圖寬高int bmpWidth = sourceBitmap.getWidth();int bmpHeight = sourceBitmap.getHeight();// 創建原圖的矩形Rect src = new Rect(0, 0, bmpWidth, bmpHeight);// 創建放大后的矩形,位于原圖下方Rect dest = new Rect(0, bmpHeight, bmpWidth * 2, bmpHeight + bmpHeight * 2);// 繪制放大后的位圖canvas.drawBitmap(sourceBitmap, src, dest, null);// 顯示到ImageView上iv.setImageBitmap(bitmap);
}
效果圖如下:
三、繪制點
點的大小取決于 setStrokeWidth()方法的參數,參數值越大,點也就越大。所以,不要以為一個點就是屏幕上的一個像素。如果將 stroke 的寬度設置為足夠大,我們發現最終繪制出來的點其實是一個正方形。繪制點的方法一共有三個:
// 該方法在(x,y)處繪制一個點。
public void drawPoint(float x, float y, Paint paint)
// 該方法的參數 pts 是一個數組,從下標 0 開始每 2 個數確定一個點,連續繪制多個點。多余的元素會忽略。
public void drawPoints(float[] pts, Paint paint)
// 從 pts 數組中的第 offset 處開始取出 count 個數字,以 2 個數為一組確實一個點,連 續繪制若干個點。忽略多余的元素
public void drawPoints(float[] pts, int offset, int count, Paint paint)
示例如下:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);// 繪制背景canvas.drawColor(Color.BLACK);//畫一個紅色的點Paint paint = new Paint();paint.setColor(Color.RED);paint.setStrokeWidth(10);canvas.drawPoint(120, 20, paint);// 畫一組藍色的點paint.setColor(Color.BLUE);float[] points = new float[]{10, 10, 50, 50, 50, 100, 50, 150};canvas.drawPoints(points, paint);// 畫一組綠色的點paint.setColor(Color.GREEN);points = new float[]{20, 20, 60, 60, 60, 80, 60, 180};canvas.drawPoints(points, 3, 4, paint);// 顯示在ImageView上iv.setImageBitmap(bitmap);
}
效果圖:
四、繪制直線
兩個點確定一條直線,所以,繪制線條時,需要指定兩個點的坐標。同畫點一樣,繪制線條也有 3 個重載的方法:
// 在(startX,startY)和(stopX,stopY)兩個點之間繪制一條直線。
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
// pts 數組中每4個數一組繪制一條直線,多余的元素會忽略。
public void drawLines(float[] pts, Paint paint)
// 從 pts 數組中的 offset 索引處開始,取出 count 個元素,并以 4 個數一組繪制直線,忽略多余的元素。
public void drawLines(float[] pts, int offset,int count, Paint paint)
五、繪制矩形
繪制矩形時,參數分為兩種:一種是指定 left、top、right、bottom 等 4 個參數,另一種直接指定一個 Rect 對象或 RectF 對象。繪制直角矩形的三個重載方法如下:
public void drawRect(float left,float top, float right,float bottom,Paint paint)public void drawRect(Rect r,Paint paint)public void drawRect(RectF r,Paint paint)
圓角矩形的幾何形狀比直角矩形相對復雜一些,我們需要指定 4 個拐角的弧度,4 個角的弧度不能單獨設置,而是統一設置為相同的值。拐角弧度實際上是圓或橢圓的一段弧線,如圖所示:
繪制圓角矩形一共有 2 個重載的方法:
// 該方法用于繪制一個圓角矩形,left、top、right、bottom 構建一個矩形,rx、ry 分別是圓角處的水平半徑和垂直半徑。rx 和 ry 不一定相同,如果不同,則是橢圓上的一段弧線。
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,Paint paint)
// 該方法是上面方法的另一種重載形式。
public void drawRoundRect(RectF rect,float rx,float ry,Paint paint)
下面的代碼繪制了三個矩形,一個直角矩形,一個為空心圓角矩形,另一個是有填充顏色的實心圓角矩形:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main5);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);// 該方法用于設置落筆時的樣式,控制我們的畫筆在離開畫板時留下的最后一點圖形paint.setStrokeCap(Paint.Cap.ROUND);// 當繪圖樣式為 STROKE 時,該方法用于指定線條連接處的拐角樣式,能使繪制的圖形更加平滑paint.setStrokeJoin(Paint.Join.ROUND);paint.setColor(Color.RED);// 繪制紅色的直角矩形canvas.drawRect(new Rect(0, 0, 100, 100), paint);// 繪制綠色的圓角矩形paint.setColor(Color.GREEN);canvas.drawRoundRect(new RectF(100, 100, 200, 200), 20, 20, paint);// 繪制藍色的圓角矩形(空心)paint.setColor(Color.BLUE);paint.setStyle(Paint.Style.STROKE);paint.setStrokeWidth(5);// 圓角的半徑可以不一樣canvas.drawRoundRect(new RectF(200, 200, 400, 400), 10, 20, paint);iv.setImageBitmap(bitmap);
}
效果圖:
注意: 繪制圓角矩形邊框的時候,需要考慮邊框的寬度,也就是paint的setStrokeWidth設置的大小,如果canvas從(0,0)左上角開始繪制圓角矩形邊框, 那么canvas.drawRoundRect的4個矩形頂點坐標需要往里面縮進邊框寬度的一半,否則繪制的區域在canvas之外是看不到的,導致繪制的邊框寬度看起來要比預想的要小一半,并且4個圓角看起來會比非圓角部分的線條要粗一點.
六、繪制圓
在對圖形進行分類時,我將圓、橢圓、扇形、弧線統一歸類到“圓”這一類中,扇形和弧線可以認為是圓或橢圓的一部分,橢圓的大小是由他的外切矩形來決定的,這實際上和幾何學中的定義完全一致,如圖所示:
繪制橢圓的方法如下:
public void drawOval(float left, float top, float right, float bottom, Paint paint)public void drawOval(RectF oval, Paint paint)
繪制橢圓時,如果外切矩形的長和寬相等,即為正方形,繪制出來的圖形就是一個正圓,但 是 Cavnas 類供了另一個更加簡單實用的方法,供圓點的坐標和半徑即可。
//(cx、cy)為圓心坐標,radius 為圓的半徑
public void drawCircle(float cx, float cy, float radius, Paint paint)
弧線和扇形本質上更是相似,弧線是橢圓上的一段,而扇形則是將弧線的兩個端點和橢圓中心點使用線條連接形成的閉合區域。理解弧線和扇形的演變過程便很容易明白方法中的參數意義,如下圖所示:
繪制弧線和扇形的方法如下:
// 參數oval是規定橢圓的范圍, startAngle 表示起始角度,sweepAngle 表示扇形或弧線所占的角度,
// 正數表示順時針,負數表示逆時針,useCenter 參數詢問是否要使用中心點,為true 表示扇形,為 false 表示弧線
public void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint)public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle,boolean useCenter,Paint paint)
下面的代碼演示了弧線和扇形的繪制方法,采用了 Style.STROKE 的圖形模式,如果將 Style設置為 Style.FILL,不管是弧線還是扇形,都可以使用顏色進行填充
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main6);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);paint.setStrokeWidth(5);paint.setStrokeJoin(Paint.Join.ROUND);paint.setStrokeCap(Paint.Cap.ROUND);// 空心模式paint.setStyle(Paint.Style.STROKE);RectF oval = new RectF(100, 100, 400, 200);// 繪制橢圓paint.setColor(Color.GRAY);canvas.drawOval(oval, paint);// 繪制圓弧,起始角度90度(也就是y軸原點),掃過45度(也就是8點中方向)paint.setColor(Color.RED);canvas.drawArc(oval,90,45,false,paint);// 繪制扇形,起始角度是0度(也就是在x軸原點),掃過-45度(也就是3點鐘方向,因為Android坐標系是向下的)paint.setColor(Color.GREEN);canvas.drawArc(oval,0,-45,true,paint);iv.setImageBitmap(bitmap);
}
效果圖:
七、繪制路徑
Path 是 Graphics2D 中一個非常重要的概念,表示“路徑”,理解該概念時保持“路徑”的本色就好。路徑可以是直的、也可以是彎的,可以是閉合的、也可以是非閉合的,可以是圓形的、也可以是方形的,可以是單個的、也可以是多個的,可以是簡單的、也可以是復雜的……總的來說,路徑是基于普通圖形但是功能比普通圖形更強的一種復雜圖形。
Path 是一個類,用于繪制復雜圖形,創建之初什么也沒有,只有往 Path 中添加了具體的形狀,Path 才會清晰可見。繪制 Path 時,所有信息都存儲在 Path 對象中,Canvas 根據 Path 對象來繪制相應的圖形。
我們將 Path 的功能歸納成以下幾類:
7.1 往 Path 中添加線條
通過 Path 可以繪制出奇形怪狀的線條,并能將線條組合在一起變成折線,閉合后就是一個多邊形了。這就是 Path 的厲害之處。為此,Path 類中定義了 5 個方法
// 將畫筆移動到點(x,y)的位置,使用的是絕對定位
public void moveTo(float x,float y)
// 將畫筆移動到一個新點,新點在上一個點的基礎上偏移(dx,dy),也就是說,新點的坐標為(x+dx,y+dy)。這里使用的是相對定位。首字母“r”就是“relative(相對)”的意思
public void rMoveTo(float dx,float dy)
// 將畫筆連接到點(x,y)的位置,并在上一個點與當前點之前畫一條直線。使用的是絕對定位。
public void lineTo(float x,float y)
// 將畫筆移動到一個新點,新點在上一個點的基礎上偏移(dx,dy),新點的坐標為(x+dx,y+dy),同時,在新點與上一個點之間畫一條直線。這里使用的是相對定位。
public void rLineTo(float dx,float dy)
// 在第一個點和最后一個點之前畫一條直線,形成閉合區域。
public void close()
下面的案例使用 Path 繪制了一個五角星,這不是一個完美的五角星幾何圖形,因為五個點的坐標并沒有正確計算出來,只是算了個大概。首先調用了 moveTo(0, 150)定義好這次繪圖的起 點,接下來調用 rLineTo()方法通過相對定位計算出下一個點的坐標,并使用直線連接,最后,調用 close()方法連接最后一點和第一個點以形成閉合區域。
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main7);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);paint.setColor(Color.WHITE);paint.setStyle(Paint.Style.STROKE);paint.setStrokeWidth(5);paint.setStrokeCap(Paint.Cap.ROUND);paint.setStrokeJoin(Paint.Join.ROUND);// 創建路徑Path path = new Path();path.moveTo(0, 150);path.rLineTo(300, 0);path.rLineTo(-300, 150);path.rLineTo(150, -300);path.rLineTo(150, 300);path.close();// 連接最后一點和第一個點以形成閉合區域// 繪制路徑canvas.drawPath(path, paint);iv.setImageBitmap(bitmap);
}
效果圖:
7.2 往 Path 中添加矩形、橢圓、弧
如果要往 Path 對象中添加矩形、橢圓、圓和弧,需要調用 Path 類中定義的一組以“add”開 頭的方法,這組方法有些需要傳遞一個類型為 Path.Direction 的參數,這是一個枚舉類型,枚舉值 CW 表示順時針,CCW 表示逆時針,下一節內容沿著圖形繪制文字時,我們可以清晰地感受到方向對繪圖帶來的影響。
// 往 Path 對象中添加一個矩形
public void addRect(RectF rect,Path.Direction dir)public void addRect(float left,float top,float right,float bottom, Path.Direction dir)
// Path 對象中添加一個圓角矩形。該方法和前面繪制圓角矩形相比在定義四個角的弧線大小時功能更強,能對四個角分別定義不同的弧線弧度
public void addRoundRect(RectF rect,float[] radii, Path.Direction dir)public void addRoundRect(RectF rect,float rx,float ry,Path.Direction dir)public void addRoundRect(float left, float top, float right, float bottom, float[] radii,Path.Direction dir)
// Path 對象中添加一個橢圓。
public void addOval(RectF oval, Path.Direction dir)public void addOval(float left, float top, float right, float bottom, Path.Direction dir)
// Path 對象中添加一個圓。
public void addCircle(float x, float y, float radius, Path.Direction dir)
// Path 對象中添加一段弧。本方法并沒有指定方向,因為角度的正負已經代表了方向,正數為順時針,負數為逆時針。
public void addArc(RectF oval, float startAngle, float sweepAngle)
public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)
我們在下面的代碼中繪制了一個 Path 對象,對象中同時包含了矩形、圓角矩形、橢圓、圓、弧線等圖形,顯然,Path 對象繪制出來的圖形更加復雜了。
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main8);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 620, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setStrokeWidth(5);paint.setColor(Color.RED);Path path = new Path();// 繪制矩形path.addRect(new RectF(10, 10, 300, 100), Path.Direction.CCW);// 繪制圓角矩形,4個角的弧度都不一樣,2個數確定一個弧度path.addRoundRect(new RectF(10, 120, 300, 220),new float[]{10, 20, 20, 10, 30, 40, 40, 30},Path.Direction.CCW);// 橢圓path.addOval(new RectF(10, 240, 300, 340), Path.Direction.CCW);// 圓path.addCircle(60, 390, 50, Path.Direction.CCW);// 弧線path.addArc(new RectF(10, 500, 300, 600), -30, -60);// 繪制路徑canvas.drawPath(path,paint);iv.setImageBitmap(bitmap);
}
效果圖:
7.3 往 Path 中添加曲線和貝塞爾曲線
曲線包括弧線和貝塞爾曲線,與前面講的矩形、圓或弧線不同,繪制曲線時需要確定一個起點,繪制的曲線會與該起點進行連接,形成一個更加復雜的圖形。
貝塞爾曲線(Bézier curve)是圖形開發中的一個重要工具,通過三個點的,確定一條平滑的曲線,又稱貝茲曲線或貝濟埃曲線,是應用于二維圖形應用程序的數學曲線。一般的矢量圖形軟件通過它來精確畫出曲線,貝茲曲線由線段與節點組成,節點是可拖動的支點,線段像可伸縮的皮筋,我們在繪圖工具上看到的鋼筆工具就是來做這種矢量曲線的。貝塞爾曲線是計算機圖形學中相當重要的參數曲線,在一些比較成熟的位圖軟件中也有貝塞爾曲線工具。貝塞爾曲線又分為一階貝塞爾曲線、二階貝塞爾曲線、三階貝塞爾曲線和高階貝塞爾曲線,一階貝塞爾曲線就是一條線段,Path 類支持二階貝塞爾曲線和三階貝塞爾曲線。如下圖所示分別是: 一階、二階、三階
前面提到,貝塞爾曲線通過 3 個點來繪制一條平滑的曲線,這 3 個點分別是起點、控制點和終點。比如,如果要繪制一條二階貝塞爾曲線,必須調用 moveTo()方法定義起點,再調用如下方法繪制貝塞爾曲線
// (x1,y1)是控制點,(x2,y2)是終點。
public void quadTo(float x1,float y1,float x2,float y2)
我們通過一段代碼演示如何繪制貝塞爾曲線
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main9);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 400, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setStrokeWidth(5);paint.setColor(Color.RED);Path path = new Path();path.moveTo(100, 100);path.quadTo(200, 50, 300, 300);canvas.drawPath(path, paint);// 畫點(起點100,100,控制點200,50,終點300,300)paint.setColor(Color.GREEN);canvas.drawPoints(new float[]{100, 100, 200, 50, 300, 300}, paint);// 添加文案paint.setStrokeWidth(2);paint.setTextSize(22);canvas.drawText("起點",90,140,paint);canvas.drawText("控制點",220,55,paint);canvas.drawText("終點",280,340,paint);iv.setImageBitmap(bitmap);
}
效果圖:
三階貝塞爾曲線有 1 個起點,2 個控制點,1 個終點,Path 類中通過下面方法進行繪制
// (x1、y1)、(x2、y2)是控制點,(x3、y3)是終點
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
quadTo()和 cubicTo()的控制點和終點利用絕對定位來進行確定,其實還有另外兩個方法,通過相對定位對各點進行定義:
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
添加曲線可以使用arcTo()方法,arcTo()方法可以和 moveTo()配合使用,通過 moveTo()確定一個起點,再通過 arcTo()繪制弧線。弧線是基于矩形的內切圓上的一段,該弧線的起始點會和 moveTo()方法定義的點進行連接。
public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
public void arcTo(RectF oval, float startAngle, float sweepAngle)
public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
上面三個方法的參數在前面都有說明,不再贅述,參數 forceMoveTo 為 true 時,表示開始一個新的圖形,不和上一個點進行連接,為 false 時才和上一個點連接
我們通過一小段代碼來演示 artTo()方法的使用技巧:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main10);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setStrokeWidth(5);// 繪制橢圓RectF oval1 = new RectF(150, 100, 350, 250);RectF oval2 = new RectF(150, 250, 350, 400);paint.setColor(Color.GRAY);canvas.drawOval(oval1, paint);canvas.drawOval(oval2, paint);Path path = new Path();paint.setColor(Color.GREEN);path.moveTo(100, 100);// true表示不會連接到上一個點(這里上一個點剛好是起始點)path.arcTo(oval1, -30, 60, true);//forceMoveTo不傳默認是false,表示會和上一個點進行連接path.arcTo(oval2, 90, -45);canvas.drawPath(path, paint);iv.setImageBitmap(bitmap);}
效果圖:
從圖中可以看出下面的弧線和上一個點是連接起來的,它的上一個點剛好就是上一個圓弧的終點
7.4 將 Path 中的圖形進行運算
我們還可以將多個 Path 進行圖形運算,得到更加復雜和不規則的圖形。Path 有一個靜態內部類 Op,定義了 5 種運算規則:
Path.Op. DIFFERENCE:差集,圖形 A 減去與圖形 B 重疊的區域后 A 余下的區域。
Path.Op. INTERSECT:交集,圖形 A 和圖形 B 的重疊區域。
Path.Op. REVERSE_DIFFERENCE:反差集,圖形 B 減去與圖形 A 重疊的區域后 B 余下的區域。
Path.Op. UNION:并集,包含了圖形 A 和圖形 B 的所有區域。
Path.Op.XOR:補集,即圖形 A 和圖形 B 的所有區域減去他們的重疊區域后余下的區域。
我們通過以下的表格來比較這 5 種圖形運算的不同效果:
圖形A代表黑色正方形,圖形B代表紅色圓形
下面通過代碼演示,先繪制原圖查看效果:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main11);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);paint.setStrokeWidth(5);// 繪制白色矩形paint.setStyle(Paint.Style.FILL); // 修改此參數可以查看填充模式還是邊框模式paint.setColor(Color.WHITE);Path path1 = new Path();path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);canvas.drawPath(path1, paint);// 繪制紅色圓paint.setColor(Color.RED);Path path2 = new Path();path2.addCircle(100, 100, 50, Path.Direction.CCW);canvas.drawPath(path2, paint);iv.setImageBitmap(bitmap);}
效果圖如下:
然后對path進行5種效果運算演示
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main11);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(800, 500, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);paint.setStrokeWidth(5);//============原圖==========================// 繪制白色矩形paint.setStyle(Paint.Style.FILL); // 修改此參數可以查看填充模式還是邊框模式paint.setColor(Color.WHITE);Path path1 = new Path();path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);canvas.drawPath(path1, paint);// 繪制紅色圓paint.setColor(Color.RED);Path path2 = new Path();path2.addCircle(100, 100, 50, Path.Direction.CCW);canvas.drawPath(path2, paint);//============原圖==========================// =============差集計算==============// path1 差 path2 ,也就是:path1-path2paint.setColor(Color.WHITE);path1.reset();path2.reset();path1.addRect(new RectF(10, 170, 110, 270), Path.Direction.CCW);path2.addCircle(100, 260, 50, Path.Direction.CCW);// 差集運算path1.op(path2, Path.Op.DIFFERENCE);canvas.drawPath(path1,paint);// =============差集計算==============// =============交集==============// path1 交 path2 ,也就是:path1&path2paint.setColor(Color.WHITE);path1.reset();path2.reset();path1.addRect(new RectF(160, 170, 260, 270), Path.Direction.CCW);path2.addCircle(250, 260, 50, Path.Direction.CCW);// 交集運算path1.op(path2, Path.Op.INTERSECT);canvas.drawPath(path1,paint);// =============差集計算==============// =============反差集==============// path1 反差集 path2 ,也就是:path2-path1paint.setColor(Color.WHITE);path1.reset();path2.reset();path1.addRect(new RectF(260, 170, 360, 270), Path.Direction.CCW);path2.addCircle(350, 260, 50, Path.Direction.CCW);// 反差集運算path1.op(path2, Path.Op.REVERSE_DIFFERENCE);canvas.drawPath(path1,paint);// =============差集計算==============// =============并集==============// path1 并集 path2 ,也就是:path1|path2paint.setColor(Color.WHITE);path1.reset();path2.reset();path1.addRect(new RectF(450, 170, 550, 270), Path.Direction.CCW);path2.addCircle(540, 260, 50, Path.Direction.CCW);// 并集運算path1.op(path2, Path.Op.UNION);canvas.drawPath(path1,paint);// =============差集計算==============// =============補集==============// path1 補集 path2 ,也就是:path1|path2 - path1&path2paint.setColor(Color.WHITE);path1.reset();path2.reset();path1.addRect(new RectF(650, 170, 750, 270), Path.Direction.CCW);path2.addCircle(740, 260, 50, Path.Direction.CCW);// 補集運算path1.op(path2, Path.Op.XOR);canvas.drawPath(path1,paint);// =============差集計算==============iv.setImageBitmap(bitmap);}
效果圖:
7.5 繪制文字
Canvas為我們供了兩組方法,一組直接從指定的位置開始繪制文字,另一組沿著 Path 繪制文字:
public void drawText(char[] text, int index, int count, float x, float y, Paint paint)
public void drawText(String text, float x, float y, Paint paint)
public void drawText(String text, int start, int end, float x, float y, Paint paint)
public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
上面這一組方法是從指定的位置(坐標)開始繪制文字,雖然都是字符串,但是供了三種形式:char[]、String 和 CharSequence,本質上并沒有什么不同,參數 index 和count、start 和 end 可以從字符串中取出子串,而參數 x、y 就是文字繪制的坐標位置,其中 y 是文字的 baseline 的值
public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)
上面這兩個重載的 drawTextOnPath()方法用于沿著 Path 定義好的路徑繪制文字,這是一個很在趣的功能,文字在 Path 的帶領下龍飛鳳舞,靈活多變。參數 hOffset 和 vOffset 用于定義文字離 Path 的水平偏移量和垂直偏移量,正數和負數影響文字與路徑的相對位 置。同樣的,也支持繪制從字符數組中截取的子串,index 表示起始索引,count 表示要截取的長度。
下面的案例中繪制了 4 個字符串,一個繪制所有的字符串,中間兩個截取子串進行繪制,最后一個沿著 Path 繪制出所有的文字,為了更好的理解文字與路徑的關系,所以把對應的路徑也繪制出來了
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main12);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(800, 450, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setColor(Color.WHITE);paint.setStyle(Paint.Style.FILL);paint.setAntiAlias(true);paint.setTextSize(30);String text = "天王蓋地虎,寶塔鎮河妖;野雞悶頭鉆,哪能上天王山";// 1.直接繪制canvas.drawText(text, 10, 50, paint);// 2.截取數量來繪制paint.setColor(Color.RED);// start從6開始,end=11,表示取[6,11)范圍的數量canvas.drawText(text, 6, 11, 10, 100, paint);// 截取方式二paint.setColor(Color.BLUE);// index從12開始,count=5,表示從索引12開始取5個字符canvas.drawText(text.toCharArray(), 12, 5, 10, 150, paint);// 3.通過路徑繪制// 先創建路徑Path path = new Path();path.moveTo(10, 300);path.quadTo(100, 100, 700, 400);paint.setColor(Color.GREEN);// 將文字繪制到路徑上canvas.drawTextOnPath(text, path, 10, 30, paint);// 繪制路徑,方便查看paint.setStyle(Paint.Style.STROKE);paint.setStrokeWidth(2);paint.setColor(Color.RED);canvas.drawPath(path, paint);// 繪制到ImageView上iv.setImageBitmap(bitmap);
}
效果圖:
7.5.1 在指定位置繪制文本
下面2個方法都是標記了過時的方法
public void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);
public void drawPosText(@NonNull char[] text, int index, int count,@NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);
通過這2個方法可以按自定的坐標集繪制字符串或者字符數組,例如要實現一個垂直方向的文本繪制可以這樣實現
class MyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, def: Int = 0) : View(context, attrs, def) {private val paint = Paint().apply {isAntiAlias = truetextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22f, resources.displayMetrics)color = Color.RED}private val text = "月黑風高"// 每個字的x和y坐標集private val array = FloatArray(text.length * 2).apply {for (i in 0 until size step 2) {set(i, 100f) // x,水平方向每個字的x相同,都是100pxset(i + 1, (i + 1) * paint.textSize) // y,垂直方向每個字間隔一個字的大小}}override fun onDraw(canvas: Canvas?) {canvas?.drawColor(Color.BLACK)// 繪制文本canvas?.drawPosText(text, array, paint)}
}
效果圖:
八、Paint的FontMetrics使用
粉紅色就是TextView的背景色, 可以看到在Ascent和Descent之外分別還有一點距離才到TextView的邊緣, 也就是右側使用橙色方塊標出的fontPadding
FontMetrics提供了如下屬性:
top: 即上邊界, 因為在Android中, y軸正方向是向下的, 而基準線(base line)是y=0, 所以這個值是一個負數.它的值等于它到base line距離的負數
ascent: 字體文件中設置的Ascent值也是負數, 理由同上,它的值是ascent到base line的距離的負數
descent: 字體文件中設置的Descent值正數,因為在基準線下面,它的值等于它到base line的距離
bottom: 下邊界, 正數,理由同上,它的值等于它到base line的距離
leading: 兩行之間, 上一行的bottom和下一行的top的間距, 然而這個值總是0, 可以忽略,用下圖來描述leading
8.1 行距
行距就是相鄰兩行的基線之間的距離.默認行距的實際值等于字體設置中的|Descent| + |Aescent|,在Android的TextView中, 可以通過android:lineSpacingExtra和android:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默認值為0, lineSpacingMultiplier默認值為1, 有以下公式
行距=默認行距 * lineSpacingMultiplier + lineSpacingExtra
8.2 計算fontPadding
頂部的fontPadding= |top - ascent |,底部的font padding= bottom - descent,通過android:includeFontPadding可以決定字體的高度是否包含fontPadding
Android中的字體高度是|bottom| + |top|, 而普通軟件(例如word, Sketch或者其他設計軟件)中, 字體高度使用的是|descent| + |ascent|, 所以Android中的字體在垂直方向上總是比設計稿的多占一點空間.
對于普通的字體, 要完美復刻設計稿的字體高度, 應該把android:includeFontPadding設置為false,默認是true
8.3文本在控件水平方向居中顯示
原理很簡單,只需要計算繪制文字的x坐標 = 控件寬度的一半 - 文字寬度的一半即可, 代碼如下:
private val mPaint: Paint = Paint().apply {isAntiAlias = truecolor = Color.REDtextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)// 繪制的文本val txt = "hello go yes!!!"// 控件的寬度和高度的一半val x = measuredWidth / 2fval y = measuredHeight / 2f// 繪制中間水平線canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)// 繪制中間垂直線canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)// 測量文字大小val rect = Rect()mPaint.getTextBounds(txt, 0, txt.length, rect)// 文字水平居中的x坐標計算val finalX = x - rect.width() / 2f// 繪制文本canvas?.drawText(txt, finalX, y, mPaint)
}
效果圖:
由上圖可見文本在水平方向是居中了的,但是垂直方向并沒有居中(看感嘆號的位置就能對比出來了),這是因為我們將繪制文本的y坐標(baseline)定為了控件高度的一半,這是錯誤的,我們需要將文本處于控件居中位置, 而不應該是y坐標(baseline), 這就需要向下移動一個baseline的距離,這個距離的計算可以看下面介紹.
8.4 文本在控件垂直方向居中顯示
- 計算文字的高度一半到baseLine的距離
Baseline在文本的垂直方向很重要,只有先確定了Baseline的位置,換句話說就是y坐標的值,我們才能準確的將文字繪制在我們想要的位置上。Baseline的概念在我們使用TextView等系統控件直接設置文字內容時是用不到的,但是如果我們想要在Canvas畫布上面繪制文字時,Baseline的概念就必不可少了。以4個參數的drawText方法為例:
/*** canvas的drawText方法* @param text:待繪制的文本* @param x:從畫布上開始繪制的x坐標(Canvas是一個原點在左上角的平面坐標系)* @param y:baseLine所在的y坐標,不少人一開始以為y是繪制文字區域的底部坐標,其實是不正確的,這是兩個概念* @param paint: 畫筆*/
public void drawText(String text, float x, float y, Paint paint)
計算原理:
/*
如下所示:
---------------------top---------------------負數2 (相對原點baseline的距離)
---------------------ascent------------------ 負數1 (相對原點baseline的距離)
--------------------文本正中----------------- 距離top和bottom都等于(fontMetrics.bottom - fontMetrics.top)/2
---------------------baseline---------------- 0 (原點)
---------------------descent-----------------正數1 (相對原點baseline的距離)
---------------------bottom------------------正數2 (相對原點baseline的距離)*/
// 文字的高度 = (fontMetrics.bottom - fontMetrics.top)
// 文字正中 = 文字高度的一半 = (fontMetrics.bottom - fontMetrics.top)/2
// 那么文字的正中到baseline的距離 = 文字高度的一半 - fontMetrics.bottom,即:
int distanceY = (int) (((fontMetrics.bottom - fontMetrics.top) / 2) - fontMetrics.bottom);
注意: 文本的正中的"值"并不是控件的getHeight()/2,也就是說getHeight() != (fontMetrics.bottom - fontMetrics.top)因為fontMetrics的值是相對于baseline的.而控件是相對左上角top來計算的.所以如果要在自定義View的正中央繪制文本,那么可以這么做
private val mPaint: Paint = Paint().apply {isAntiAlias = truecolor = Color.REDtextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)val txt = "hello go yes!!!"// 控件的寬度和高度的一半val x = measuredWidth / 2fval y = measuredHeight / 2f// 繪制中間水平線canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)// 繪制中間垂直線canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)//=======================水平居中處理===============================// 測量文字大小val rect = Rect()mPaint.getTextBounds(txt, 0, txt.length, rect)// 文字水平居中的x坐標計算val finalX = x - rect.width() / 2f//=======================垂直居中處理===============================// 計算文本高度一半到baseline的距離val metrics = mPaint.fontMetricsval distance = (metrics.bottom - metrics.top) / 2f - metrics.bottom// 矯正y坐標(我們預想的就是讓文本正中的位置處于控件居中的位置,而文本正中距離baseline的距離就是我們計算y坐標的偏移量)val finalY = y + distance// 繪制文本canvas?.drawText(txt, finalX, finalY, mPaint)
}
效果圖:
注意看感嘆號的位置是居中的, 換個中文就很明顯了
8.5 Paint的setTextAlign使用
該方法用于控制文本在水平方向的對齊方式, 可以理解成該字符串與起點的相對位置, 常用有3種:
1)Align.LEFT: 居左繪制,即通過drawText函數指定的起點在最左側,文字從起點位置開始繪制
2)Align.CENTER:居中繪制,即通過drawText函數指定的起點在文字中間位置
3)Align.Right: 居右繪制,即通過drawText函數指定的起點在文字右側位置.
如下圖所示:
因此如果需要將文本繪制在控件的水平居中的位置,可以這么做
private val mPaint: Paint = Paint().apply {isAntiAlias = truecolor = Color.REDtextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)textAlign = Paint.Align.CENTER // 文字水平居中對齊
}override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)val txt = "我是勝哥"// 控件的寬度的一半val x = measuredWidth / 2f// 繪制中間垂直線canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)// 水平居中繪制文本, 可以看到此時計算x坐標的時候就不需要計算文本的寬度了canvas?.drawText(txt, x, 100f, mPaint)
}
效果如下:
九、繪制驗證碼
在 ImageView 繪制一個空心矩形,隨機產生 100 條干擾線,并隨機生成 4 個數,字顯示在矩形框內。
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ImageView iv = findViewById(R.id.iv);Bitmap bitmap = Bitmap.createBitmap(500, 300, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);canvas.drawColor(Color.BLACK);Paint paint = new Paint();paint.setAntiAlias(true);paint.setColor(Color.WHITE);paint.setStyle(Paint.Style.FILL);// 先繪制長方型canvas.drawRect(new Rect(100, 100, 370, 200), paint);// 繪制隨機文字char[] text = new char[]{'我', '你', '他', '它', '東', '西', '南', '北', '中'};Random random = new Random();// 生成隨機數char[] randomText = new char[4];int index = 0;while (index < 4 && randomText[index] == 0) {char getChar = text[random.nextInt(text.length)];boolean contains = false;for (int i = 0; i < randomText.length; i++) {if (getChar == randomText[i]) {contains = true;break;}}if (!contains) {randomText[index] = getChar;index++;}}paint.setColor(Color.BLACK);paint.setTextSize(30);paint.setFakeBoldText(true);// 設置字間距為一個字的寬度paint.setLetterSpacing(1);// 繪制文本canvas.drawText(randomText, 0, 4, 110, 160, paint);// 繪制線條paint.setStyle(Paint.Style.STROKE);for (int i = 0; i < 100; i++) {// 隨機顏色int color = Color.argb(150,55 + random.nextInt(200),55 + random.nextInt(200),55 + random.nextInt(200));paint.setColor(color);// 繪制隨機直線條/*int startY = 100 + random.nextInt(100);int endX = 100 + random.nextInt(270);canvas.drawLine(100, startY, endX, startY, paint);*/// 繪制隨機貝塞爾曲線Path path = new Path();// 控制點int controlX = 100 + random.nextInt(100);int controlY = 100 + random.nextInt(100);// 起點path.moveTo(100, 100 + random.nextInt(100));// 終點int endX = 370;int endY = 100 + random.nextInt(100);path.quadTo(controlX, controlY, endX, endY);canvas.drawPath(path, paint);}iv.setImageBitmap(bitmap);
}
效果圖:
總結
以上是生活随笔為你收集整理的Android绘图(一)基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: B-Suffix Array
- 下一篇: [SDOI2011]消耗战