1. 程式人生 > >帶你玩轉自定義view系列

帶你玩轉自定義view系列

View 的簡介

View是Android所有控制元件的基類,接下來借鑑網上的一張圖片讓大家一目瞭然(圖片出自:http://blog.51cto.com/wangzhaoli/1292313

 

Android 的相關座標系

圖片內容有點多,自定義 View 做得好事可以提升使用者對 APP 的體驗感的。接下來就學習一下 Android 的相關座標系。

Android 座標系

在物理中,要描述一個物體的運動,就必須選定一個參考系。所謂滑動,正式相對於參考系的運動。在Android中,將螢幕最左上角的頂點作為Android座標系的原點,從原點向右是X軸正方向,從原點向下是Y軸正方向:

 

系統提供了 getLocationOnScreen(intLocation[]) 這樣的方法來獲取 Android 座標中點的位置,即該檢視左上角在 Android 座標系中的座標。另外,在觸控事件中使用 getRawX() 和 getRawY() 方法所獲取的座標同樣是 Android 座標系中的座標。

檢視座標

Android 中除了上面所說的這種座標系之外,還有一個檢視座標系,他描述了子檢視在父檢視中的位置關係。這兩種座標系並不矛盾也不復雜,他們的作用是相輔相成的。

與 Android 座標系類似,檢視座標系同樣是以原點向右為X軸正方向,以原點向下為Y軸正方向。

只不過在檢視座標系中,原點不再是 Android 座標系中的螢幕左上角,而是以父檢視左上角為座標原點。

 

在觸控事件中,通過 getX() 和 getY() 所獲得的座標就是檢視座標中的座標。

在 Android 中,系統提供了非常多的方法來獲取座標值、相對距離等。方法多是好,但是不方便初學者學習,不知道什麼情況下使用。下面就總結了一些 API,結合 Android 座標系來看看該如何使用它們。

 

這些方法可以分成如下兩個類別:

View提供的獲取座標方法:
**getTop(): **獲取到的是View自身的頂邊到其父佈局頂邊的距離
**getLeft(): **獲取到的是View自身的左邊到其父佈局左邊的距離
**getRight(): **獲取到的是View自身的右邊到其父佈局左邊的距離
**getBottom(): **獲取到的是View自身的底邊到其父佈局頂邊的距離

另外View獲取自身寬高

**getHeight(): **獲取View自身高度
**getWidth(): **獲取View自身寬度
MotionEvent提供的方法:
**getX(): **獲取點選事件距離控制元件左邊的距離,即檢視座標
**getY(): **獲取點選事件距離控制元件東邊的距離,即檢視座標
**getRawX(): **獲取點選事件距離整個螢幕左邊的距離,即絕對座標
**getRawY(): **獲取點選事件距離整個螢幕頂邊的距離,即絕對座標

以上就是簡單的 Android 座標和 View 的檢視座標。

Android畫筆的詳解

Android提供了2D圖形繪製的各種工具,如Canvas(畫布)、Point(點)、Paint(畫筆)、Rectangles(矩形)等,利用這些工具可以直接在介面上進行繪製。

在自定義View中,我們經常用到的Canvas(畫布)和Paint(畫筆),像我們畫畫一樣,需要畫布和畫筆,在View中繪製控制元件,Canvas就代表著畫布,Paint就代表著畫筆。

這是的Android的的的官網裡畫的API:https://developer.android.com/reference/android/graphics/Paint

官網中的API有很多,下面是比較常用的一些API:

Paint.setAntiAlias(boolean flag);//設定抗鋸齒效果 設定true的時邊緣會將鋸齒模糊化Paint.setDither(boolean flag);//設定防抖動,設定true的時圖片看上去會更柔和點Paint.setColor(int color);//設定畫筆顏色Paint.setARGB(int a, int r, int g, int b); //設定畫筆的ARGB值Paint.setAlpha(int alpha);//設定畫筆的Alpha值Paint.setStyle(); //設定畫筆的style (三種:FILL填充 FILL_AND_STROKE填充加描邊 STROKE描邊 )Paint.setStrokeWidth(float width);//設定描邊寬度Paint.setXfermode(Xfermode xfermode);//設定圖形重疊時的處理方式,如合併,取交集或並集,經常用來製作橡皮的擦除效果Paint.setShader(Shader shader);//設定影象效果,使用Shader可以繪製出各種漸變效果Paint.setShadowLayer(float radius ,float dx,float dy,int color);//在圖形下面設定陰影層,產生陰影效果,radius為陰影的半徑,dx和dy為陰影在x軸和y軸上的距離,color為陰影的顏色 //下面寫文字的時候經常用到的Paint.setTextSize(float textSize);//設定畫筆文字大小Paint.measureText(String text);//測試文字的長度Paint.setTextAlign(Paint.Align align);// CENTER(文字居中) LEFT(文字左對齊) RIGHT(文字右對齊)

下面就演示一下上面這幾個API的效果。

Paint.setStye()

Paint.setStyle() //設定畫筆的style,有三種:

  • Paint.Style.FILL //將填充使用此樣式繪製的幾何和文字,忽略繪畫中與筆劃相關的所有設定

  • Paint.Style.FILL_AND_STROKE //使用此樣式繪製的幾何和文字將同時填充和描邊,尊重繪畫中與筆劃相關的欄位

  • Paint.Style.STROKE //使用此樣式繪製的幾何和文字將被描邊,尊重繪畫上與筆劃相關的欄位

演示一個小demo:

paint = new Paint();       paint.setColor(Color.RED);//畫筆顏色為紅色       paint.setStrokeWidth(80); //描邊寬度為80(為了區分效果,特意設定特別大)       float radius = 100f;       //將填充使用此樣式繪製的幾何和文字,忽略繪畫中與筆劃相關的所有設定       paint.setStyle(Paint.Style.FILL);       canvas.drawCircle(400, 200, radius, paint);       //使用此樣式繪製的幾何和文字將同時填充和描邊,尊重繪畫中與筆劃相關的欄位       paint.setStyle(Paint.Style.FILL_AND_STROKE);       canvas.drawCircle(400, 500, radius, paint);       //使用此樣式繪製的幾何和文字將被描邊,尊重繪畫上與筆劃相關的欄位       paint.setStyle(Paint.Style.STROKE);       canvas.drawCircle(400, 900, radius, paint);

結果:

 

Paint.setShader(Shader shader)

Paint.setShader(Shader shader) //設定影象效果,使用Shader可以繪製出各種漸變效果

Shader:著色器,用來給影象著色,此類是基類, Shader的API (https://developer.android.com/reference/android/graphics/Shader)。有5個子類:

  • BitmapShader

  • ComposeShader

  • LinearGradient

  • RadialGradient

  • SweepGradient

在瞭解上面5個類之前,先了解一下Shader.TileMode這個列舉,有三個值:

  • Shader.TileMode.CLAMP :如果著色器在其原始邊界之外繪製,則複製邊緣顏色

  • Shader.TileMode.MIRROR :水平和垂直重複著色器的影象,交替映象,使相鄰的影象始終接縫

  • Shader.TileMode.REPEAT :水平和垂直重複著色器的影象

BitmapShader

這裡只介紹一種著色器,其餘的點選閱讀原文進行檢視。

其實這個Shader用於繪製bitmap作為紋理,然後通過平鋪模式進行填充

/**        * 建構函式        * @bitmap 用來填充圖形的Bitmap        * @tileX X軸Bitmap用Shader.TileMode模式填充        * @tileY Y軸Bitmap用Shader.TileMode模式填充        */       BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

演示一下:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower);       BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.MIRROR);       paint.setShader(shader);       canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);

