目錄
-
座標系以及view的位置信息
自定義view在平時有很多應用,我們知道自定義view,其實就是通過Canvas進行繪製,但是在繪製之前,一些基本的知識要明確,安卓的座標系和我們實際在數學中用的座標系還有一些區別, 在安卓中初始化以屏幕的左上角爲原點,
這是關於座標的解釋,還有我們再繪畫view的時候。經常用到view的長寬,left,top,right,bottom的座標。view獲取左,上,右下這些座標都是基於parent的基礎,是指相對parent的距離。不是相對於整個屏幕的。
通過procession畫的,還處於摸索階段,如果哪位同學知道畫數學函數之類好用的軟件可以推薦一下。
-
API簡介
作用 | Api | 說明 |
---|---|---|
繪製顏色 | drawColor, drawRGB, drawARGB | 通過ARGB設置畫布顏色 |
繪製基本形狀 | drawPoints, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc,drawPath | 繪製點,線,長方形,圓形,橢圓,弧形,以及路徑 |
繪製圖片 | drawBitmap, drawPicture | 繪製位圖 |
繪製文本 | drawText, drawPosText, drawTextOnPath | 繪製各式文本,其中根據路徑繪製比較常用 |
畫布裁剪 | clipPath, clipRect | 可以設置畫布的展示區域 |
畫布狀態 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 此部分api就是保存圖層狀態、 回滾到指定狀態、 獲取保存次數,可以看成類似git保存版本狀態 |
畫布變換 | translate, scale, rotate, skew | 位移、縮放、 旋轉、錯切 |
Matrix | getMatrix, setMatrix, concat | 實際上畫布的位移,縮放等操作的都是圖像矩陣Matrix,通過C++源碼可以看出。在這裏google已經把matrix封裝好,但是如果需要特殊效果,需要自己使用matrix操作,比如窗口抖動等效果沒有封裝好的api可用 |
-
Canvas基本操作
要畫東西,你需要4個基本組件:一個用來容納像素的位圖,一個用來承載畫布的畫布繪製調用(寫入位圖),一個繪製素材(例如Rect,
路徑,文本,位圖),和畫筆(描述顏色和樣式畫)。其中關於繪製基本形狀沒有什麼可說的。其中繪製弧度:
這裏是畫了一個圓弧,其實畫圓弧就是截取一個長方形內切橢圓的一段弧度,代碼如下:
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawArc(canvas);
}
private void drawArc(Canvas canvas){
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
//表示空心,這樣畫出的是線,否則就是實心的圖形
mPaint.setStyle(Paint.Style.STROKE);
canvas.translate(500,500);
RectF rf = new RectF(0,0,300,300);
canvas.drawArc(rf,0,-80,false,mPaint);
canvas.drawRect(rf,mPaint);
}
畫出這個長方形是爲了更好的理解弧度的由來。這裏需要注意的有幾點,1 Rect和RectF都是表示長方形,只是RectF參數是float,Rect參數是int。 2 畫長方形他的四個參數分別是,左,上,右,下,要注意底部的座標必須大於頂部,右部必須大於左部。 3 在ondraw千萬不允許用new函數,因爲ondraw會頻繁調用,如果使用new,分配大量內存,會造成內存抖動,這裏只是爲了演示代碼,所以使用了new。
對於path其實可以看出一連串的點連接而成。 其中還可以包括貝塞爾曲線之類的。我們可以根據曲線來畫文字,比如
代碼如下:
private void drawTexts(Canvas canvas){
Path paths = new Path();
canvas.translate(500,500);
RectF rf = new RectF(0,-400,400,0);
paths.addArc(rf, 60, 180);
canvas.drawPath(paths,mPaint);
mPaint.setTextSize(50);
canvas.drawTextOnPath("中國人民萬歲", paths, 0, -20, mPaint);
}
drawTextOnPath中的3,4個參數是指文字相對path水平和豎直方向的位移。
-
Canvas變化
我們可以大致把canvas的變化分爲四類,位移(translate),旋轉(rotate),縮放(scale),傾斜(skew).
1 translate 位移比較簡單。 就是將當前的原點一定到指定的x,y的位置。
比如:
private void drawTranslate(Canvas canvas){
//初始的時候原點爲(0,0),畫一個圓心爲(200,200)半徑爲100的圓
mPaint.setColor(Color.GREEN);
canvas.drawCircle(200,200,100,mPaint);
//將原點移動到200,200
canvas.translate(200,200);
//移動之後(200,0)就相當於移動前,(400,200.)
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawCircle(200,0,100,mPaint);
}
移動是可以疊加的。 第二次移動是以第一次移動後的原點位置爲標準。其他以此類推。
2 scale,縮放。 api提供了2中縮放的方法:
public void scale(float sx, float sy) {
if (sx == 1.0f && sy == 1.0f) return;
nScale(mNativeCanvasWrapper, sx, sy);
}
public final void scale(float sx, float sy, float px, float py) {
if (sx == 1.0f && sy == 1.0f) return;
translate(px, py);
scale(sx, sy);
translate(-px, -py);
}
可以看出兩種縮放的方式,第一個就直接進行縮放,第二種是以px,py爲原點進行縮放。區別可看如下代碼:
我們做如下效果的縮放:
private void drawScale(Canvas canvas){
canvas.translate(400,800);
mPaint.setColor(Color.RED);
RectF rt = new RectF(0,-300,400,0);
//簡單的 畫2條線當做X,Y軸
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
//首先畫出黑色的基礎矩形
mPaint.setColor(Color.BLACK);
canvas.drawRect(rt,mPaint);
//保存當前設置
canvas.save();
//將座標軸按x軸,y軸分別擴大1.3倍,然後再用藍色畫這個矩形,
//會發現藍色矩形長寬都是原來基礎矩形的1.3被,當這裏設置(0,1)時縮小,(1,+∞)時擴大
canvas.scale(1.3f,1.3f);
mPaint.setColor(Color.BLUE);
canvas.drawRect(rt,mPaint);
//恢復保存以前的狀態。就是canvas沒有放大前的狀態,和save成對出現
canvas.restore();
canvas.save();
//以200,0爲原點進行縮放,將x,y縮小爲原來的0.5倍,然後再畫。
//其實他是分爲三步,第一個進行唯一,translate(200,0),然後縮放scal(0.5,0.5)
//然後在進行位移(-200,0);但是第二次位移在縮放的基礎上了。所以再位移-200,並沒有回到原來的縮放前的原點。
//因爲是在(200,0)的基礎上唯一,x軸縮小0.5,位移-200,變爲-100.所以實際上如果沒有縮放,他是在(200,0)
//的基礎上進行爲(-100,0)的位移。所以雖然看着(200,0)和(-200,0)正好互補,但是因爲縮放的存在不能回到原來了。
//所以紅色的矩形如圖所示的位置
canvas.scale(0.5f,0.5f,200,0);
mPaint.setColor(Color.RED);
canvas.drawRect(rt,mPaint);
//再次回到save之前的狀態,即canvas沒有做任何變化的狀態
canvas.restore();
canvas.save();
//(2,-0.5)x軸變爲原來的2倍,y軸首先變爲原來的0.5被,然後需要y軸的反轉。
canvas.scale(2,-0.5f);
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawRect(rt,mPaint);
//畫一個紫色的變化後的矩形。如圖所示
canvas.restore();
}
我們看最初展示的android源碼可以看出。scale(float sx, float sy, float px, float py)是先進行位移再旋轉,然後再次位移。translate(px, py)移動的物理距離分別是px和py,經過scale(sx, sy)縮放後再通過translate(-px, -py)位移,移動的物理距離就是-px*sx和-py*sy。
所以我們可以看出縮放情況如下:
倍數(n) | 說明 |
---|---|
(-∞, 0) | 首先對x,y軸進行n倍伸縮,然後對應的座標軸 |
(0,+∞) | 直接將對應的x,y軸進行n倍伸縮 |
n==1 | 根據代碼可以看出不做任何變化 |
3 rotate(旋轉),旋轉同樣提供了2中方法
public void rotate(float degrees) {
if (degrees == 0.0f) return;
nRotate(mNativeCanvasWrapper, degrees);
}
public final void rotate(float degrees, float px, float py) {
if (degrees == 0.0f) return;
translate(px, py);
rotate(degrees);
translate(-px, -py);
}
第一種是直接以原點爲中心進行旋轉degrees度, 第二種是以px,py爲原點進行旋轉。它也是分爲3步,1是進行位移(px,py),然後旋轉,之後再進行位移(-px,-py),同理,旋轉之後,表面看兩次位移正好互補,但是不能回到原來的點了。首先來看以原點爲標準旋轉
座標軸變了,所有的座標點也就變了,計算的時候。 你可以仍然以水平爲X軸思考,無論是畫線,還是圖形。在原來X軸上的操作,旋轉之後會和X軸一樣旋轉。比如下圖:
private void drawRotate(Canvas canvas){
canvas.translate(400,800);
mPaint.setColor(Color.RED);
RectF rt = new RectF(0,-300,400,0);
//簡單的 畫2條線當做X,Y軸
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawCircle(500,-100,100,mPaint);
mPaint.setStrokeWidth(30);
mPaint.setColor(Color.GREEN);
canvas.drawPoint(0,0,mPaint);
mPaint.setStrokeWidth(5);
canvas.save();
//旋轉60度
canvas.rotate(60);
mPaint.setColor(Color.RED);
//畫x,y軸還有圓形的代碼和旋轉一直一模一樣
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawCircle(500,-100,100,mPaint);
canvas.restore();
}
通過看代碼我們會發現。 繪畫黑色的座標軸與圓形, 和旋轉之後畫紅色的座標與圓形的代碼一模一樣。 所以無論怎樣旋轉,任何圖形與其座標軸的相對位置不會發生改變。這就是網上很多圓形進度條的原理。如下效果:
這種氣勢就是畫一條水平的線,然後不停的旋轉。
4 傾斜skew
public void skew(float sx, float sy) {
if (sx == 0.0f && sy == 0.0f) return;
nSkew(mNativeCanvasWrapper, sx, sy);
}
參數sx,sy分別是x,y軸上傾斜角度的tan值。比如skew(1,1)則分別是切斜45度
這是x軸切斜45度的效果,代碼如下:
private void drawSkew(Canvas canvas){
canvas.translate(400,800);
RectF rt = new RectF(0,-300,400,0);
//簡單的 畫2條線當做X,Y軸
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawRect(0,0,200,200,mPaint);
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
其實可以把canvas的操作看成是畫很多層,移動之後就屬於在另一層重新畫,無論怎樣移動,都可以想象成再水平面操作。因爲所有的繪畫元素與原點的相對位置不變。
需要注意一點,繪畫的移動,縮放,旋轉,傾斜,都是可以連續多次進行的。多次操作效果疊加。
-
save和restore
實際開發中繪畫的時候經常會有移動,縮放等操作。但是變換完成之後,還想恢復到原來的狀態。再進行繪製。如果沒有save,和restore,比如我們translate(100,20),操作完成之後,還要進行translate(-100,-20)進行復原,這還是一次操作。如果多次操作就會很麻煩。canvas提供save和restore解決了這個問題。save就相當於git中保存狀態,restore就相當於回滾操作。回到save的狀態。
private void drawSaveRestore(Canvas canvas){
//畫一個以300,300位圓心,100位半徑的圓,這個時候座標原點爲左上角(0,0)
mPaint.setColor(Color.RED);
canvas.drawCircle(300,300,100,mPaint);
mPaint.setStrokeWidth(15);
canvas.drawLine(0,0,getRight()-100,0,mPaint);
canvas.drawLine(0,0,0,getBottom()-100,mPaint);
mPaint.setStrokeWidth(8);
//保存這個時候的狀態。即原點爲(0,0)的狀態
canvas.save();
//進行移動,將原點移動到(500,500)的位置
canvas.translate(500,500);
mPaint.setColor(Color.BLACK);
canvas.drawLine(0,0,500,0,mPaint);
canvas.drawLine(0,0,0,500,mPaint);
//畫一個以(500,500)爲原點長寬都是200的正方形
canvas.drawRect(0,0,200,200,mPaint);
//恢復到save的狀態。這個時候,座標原點又是(0,0)了,
canvas.restore();
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawRect(0,0,200,200,mPaint);
}
save和restore是成對出現的。是一對一的。canvas還提供了restoreToCount(int saveCount)方法,去恢復到指定的保存狀態。我們可以查看源碼。一切說的很清楚。
/**
* Efficient way to pop any calls to save() that happened after the save
* count reached saveCount. It is an error for saveCount to be less than 1.
*
* Example:
* int count = canvas.save();
* ... // more calls potentially to save()
* canvas.restoreToCount(count);
* // now the canvas is back in the same state it was before the initial
* // call to save().
*
* @param saveCount The save level to restore to.
*/
public void restoreToCount(int saveCount) {
if (saveCount < 1) {
if (!sCompatibilityRestore || !isHardwareAccelerated()) {
// do nothing and throw without restoring
throw new IllegalArgumentException(
"Underflow in restoreToCount - more restores than saves");
}
// compat behavior - restore as far as possible
saveCount = 1;
}
nRestoreToCount(mNativeCanvasWrapper, saveCount);
}
其實我們可以把save操作當做入棧操作,然後restore可以簡單看做是出棧操作。但是save都有編號,也可以指定編號進行恢復。
-
分層的概念layer
如果做過地圖的項目大家都清楚。在標誌某個地點或者其他類似效果。會通過層的概念在指定地點添加覆蓋物。canvas也有分層的概念,通過savelayer的方式保存,
代碼如下:
private void drawLayer(Canvas canvas) {
canvas.translate(100, 100);
mPaint.setColor(Color.RED);
canvas.drawCircle(75, 75, 75, mPaint);
canvas.saveLayerAlpha(0, 0, 200, 200, 0x88);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(125, 125, 75, mPaint);
canvas.restore();
}
在savexxx的函數中,存在flag的參數,在api28中已經全部改爲:ALL_SAVE_FLAGS,其他已經無效:
/**
* Restore everything when restore() is called (standard save flags).
* <p class="note"><strong>Note:</strong> for performance reasons, it is
* strongly recommended to pass this - the complete set of flags - to any
* call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
* variants.
*
* <p class="note"><strong>Note:</strong> all methods that accept this flag
* have flagless versions that are equivalent to passing this flag.
*/
public static final int ALL_SAVE_FLAG = 0x1F;
private static void checkValidSaveFlags(int saveFlags) {
if (sCompatiblityVersion >= Build.VERSION_CODES.P
&& saveFlags != ALL_SAVE_FLAG) {
throw new IllegalArgumentException(
"Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
}
}
通過這裏的源碼可以看出。
-
總結
其實view的繪製原理是一個很複雜的過程。相信的大家都可以大致說出,measure,layout,draw這三個流程,此篇只是簡單介紹canvas的應用,甚至不用去管他的流程,先明白怎樣畫的即可。所有複雜的view繪製,其實都可以拆分成簡單的操作,繪製可以從簡單做起。