結果:

 

X軸用Shader.TileMode.CLAMP模式,就是用bitmap的右邊緣去填充X軸的其餘空間
Y軸用Shader.TileMode.MIRROR模式,就是在用相鄰兩張影象互為映象的方式填充整個Y軸其餘空間

接下來XY軸換一下模式:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower);       BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.MIRROR, BitmapShader.TileMode.REPEAT);       paint.setShader(shader);       canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);

結果:

 

X軸用Shader.TileMode.MIRROR模式,就是在用相鄰兩張影象互為映象的方式填充整個X軸其餘空間
Y軸用Shader.TileMode.REPEAT模式,就是用相同的影象重複填充整個Y軸其餘空間

其餘四種著色器示例請閱讀原文進行檢視。

Paint.setShadowLayer(float radius ,float dx,float dy,int color)

Paint.setShadowLayer(float radius ,float dx,float dy,int color) //在圖形下面設定陰影層,產生陰影效果

/**        * @radius radius為陰影半徑,半徑越大,陰影面積越大,越模糊;反之,半徑越小,陰影面積越小,也越清晰,radius=0時,陰影消失        * @dx dx為陰影在x軸上的偏移值        * @dy dy為陰影在y軸上的偏移值        * @color color為陰影的顏色        */       Paint.setShadowLayer( float radius, float dx, float dy, int color);

演示一下:

paint.setColor(Color.RED);       paint.setShadowLayer(20, 0, 0, Color.YELLOW);       paint.setTextSize(100);       canvas.drawText("I am Layne", 200, 300, paint);

結果:

 

改一下:

paint.setShadowLayer(20,50, 50, Color.YELLOW);

結果:

 

改一下:

paint.setShadowLayer(1,50, 50, Color.YELLOW);

結果:

 

再改一下:

paint.setShadowLayer(0,50, 50, Color.YELLOW);

結果:

 

新增陰影:

paint.setColor(Color.RED);       paint.setShadowLayer(30, 0, 0, Color.BLACK);       setLayerType(LAYER_TYPE_SOFTWARE, paint);//要注意加上這句       canvas.drawCircle(400, 800, 100, paint);

**結果: **

 

常用畫筆的 API 介紹完了

Android畫布的詳解

接下來學習一下自定義View之Canvas(畫布)的詳解

先來看看Canvas常用方法:

功能分類 | Canvas常用方法 | 備註 |
| 繪製顏色 | drawARGB | 通過設定ARGB值繪製顏色 |
| drawRGB | 通過設定RGB值繪製顏色 |
| drawColor | 繪製顏色 |
| 繪製圖形 | drawPoint,drawPoints | 繪製點,點集合 |
| drawLine,drawLines | 繪製線,線集合 |
| drawRect | 繪製矩形 |
| drawCircle | 繪製圓 |
| drawOval | 繪製橢圓 |
| drawArc | 繪製弧 |
| 畫布操作 | translate、rotate、scale、save、restore | 依次為位移、旋轉、縮放、儲存畫布和恢復畫布 |
| drawPath | 按路徑繪製 |

Canvas 繪製顏色的 API

canvas.drawARGB(int a, int r, int g, int b)canvas.drawRGB(int r, int g, int b)canvas.drawColor(int color) canvas.drawColor(int color, PorterDuff.Mode mode)

顏色的四種模式:

顏色模式 備註
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(16位)
RGB565 螢幕預設模式(16位)
Alpha8 僅有透明通道(8位)

ARGB分別代表的型別:

型別 備註
A(Alpha) 透明度,取值範圍 [0,255],0代表完全透明,255代表完全不透明
R(Red) 紅色,取值範圍 [0,255],0代表無色,255代表紅色
G(Green) 綠色,取值範圍 [0,255],0代表無色,255代表綠色
B(Blue) 藍色,取值範圍 [0,255],0代表無色,255代表藍色

從表中可以看出,ARGB的取值範圍都是[0,255],其實就是16進制中的[0x00,0xff]
A從0x00到0xff對應表示從透明到不透明
RGB從0x00到0xff對應表示顏色從淺到深

示例程式碼:

canvas.drawARGB(0, 0, 0, 0);canvas.drawARGB(255, 255, 0, 0);canvas.drawARGB(255, 0, 255, 0);canvas.drawARGB(255, 0, 0, 255);

**結果: **

 

Canvas 繪製形狀

canvas.drawPoint(float x, float y, Paint paint) //繪製點canvas.drawPoints( float[] pts, Paint paint) //繪製多個點canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) //繪製線canvas.drawLines(float[] pts, Paint paint)//繪製多條線canvas.drawRect(float left, float top, float right, float bottom, Paint paint) //繪製矩形canvas.drawRect(RectF rect, Paint paint) //繪製矩形canvas.drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,Paint paint) //繪製圓角矩形canvas.drawRoundRect(RectF rect, float rx, float ry, Paint paint) //繪製圓角矩形canvas.drawCircle(float cx, float cy, float radius,Paint paint) //繪製圓canvas.drawOval(float left, float top, float right, float bottom,Paint paint) //繪製橢圓canvas.drawOval(RectF oval,Paint paint) //繪製橢圓canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter,Paint paint) //繪製圓弧canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint) //繪製圓弧

畫點

Paint paint = new Paint();       paint.setColor(Color.GREEN);       paint.setStrokeWidth(20f);       canvas.drawPoint(300, 300, paint);       float[] pts = {400, 400, 500, 400, 600, 400};       canvas.drawPoints(pts, paint);

drawPoint的兩個引數就是(x,y)。而drawPoints的第一個引數是float陣列,每兩個就代表一個座標,如上面的例子有(400,400)、(500,400)和(600,400)。結果:

 

畫線

Paint paint = new Paint();       paint.setColor(Color.GREEN);       paint.setStrokeWidth(20f);       canvas.drawLine(200, 500, 1000, 500, paint);       float[] pts = {800, 400, 1000, 500,               800, 600, 1000, 500};       canvas.drawLines(pts, paint);

drawLine的前四個引數就代表起始座標(x1,y1),和終點座標(x2,y2)。而drawLines第一個引數也是float陣列,如上面的例子前四個起始點(800, 400),終點(1000, 500)。接著下一條線的起點(800, 600),終點(1000, 500)。結果:

 

畫矩形

Paint paint = new Paint();       paint.setColor(Color.GRAY);       paint.setStyle(Paint.Style.STROKE); //只描邊       paint.setStrokeWidth(10f);       //1.直接通過座標畫矩形       canvas.drawRect(100, 100, 800, 500, paint);       //2.通過Rect 畫矩形       Rect mRect = new Rect(100, 550, 800, 950);       paint.setStyle(Paint.Style.FILL); //只描內容       canvas.drawRect(mRect, paint);       //3.通過RectF 畫矩形       RectF mRectF = new RectF(100, 1000, 800, 1400);       paint.setStyle(Paint.Style.FILL_AND_STROKE); //描邊和內容       paint.setStrokeWidth(10f);       canvas.drawRect(mRectF, paint);

以上的座標引數都是四個的,即用前面兩個代表左上角座標(x1,y1)和右下角座標(x2,y2)來確定矩形的。結果:

 

剩餘的畫圓、橢圓、圓角矩形和圓弧的操作點選原文進行檢視,基本差不多,這裡就不做多演示。

Canvas 畫布的操作

canvas.translate(float dx, float dy) //平移canvas.rotate(float degrees) //旋轉canvas.rotate(float degrees, float px, float py) //改變旋轉中心並旋轉canvas.scale(float sx, float sy) //縮放canvas.scale(float sx, float sy, float px, float py) //改變縮放中心並縮放canvas.save(); //儲存畫布canvas.restore(); //恢復畫布

Canvas畫布的操作可以讓我們更加容易繪製圖形,Canvas畫布操作只會對後面的繪製起作用,對前面已經繪製的是不影響的。

translate

translate是對座標系的平移,且是可以多次重疊的,預設是在螢幕的左上角(0,0)。

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只內容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //設定抗鋸齒       paint.setDither(true); //設定防抖動       //將座標系移到Canvas寬度的一半,高度400的位置       canvas.translate(getMeasuredWidth() / 2, 400);       //繪製一個紅色圓       paint.setColor(Color.RED);       canvas.drawCircle(0, 0, 100, paint);       //座標系原點在前面位置的基礎上再往下移動250畫素       canvas.translate(0, 250);       paint.setColor(Color.GREEN);       canvas.drawCircle(0, 0, 100, paint);       //座標系原點在前面位置的基礎上再往下移動250畫素       canvas.translate(0, 250);       paint.setColor(Color.BLUE);       canvas.drawCircle(0, 0, 100, paint);

**結果: **

 

rotate

rotate是對座標系的旋轉,rotate有兩個方法

canvas.rotate(float degrees) canvas.rotate(float degrees, float px, float py)

rotate第一個引數是旋轉的角度,後面兩個引數是可以確定旋轉中心,如果不填預設是(0,0)。

演示一下:

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只內容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //設定抗鋸齒       paint.setDither(true); //設定防抖動       //將座標系移動到螢幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       RectF rectF = new RectF(-300, -300, 0, 0);       //繪製一個紅色矩形       paint.setColor(Color.RED);       canvas.drawRect(rectF, paint);       //將座標系旋轉180度,不會影響前面已經繪製的圖形       canvas.rotate(180);       paint.setStyle(Paint.Style.FILL);       //繪製一個藍色矩形       paint.setColor(Color.BLUE);       canvas.drawRect(rectF, paint);

結果:

 

scale

scale也有兩個方法

canvas.scale(float sx, float sy)canvas.scale(float sx, float sy, float px, float py)

scale是縮放,縮放中心預設是座標原點,且多次縮放是可以重疊的。scale的前兩個引數sx和sy是X軸和Y軸的縮放倍數,後兩個引數px和py是控制縮放中心的位置。演示一下:

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只內容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //設定抗鋸齒       paint.setDither(true); //設定防抖動       //將座標系移動到螢幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       RectF rectF = new RectF(-300, -300, 0, 0);       paint.setColor(Color.RED);       //左上角繪製紅色矩形       canvas.drawRect(rectF, paint);       //X軸 Y軸分別縮放到原來的1/2並以原點(0,0)位對稱點進行翻轉       canvas.scale(-0.5f, -0.5f);       //繪製綠色矩形       paint.setColor(Color.GREEN);       canvas.drawRect(rectF, paint);

結果:

 

這裡的sx和sy引數取值挺有考究的,可以自己寫個 demo 試試

sx sy取值範圍 備註
(1,+∞) 根據縮放中心放大到原來的n倍
1 跟原來大小一樣,沒變化
(0,1) 根據縮放中心縮放
0 sx sy有一個取0圖形就消失了
(-1,0) 根據縮放中心縮放並翻轉
-1 翻轉
(-∞,-1) 根據縮放中心放大並翻轉

save和restore

save:表示儲存Canvas之前的狀態。save儲存之後,可以呼叫Canvas的平移、縮放、旋轉、錯切和裁剪等操作。
restore:恢復Canvas之前儲存的狀態,防止save後對Canvas執行的操作對後續的繪製有影響。

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只內容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //設定抗鋸齒       paint.setDither(true); //設定防抖動       //儲存畫布       canvas.save();       //座標原點移到螢幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       //以螢幕中心為座標原點在(100,100)為圓心處繪製紅色圓       paint.setColor(Color.RED);       RectF rectF = new RectF(0, 0, 200, 200);       canvas.drawRect(rectF, paint);       //恢復畫布       canvas.restore();       //恢復畫布後,座標原點(0,0)預設在螢幕左上角,       //即以螢幕左上角為座標原點在(100,100)為圓心處繪製藍色圓       paint.setColor(Color.BLUE);       RectF rectf = new RectF(50, 50, 100, 100);       canvas.drawRect(rectf, paint);

**結果: **

 

以上的程式碼去掉save()和restore()方法,再執行,結果:

 

如果去掉save()和restore(),那麼所有的影象都在座標原點移動到螢幕中心後繪製;如果有save()和restore(),在restore()之後,影象的座標原點又回到了螢幕的左上角了。

drawPath按路徑繪製。

Canvas之Path的詳解

Canvas的繪製圖形只能繪製一些常規的,比如點、線、圓、橢圓、矩形等的。如果想要繪製更復雜的圖形,那麼就得靠Path了。

Path的定義:
Path類將多種符合路徑(多個輪廓,如直線段、二次曲線、立方曲線等)封裝在其內部的幾何路徑。

Path的繪製:
通過設定Paint.Style的FILL(只描內容)、STROKE(只描邊)、FILL_AND_STROKE(描邊和內容),然後呼叫canvas.drawPath(path, paint);Path還可以用於剪下或者在路徑上繪製文字canvas.drawTextOnPath()。

Path有兩個建構函式

Path() // 空的建構函式Path(Path src) //建立一個新的路徑,並且從src路徑裡賦值內容

Path一些常用的API:

功能分類 | Path的常用API | 備註 |
| 線操作 | lineTo、rLineTo | 繪製線 |
| 點操作 | moveTo、rMoveTo | 改變後面操作的起始點位置 |
| setLastPoint | 改變前面操作中最後點的位置 |
| 常規圖形操作 | addRect | 繪製矩形 |
| addRoundRect | 繪製圓角矩形 |
| addCircle | 繪製圓 |
| addOval | 繪製橢圓 |
| addArc、arcTo | 繪製圓弧 |
| 閉合path操作 | close | 如果連線Path起點和終點能形成一個閉合圖形,則會將起點和終點連線起來形成一個閉合圖形 |
| 貝塞爾曲線 | quadTo、rQuadTo、cubicTo、rCubicTo | 貝塞爾曲線 |

線操作

lineTo(float x, float y) //添加當前點到目標點(x,y)構成的直線到pathrLineTo(float dx, float dy) //基於當前座標系,即以path最後的那個點//為座標系原點(0,0),如果前面沒有path的點,預設是螢幕左上角(0,0)

注:lineTo、rLineTo起始點預設是螢幕左上角的座標系原點(0,0)

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //螢幕左上角(0,0)到(200,200)畫一條直線        path.lineTo(200, 200);        //(200, 200)到(200, 400)畫一條直線        path.lineTo(200, 400);        //以(200, 400)為起始點(0,0)偏移量為(200, 400)畫一條直線,        //其終點座標實際在螢幕的位置為(400, 800)        path.rLineTo(400, 800);        canvas.drawPath(path, paint);

結果:

 

點操作

moveTo(float x, float y) //改變接下來操作的起點位置為(x,y)rMoveTo(float dx, float dy) //接下來要操作的起點位置為(x+dx,y+dy)setLastPoint(float dx, float dy) //改變前一步操作點的位置,會改變前一步的操作

先對比一下moveTo()和rMoveTo()的區別,演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //將座標系原點從(0,0)移動到(200,200)        path.moveTo(200,200);        //畫從(200,200)到(400,400)之間的直線        path.lineTo(400, 400);//        path.rMoveTo(100, 0); //暫時註釋        path.lineTo(800, 400);        canvas.drawPath(path, paint);

暫時註釋了rMoveTo()方法,看看結果:

 

然後解掉rMoveTo()方法的註釋,意思是下一步的起點位置由(400, 400)變為(400+100, 400+0),即(500,400),結果:

 

再來看看moveTo()和setLastPoint()的區別,同樣以上的程式碼,加上path.setLastPoint(800, 200)方法:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //將座標系原點從(0,0)移動到(200,200)        path.moveTo(200,200);        //畫從(200,200)到(400,400)之間的直線        path.lineTo(400, 400);        path.setLastPoint(800, 200);        path.lineTo(800, 400);        canvas.drawPath(path, paint);

結果:

 

紅線原本是設定setLastPoint(800, 200)之前的路徑,在設定之後,影響到了前一步lineTo(400, 400)的操作,使之變成了lineTo(800, 200),結果就如圖了。

得出結論:rMoveTo()影響的是後面操作的起點位置,並不會影響之前的操作;而setLastPoint()改變前一步操作最後一個點的位置,不僅影響前一步操作,同時也會影響後一步操作。

繪製常規圖形

//繪製圓addCircle(float x, float y, float radius, Direction dir)  //繪製橢圓addOval(RectF oval, Direction dir)addOval(float left, float top, float right, float bottom, Direction dir) //繪製矩形addRect(RectF rect, Direction dir) addRect(float left, float top, float right, float bottom, Direction dir) //繪製圓角矩形addRoundRect(RectF rect, float rx, float ry, Direction dir) addRoundRect(float left, float top, float right, float bottom, float rx, float ry,Direction dir)addRoundRect(RectF rect, float[] radii, Direction dir)addRoundRect(float left, float top, float right, float bottom, float[] radii,Direction dir)

所有API裡面都有一個共同的引數Direction:

Direction 備註
Path.Direction.CCW counter-clockwise ,沿逆時針方向繪製
Path.Direction.CW clockwise ,沿順時針方向繪製

Direction其用法演示:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(2f);        paint.setTextSize(40f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //以(600,600)為圓心,300為半徑繪製圓        //Path.Direction.CW順時針繪製圓 Path.Direction.CCW逆時針繪製圓        path.addCircle(600, 600, 300, Path.Direction.CCW);        //沿path繪製文字        canvas.drawTextOnPath("我是Layne,在測試Direction,這是CCW逆時針繪製圓", path, 0, 0, paint);        canvas.drawPath(path, paint);

結果對比:

 

其他圖形繪製示例:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //以(400,200)為圓心,半徑為100繪製圓        path.addCircle(550, 200, 100, Path.Direction.CW);        //繪製橢圓        RectF rectF = new RectF(100, 350, 500, 600);        //第一種方法繪製橢圓        path.addOval(rectF, Path.Direction.CW);        //第二種方法繪製橢圓        path.addOval(600, 350, 1000, 600, Path.Direction.CW);        //繪製矩形        RectF rect = new RectF(100, 650, 500, 900);        //第一種方法繪製矩形        path.addRect(rect, Path.Direction.CW);        //第一種方法繪製矩形        path.addRect(600, 650, 1000, 900, Path.Direction.CCW);        //繪製圓角矩形        RectF roundRect = new RectF(100, 950, 300, 1100);        //第一種方法繪製圓角矩形        path.addRoundRect(roundRect, 20, 20, Path.Direction.CW);        //第二種方法繪製圓角矩形        path.addRoundRect(350, 950, 550, 1100, 10, 50, Path.Direction.CCW);        //第三種方法繪製圓角矩形        //float[] radii中有8個值,依次為左上角,右上角,右下角,左下角的rx,ry        RectF roundRectT = new RectF(600, 950, 800, 1100);        path.addRoundRect(roundRectT, new float[]{50, 50, 50, 50, 50, 50, 0, 0}, Path.Direction.CCW);        //第四種方法繪製圓角矩形        path.addRoundRect(850, 950, 1050, 1100,new float[]{0, 0, 0, 0,50, 50, 50, 50}, Path.Direction.CCW);        canvas.drawPath(path, paint);

結果:

 

繪製圓弧

//繪製圓弧addArc(RectF oval, float startAngle, float sweepAngle)addArc(float left, float top, float right, float bottom, float startAngle,float sweepAngle)//forceMoveTo:是否強制將path最後一個點移動到圓弧起點,//true是強制移動,即為不連線兩個點;false則連線兩個點arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)arcTo(RectF oval, float startAngle, float sweepAngle)arcTo(float left, float top, float right, float bottom, float startAngle,float sweepAngle, boolean forceMoveTo)

addArc()和arcTo()都是新增圓弧到path中,不過他們之間還是有區別的。addArc()是直接新增圓弧到path中;而arcTo()會判斷要繪製圓弧的起點與繪製圓弧之前path中最後的點是否是同一個點,如果不是同一個點的話,就會連線兩個點。

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(20f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //在F(400, 200, 700, 500)區域內繪製一個270度的圓弧        RectF rectF = new RectF(400, 200, 700, 500);        path.addArc(rectF, 0, 270);        //在(400, 600, 600, 800)區域內繪製一個90度的圓弧,並且不連線兩個點        RectF rectFTo = new RectF(400, 600, 700, 900);        path.arcTo(rectFTo, 0, 180, true); //等價於path.addArc(rectFTo, 0, 180);        canvas.drawPath(path, paint);

結果:

 

把上面程式碼修改成連線兩點:

path.arcTo(rectFTo, 0, 180, false); //等價於path.addArc(rectFTo, 0, 180);

結果:

 

閉合path操作

如果path的重點和起始點不是同一個點的話,那麼path.close()就會連線這兩個點,形成一個封閉的圖形。演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(20f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        //在F(400, 200, 700, 500)區域內繪製一個270度的圓弧        RectF rectF = new RectF(400, 200, 700, 500);        path.addArc(rectF, 0, 270);//        path.close(); //先註釋        canvas.drawPath(path, paint);

結果:

 

接下來解掉path.close()的註釋,再執行一次,結果:

 

貝塞爾曲線

貝塞爾曲線就麻煩多了,也複雜多了。這裡不詳細說明了,再網上找到一篇說明比較詳細的文章。
安卓自定義View進階 - 貝塞爾曲線:(https://blog.csdn.net/u013831257/article/details/51281136)

這裡直接複製了裡面的二階曲線的實現:

public class CanvasView extends View {    private Paint mPaint;    private int centerX, centerY;    private PointF start, end, control;    public CanvasView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        mPaint = new Paint();        mPaint.setColor(Color.BLACK);        mPaint.setStrokeWidth(8);        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setTextSize(60);        start = new PointF(0, 0);        end = new PointF(0, 0);        control = new PointF(0, 0);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        centerX = w / 2;        centerY = h / 2;        // 初始化資料點和控制點的位置        start.x = centerX - 200;        start.y = centerY;        end.x = centerX + 200;        end.y = centerY;        control.x = centerX;        control.y = centerY - 100;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        // 根據觸控位置更新控制點,並提示重繪        control.x = event.getX();        control.y = event.getY();        invalidate();//重新整理View,重新繪製        return true;    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        // 繪製資料點和控制點        mPaint.setColor(Color.GRAY);        mPaint.setStrokeWidth(20);        canvas.drawPoint(start.x, start.y, mPaint);        canvas.drawPoint(end.x, end.y, mPaint);        canvas.drawPoint(control.x, control.y, mPaint);        // 繪製輔助線        mPaint.setStrokeWidth(4);        canvas.drawLine(start.x, start.y, control.x, control.y, mPaint);        canvas.drawLine(end.x, end.y, control.x, control.y, mPaint);        // 繪製貝塞爾曲線        mPaint.setColor(Color.RED);        mPaint.setStrokeWidth(8);        Path path = new Path();        path.moveTo(start.x, start.y);        path.quadTo(control.x, control.y, end.x, end.y);        canvas.drawPath(path, mPaint);    }}

結果:

figure style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: 0.544000029563904px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; font-size: 16px; color: rgb(62, 62, 62); line-height: inherit; background-color: rgb(255, 255, 255);"

 

 

PathMeasure的詳解

PathMeasure是什麼?
PathMeasure是用來對Path進行測量的工具,一般來說PathMeasure是和Path配合著使用的。通過PathMeasure,我們可以知道Path路徑上某謳歌點的座標、Path的長度等的。

PathMeasure有兩個建構函式:

//構建一個空的PathMeasurePathMeasure() //構建一個PathMeasure並關聯一個指定的建立好的PathPathMeasure(Path path, boolean forceClosed) 

無參建構函式PathMeasure()在使用前必須呼叫setPath(Path path, boolean forceClosed)來關聯一個Path。其實就是等價於有引數的建構函式PathMeasure(Path path, boolean forceClosed)了。如果關聯之後的Path有所更改,那麼就需要呼叫setPath(Path path, boolean forceClosed)重新關聯。

PathMeasure常用的API:

setPath(Path path, boolean forceClosed) isClosed()getLength()getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)getMatrix(float distance, Matrix matrix, int flags)getPosTan(float distance, float pos[], float tan[])nextContour()

setPath(Path path, boolean forceClosed):
此方法是關聯一個預先建立好的Path。第二個引數forceClosed如果為true,並且關聯的Path未閉合時,測量的Path長度可能比Path實際長度長一點,因為測量的是Path閉合的長度。但是關聯的Path不會有任何變化

isClosed():
判斷關聯的Path是否是閉合狀態。如果forceClosed為true,那麼此方法一定返回true。

getLength():
返回已關聯的Path總長度。如果setPath()時設定的forceClosed為true,則返回的值可能會比實際長度的長。

getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo):
擷取Path的一段。如果擷取成功,則返回true,反之則返回false。

引數 備註
startD 起點在Path的位置,取值範圍0<=startD < stopD<=getLength()
stopD 終點在Path的位置,取值範圍0<=startD < stopD<=getLength()
dst 將擷取的path的片段新增到dst中
startWithMoveTo 起點是否使用MoveTo,如果為true,則擷取的path的第一個點不會變化,擷取的path也不會改變,如果為false,則擷取的path可能會發生形變。

注:
1.如果擷取Path的長度為0,則返回false,大於0則返回true;
2.startD、stopD必須為和法制(0,getLength()),如果startD>=stopD,則返回false;
3.在Android 4.4或之前的版本在開啟硬體加速時,繪製可能會不顯示,請關閉硬體加速或者給dst新增一個簡單的操作,如:dst.rLineTo(0,0)

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        //初始化Path並順時針繪製一個矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());        canvas.drawPath(sourcePath, paint);

結果:

 

 

現在要擷取上圖中的下半部分,總長度是2000,中間的部分就是[750,1750]。演示一下:

        //初始化Path並順時針繪製一個矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一個空的Path        Path dstPath = new Path();        //擷取sourcePath的一部分新增到dstPath中        measure.getSegment(750, 1750, dstPath, true);        dstPath.rLineTo(0, 0);//真機剛剛好是4.4版本的,加個簡單的操作        canvas.drawPath(dstPath, paint);

結果:

 

上面程式碼中的dstPath初始化完之後,並沒有內容的,試試有內容的情況:

        //初始化Path並順時針繪製一個矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一個空的Path        Path dstPath = new Path();        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        //擷取sourcePath的一部分新增到dstPath中        measure.getSegment(750, 1750, dstPath, true);        canvas.drawPath(dstPath, paint);

結果:

 

有意思的是,將dstPath的rLineTo()和lineTo()放在getSegment()之後,後面內容的起點就成了getSegment()的終點了。演示一下:

        //初始化Path並順時針繪製一個矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一個空的Path        Path dstPath = new Path();        //擷取sourcePath的一部分新增到dstPath中        measure.getSegment(750, 1750, dstPath, true);        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        canvas.drawPath(dstPath, paint);

結果:

 

再把上面程式碼中的getSegment()的startWithMoveTo引數改成false會變成什麼樣呢?演示一下:

          //初始化Path並順時針繪製一個矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一個空的Path        Path dstPath = new Path();        //為了易於理解,把這兩個方法放回getSegment()前面        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        //擷取sourcePath的一部分新增到dstPath中        measure.getSegment(750, 1750, dstPath, false);        canvas.drawPath(dstPath, paint);

結果:

 

和上上結果對比可得出:startWithMoveTo引數為true時,被擷取的path片段會保持原狀;startWithMoveTo引數為false時,會將擷取的path片段的起始點移動到dstPath的終點,以保持dstPath的連續性。

getMatrix(float distance, Matrix matrix, int flags):
距離Path起始點的一段長度distance,通過計算得到該位置座標並返回一個處理好的矩陣,該矩陣以左上角為旋轉點,如果Path不存在或者長度為0,該方法返回false。

引數 備註
distance 距離Path起始點的距離,取值範圍0 <= distance <=getLength()
matrix 根據 flags封裝matrix,flags不同,存入matrix的就不同
flags PathMeasure.POSITION_MATRIX_FLAG:位置資訊 ,PathMeasure.TANGENT_MATRIX_FLAG:切邊資訊,方位角資訊,使得圖片按path旋轉。

getPosTan(float distance, float pos[], float tan[]):
距離Path起始點的長度distance,通過計算返回該長度在Path上的座標及該座標的正切值分別複製給pos[]、tan[]

引數 備註
distance 距離Path起始點的距離,取值範圍0 <= distance <= getLength()
pos[] distance在path上的座標,即pos[]存的該點的座標x,y值
tan[] distance在path上對應座標點在path上的方向,tan[0]是鄰邊邊長,tan[1]是對邊邊長。通過Math.atan2(tan[1], tan[0])*180.0/Math.PI 可以得到正切角的弧度值。

nextContour():
如果Path有多條曲線組成,且彼此不連線,那麼getLength()、getSegment()、getMatrix()和getPosTan()這些方法,都只是針對當前正在操作的。舉個例子,Path由多條曲線組成,且彼此不連線,那麼getLength()返回的只是當前操作曲線的長度,並不是所有曲線的長度。那麼怎麼獲取下一條曲線的長度呢?這時就得用nextContour()跳轉到下一條曲線了,跳轉成功返回true,失敗就返回false。演示一下:

 Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描邊        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //設定抗鋸齒        paint.setDither(true); //設定防抖動        Path path = new Path();        PathMeasure measure = new PathMeasure();        //繪製一條從(100,100)到(900,100)的直線,長度為800        path.moveTo(100, 100);        path.lineTo(900, 100);        //繪製一條從(100,200)到(500,100)的直線,長度為400        path.moveTo(100, 200);        path.lineTo(500, 200);        measure.setPath(path, false);        //輸出第一條曲線的長度        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());        measure.nextContour(); //跳轉到下一條曲線        //輸出第二條曲線的長度        Log.e("PathMeasure", "next measure.getLength():" + measure.getLength());        canvas.drawPath(path, paint);

輸出的結果:

 

Region區域

Region 在 Android 的繪製中是區域的意思,使用 Region 可以對圖形進行很多操作,比如區域的合併,取交集、或抑或等等。

Region 的建構函式有以下四個:

public Region()  //無參構造public Region(Region region) //傳入指定一個區域public Region(Rect r)  //傳入一個矩形public Region(int left, int top, int right, int bottom) //傳入兩個點,其實就是一個矩形

方法二就是傳入一個 Region 指定區域,方法三四都是一個意思,就是傳入一個矩形。方法一是無參構造,Region 是可以後期指定代表區域的,以下是後期指定代表區域的方法:

public void setEmpty()  //清空public boolean set(Region region)   public boolean set(Rect r)   public boolean set(int left, int top, int right, int bottom)   public boolean setPath(Path path, Region clip)//將path和clip的兩個區域取交集

以上的 set 方法都是指定新的區域來代替 Region 物件中原有的區域。

還有以上的方法在繪製圖像過程中,cavas 沒有直接繪製 Region 的方法,要繪製指定的 Region 需要使用 RegionIterator,RegionIterator 是一個迭代器,其主要作用是從指定的 Region 中獲取 rect,也就是矩形。

1SetPath()

public boolean setPath(Path path, Region clip)//將path和clip的兩個區域取交集

如註釋,該方法的作用是將 path 區域和 clip 區域取交集。接下來演示一下:

public class RegionView extends View {   private Paint mPaint;   public RegionView(Context context) {       this(context, null);   }   public RegionView(Context context, AttributeSet attrs) {       this(context, attrs, 0);   }   public RegionView(Context context, AttributeSet attrs, int defStyleAttr) {       super(context, attrs, defStyleAttr);       initPaint();   }   private void initPaint() {       mPaint = new Paint();       mPaint.setColor(Color.GREEN);       mPaint.setStrokeWidth(3);       mPaint.setStyle(Paint.Style.STROKE);   }   @Override   protected void onDraw(Canvas canvas) {       super.onDraw(canvas);       //畫一個圓       Path path = new Path();       path.addCircle(500, 500, 300, Path.Direction.CW);       //指定一個區域,然後取與圓的交集       Region region = new Region();       region.setPath(path, new Region(0, 0, 1000, 1000));       //繪製交集區域       RegionIterator iterator = new RegionIterator(region);       Rect rect = new Rect();       while (iterator.next(rect)) {           canvas.drawRect(rect, mPaint);       }   }}

上面程式碼可以看到,ReginIterator 類是依次取出構成區域大小不同的矩形,然後由 cavas 繪製,從而組成一個圖形,下面看結果:

 

從圖中可以看出,圓形是由若干個矩形組成,依次排列成圓形,因為程式碼中畫筆使用的風格是 STROKE(描邊),所以中間一些就是空的。如果使用 Fill(填充),那麼組成的就是一個實心圓。

2區域的操作

public final boolean union(Rect r)   public boolean op(Rect r, Op op) {  public boolean op(int left, int top, int right, int bottom, Op op)   public boolean op(Region region, Op op)   public boolean op(Rect rect, Region region, Op op)

區域的操作有很多種,上面第一種 union() 主要是取並集,後面的 op() 方法就是 operation,操作的意思,具體如何操作?還要看後面的 op 引數來決定。Op 是一個列舉類,具體取值如下:

public enum Op {       DIFFERENCE(0), //取補集       INTERSECT(1),  //取交集       UNION(2),  //取並集       XOR(3),    //取異並集       REVERSE_DIFFERENCE(4),  //取反轉補集       REPLACE(5);    //取後者區域代替前者       Op(int nativeInt) {           this.nativeInt = nativeInt;       }       /**        * @hide        */       public final int nativeInt;   }

程式碼示例如下:

public class RegionView extends View {   private Paint mPaint;   public RegionView(Context context) {       this(context, null);   }   public RegionView(Context context, AttributeSet attrs) {       this(context, attrs, 0);   }   public RegionView(Context context, AttributeSet attrs, int defStyleAttr) {       super(context, attrs, defStyleAttr);       initPaint();   }   private void initPaint() {       mPaint = new Paint();       mPaint.setColor(Color.GREEN);       mPaint.setStrokeWidth(3);       mPaint.setStyle(Paint.Style.STROKE);   }   @Override   protected void onDraw(Canvas canvas) {       super.onDraw(canvas);       Path path1 = new Path();       Path path2 = new Path();       path1.addCircle(500, 500, 300, Path.Direction.CW);       path2.addCircle(500, 800, 300, Path.Direction.CW);       canvas.drawPath(path1, mPaint);       canvas.drawPath(path2, mPaint);       Region region1 = new Region();       Region region2 = new Region();       region1.setPath(path1, new Region(0, 0, 1500, 1500));       region2.setPath(path2, new Region(0, 0, 1500, 1500));       region1.op(region2, Region.Op.REPLACE);       mPaint.setColor(Color.GREEN);       mPaint.setStyle(Paint.Style.FILL);       RegionIterator iterator = new RegionIterator(region1);       Rect rect = new Rect();       while (iterator.next(rect)) {           canvas.drawRect(rect, mPaint);       }   }}

各結果如下:

 

結尾

喜歡的朋友可以收藏點個贊哦。歡迎轉發