前言
最近年底了,打算把自己的Android知識都整理一下。
Android技能樹系列:
Android基礎知識
Android技能樹 — Android儲存路徑及IO操作小結
資料結構基礎知識
演算法基礎知識
這次是相對View做個小結,主要是View的工作原理,繪製流程等。為什麼要總結這塊,因為平時自定義View的情況多多少少都會遇到,如果能深刻了解這塊知識,對自定義View的掌握才能更透徹。有些人可能會說那我肯定不會的,我也不用看這個總結文章了,沒關係,我這次寫的很簡單,基本大家都能理解。看完後,大家應該都會自己寫效果不復雜的自定義View和自定義ViewGroup。
PS: 非廣告。我本身View的相關知識也是以前從其他地方學到的。我比較推薦這塊內容看(Android開發藝術探索 和 扔物線的View相關內容。所以文中有些的知識點也會引用這二塊地方。)
如下圖所示:我主要是整理了這些相關知識:
我們可以看大分類:
我們知道一個View要繪製好,是要有三步的(我估計百分之99.9的人都知道這三步): measure測量,layout確定位置,然後draw畫出來。所以我這次也是主要這三步來說明的。而大家可能看到這裡有一個額外的ViewRoot的知識點,主要是給前面的三步做個補充知識。
ViewRoot(補充知識)
ps:不看其實問題也不大,不想了解的直接看本文的主要的measure,layout,draw三步曲。
ViewRoot
字面意思是不是讓你感覺是整個ViewTree的根節點。錯!ViewRoot不是View,它的實現類是ViewRootImpl
,它是DecorView
和WindowManager
之間的紐帶。所以ViewRoot
更恰當來說是DecorView
的“管理者”。
(PS:下次面試官問你ViewRoot是啥,你可別說是ViewTree的根節點。哈哈。)
所以這時候既然開始整個介面要繪製了。明顯就是ViewRoot開始發起呼叫方法,畢竟“管理者”麼。所以View的繪製流程是從ViewRoot
的performTraversals
方法開始的。所以performTraversals
方法依次呼叫performMeasure
,performLayout
和performDraw
三個方法。因為這三個方法及後面的方法呼叫都差不多,我們以performMeasure
為例,performMeasure
會呼叫measure
方法,而measure
方法又會呼叫onMeasure
方法(PS:是不是就發現了為啥我們平時都是重寫onMeasure
方法了。),然後又會在onMeasure
方法裡面去呼叫所有子View的measure
過程。
我們可以看到思維腦圖中有提到頂級View就是DecorView
,那DecorView
是什麼呢?
DecorView
是一個FrameLayout,裡面包含了一個豎向的LinearLayout
,一般來說這個LinearLayout是有上下二部分(這裡具體跟Android SDK和主題有關):
是不是看到了熟悉的Content這個名字,沒錯。我們在Activity裡面設定佈局setContentView
就是把我們的佈局加到這個id為android.R.id.content
的FrameLayout
裡面。
我們現在正式進入View整個繪製流程:
View的大小
大家可以看到,為了方便大家理解,我寫了二個現實生活場景故事對比。
故事對比<1>
我們可以看到,我們的氣球放到櫃子裡面,決定氣球大小的因素有二個:櫃子給它的限制,還有它自身的因素(質量好壞,好的能吹的很大)。而我們的View也是一樣的,首先我們用MeasureSpec來決定我們的View大小,那我們的MeasureSpec和氣球一樣,也受到二個因素的影響:
- ViewGroup的影響
- 自身的LayoutParams
總結起來就是一句話:在測量過程中,系統會將View的LayoutParams根據父容器ViewGroup所施加的規則下,轉換得出相對應的MeasureSpec,然後根據這個MeasureSpec來測量出View的高/寬。
可能大家會問什麼是MeasureSpec,別急,我們馬上就來介紹
MeasureSpec知識
其實直接看腦圖,應該就能看得懂吧,主要是這麼幾個知識點:
- MeasureSpec是由SpecMode和SpecSize組合成的。
- SpecMode的種類:UNSPECIFIED,EXACTLY,AT_MOST。
- 普通的View是由父容器限制和自身的LayoutParams來生成相應的MeasureSpec,而DecorView因為是頂層View了。我們可以想象哪來的父容器啊,在外面一層就直接是螢幕了,所以是由螢幕的尺寸和自身的LayoutParams決定。
對比故事<2>
沒錯,通過對比,我們可以發現規律原來很簡單。因為我們腦子裡面可以用這個氣球的對比故事更好的理解。
我做一個總結表格:(要理解上面的分析過程,而不是背下這個表格,背下來沒啥意思)
View的測量
通過上面我們已經知道MeasureSpec是用來確定View的測量的,也已經能根據不同的情況來獲得相應的MeasureSpec了。那我們的到底應該在哪裡去建立MeasureSpec呢?然後給子View去約束呢?
其實奧祕就在我們平時重寫的onMeasure()
方法中:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製程式碼
我們是不是看到了onMeasure
方法裡面傳入了(int widthMeasureSpec, int heightMeasureSpec)
,沒錯,這裡傳入的二個引數,就是當前你重寫這個方法的所在的View(子View或者ViewGroup)的進行過一系列的操作最後獲得的MeasureSpec。
那我們拿到這二個引數後,View還是不知道我們到底給它的寬和高是多少。應該肯定最後是我們呼叫型別:view.setMeasureWidth(XX),view.setMeasureHeight(XX)
這樣,它才能被設定測量的寬和高。沒錯,setMeasuredDimension(int measuredWidth, int measuredHeight)
方法就是我們用來設定view的測量寬和高。
當然你可能會問,那我如果直接呼叫這個方法來設定view的寬和高,那我感覺我不用MeasureSpec
都沒關係啊。比如下面的程式碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//沒有使用相應的MeasureSpec
setMeasuredDimension(100,100);
}
複製程式碼
沒錯,我們可以不是通過正規的測量過程來決定測量的寬和高,我們就是任性的直接定了寬高是100。但是這樣就不符合規則流程了。而且做出來的東西也不會特別好。比如這時候,你在xml中對你的view設定match_parent
,wrap_content
,200dp
就會都無效,因為程式碼最後都是用了100。
onMeasure()方法的構成
我們前面提過,自定義View是要重寫onMeasure()方法的,我們再仔細分析下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我們一般會自己寫的程式碼
........
........
.......
}
複製程式碼
我們可以看到,主要分為二塊:
- super.onMeasure(),
- 自己寫的程式碼。
我們根據不同的情況一步步來看這些程式碼的作用。
super.onMeasure() 分析1 :比如我們的自定義View直接繼承了View.java:
public class DemoView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
}
複製程式碼
我們可以檢視super.onMeasure
方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
複製程式碼
我們看到果然呼叫了setMeasureDimension方法來進行寬高的設定了。
PS:接下來的原始碼這個分析可以不看,直接看結論。嘿嘿。嘿嘿。我知道很多人都不想看。
我們可以看到主要是三個方法(我們這裡就看width的測量):
- 先getSuggestedMinimumWidth方法獲取了某個值。
- 通過getDefaultSize方法來對第一步獲取到的值和約束一起處理後,得到最終值。
- 通過setMeasuredDimension方法把我們最終的值給賦值進去。
1和2的方法先不看,我們起碼知道了。我們最終確定一個View的測量大小,是通過setMeasuredDimension來設定的(其實我感覺我說的廢話,看這個方法的名字就很明確了)。
我們再回頭來看1中的方法:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製程式碼
如果我們的View沒有設定background,則返回的最小值為mMinWidth(啥是mMinWidth?????就是我們在xml設定的android:minWidth
的值)。如果我們設定了background,則獲取mBackground.getMinimumWidth()
(其實這個方法就是返回Drawable的原始寬度)。最後返回max(mMinWidth, mBackground.getMinimumWidth())
二者中的最大值。
我們再來看2中的方法:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製程式碼
其實上面我們的MeasureSpec的建立規則會的話,其實應該就能看的懂。如果是specMode是UNSPECIFIED
,則返回我們1中的方法getSuggestedMinimumWidth
獲取到的值,如果是AT_MOST
和EXACTLY
,則直接返回specSize。(View原始碼這裡的寬度的建立規則和我們前面講的測量的規則區別就在於,當specMode是UNSPECIFIED
的時候,返回的是getSuggestedMinimumWidth
的值,而我們是返回了0。)
結論1:如果寫的自定義View是直接繼承View的,而且寫了super.measure(),則會預設給這個View設定了一個測量寬和高(這個寬高是多少?如果沒有設定背景,則是xml裡面設定的android:minWidth/minHeight(這個屬性預設值是0),如果有背景,則取背景Drawable的原始高寬值和android:minWidth/minHeight二者中的較大者。)
super.onMeasure() 分析2 :比如我們的自定義View繼承了現有的控制元件,比如ImageView.java:
public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
複製程式碼
這時候我們的super.onMeasure()
方法呼叫的就是ImageView
裡面的onMeasure
方法了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//ImageView 的一大堆計算寬高的程式碼。
......
......
......
//當然最終肯定要把算好的寬高告訴View
setMeasuredDimension(widthSize, heightSize);
}
複製程式碼
我們發現如果我們的View直接繼承ImageView,ImageView已經執行了一大堆已經寫好的程式碼測出了相應的寬高。我們可以在它基礎上更改即可。
比如我們的Image2View是一個自定義的正方形的ImageView,:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這裡已經幫我們測好了ImageView的規則下的寬高,並且通過了setMeasuredDimension方法賦值進去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我們這裡通過getMeasuredWidth/Height放來獲取已經賦值過的測量的寬和高
//然後在ImageView幫我們測量好的寬高中,取小的值作為正方形的邊。
//然後重新呼叫setMeasuredDimension賦值進去覆蓋ImageView的賦值。
//我們從頭到位都沒有進行復雜測量的操作,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
複製程式碼
結論2:如果寫的自定義View是繼承現有控制元件的,而且寫了super.measure(),則會預設使用那個現有控制元件的測量寬高,你可以在這個已經測量好的寬高上做修改,當然也可以全部重新測過再改掉。
super.onMeasure() 分析3:我們寫的自己的程式碼與super.measure的前後位置關係
我們可以看到,不管你是繼承View還是現有的控制元件(比如ImageView),super.onMeasure()
中都預設會按照自己的邏輯測量一個寬和高,然後呼叫setMeasuredDimension()
方法賦值進去。
- 如果我們的自己的程式碼寫在super.measure前面,那麼你寫的測量的邏輯測定好寬高,並且賦值後,最終都會再次被super.measure中的setMeasuredDimension()所覆蓋。
- 如果我們的自己的程式碼寫在super.measure後面,你可以在你繼承的父類的測量結果的基礎進行更改(當然你不用父類的測量結果也是沒關係的),然後再次呼叫
setMeasuredDimension()
賦值。 - 如果你的測量寬高的邏輯,不是基於你繼承的控制元件的測量的基礎上進行,完全由你來重新測定的話,super.onMeasure()不寫也不會有問題。
具體實現自定義View的測量
1. 比如我們直接是繼承現有的控制元件,比如ImageView,實現一個正方形的ImageView(上面已經提到過了):
public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這裡的super.onMeasure()方法裡面,已經是呼叫了ImageView的onMeasure()方法。
//所以已經進行了測量了。並且在這個方法最後呼叫了setMeasuredDimension(widthSize, heightSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//所以你不寫任何東西,這個測量結果都已經確定過了,因為已經執行過了setMeasuredDimension。
//但比如你想要在ImageView的基礎上,讓這個ImageView變成一個正方形的ImageView。
//因為測出來的寬高可能不同,是一個矩形。我們就需要手動的再去設定一次寬和高。
int width = getMeasuredWidth();//獲取ImageView原始碼裡面已經測量好的寬度
int height = getMaxHeight();//獲取ImageView原始碼裡面已經測量好的高度
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
}
複製程式碼
我們發現,我們是在已經我們繼承的現有的控制元件幫我們測量好寬高後,可以再次在這個已經測量好的寬高的基礎上進行更改。我們並沒有用到我們前面學到的MeasureSpec的知識,因為super.onMeasure()中已經幫我們把MeasureSpec處理好了。
2. 比如我們自己直接繼承了View:
public class CircleView extends View {
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//View測量寬高的三步曲
//1.設定預設值,wrap_content的情況下的值。
//因為wrap_content只是說不超過某個最大值,如果不設定預設值,效果與Match_parent一樣了。
int defaultWidthSize = 200;
int defaultHeightSize = 200;
//2.呼叫resolveSize()方法,把MeasureSpec和我們的預設值放進去
//這個方法返回一個最終根據你傳入的預設值及MeasureSpec共同作用後的最終結果
defaultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
defaultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//呼叫setMeasuredDimension方法賦值寬和高
setMeasuredDimension(defaultWidthSize, defaultHeightSize);
}
}
複製程式碼
是不是超級超級超級簡單。大家可能就會問,那個resolveSize()
方法是什麼,怎麼這麼神奇。
PS:下面的resolveSize()原始碼分析不看也沒啥關係,反正會用就行了。哈哈,不影響使用。
我們可以來看下它的原始碼:
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//1.拿到specMode 和 specSize
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//2.根據不同的specMode來進行判斷最終值是什麼
switch (specMode) {
case MeasureSpec.AT_MOST:
/*
2.1如果specMode是AT_MOST模式,我們本來應該直接是specSize
但是如果我們的預設值比我們的specSize大就很尷尬了。氣球預設的大小都裝不進櫃子了。這時候我們View的大小要設定成specSize,如果預設大小比我們的specSize小就沒關係,直接為預設值。
*/
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
/*
2.2如果是EXACTLY,直接就是specSize
*/
case MeasureSpec.EXACTLY:
result = specSize;
break;
/*
2.3如果是UNSPECIFIED模式,則直接就是我們設的預設值
*/
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製程式碼
3. ViewGroup的測量
在講ViewGroup的測量前面,我要提問個問題,大家應該知道了某個View的MeasureSpec在是在onMeasure()方法的引數裡面傳進來的。我們是直接拿來用了。那又是那裡呼叫了onMeasure()方法幫忙把這二個引數帶進來的呢。這二個引數又是哪裡生成的呢?
答案就是這個子View的父容器給它的。父容器在他自己的onMeasure()方法裡面會根據自己的onMeasure()傳進來的MeasureSpec,及這個子View的自身的LayoutParams情況,生成相應的childMeasureSpec,然後呼叫子View的measure()傳遞進去的(前面提過,measure()方法會呼叫onMeasure()方法。)
比如我們寫一個圓形排布的ViewGroup(LinearLayout是一排的排布)。
public class CircleLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.父容器的onMeasure()傳進來的二個引數widthMeasureSpec和 heightMeasureSpec
//2.還差子View的LayoutParams,獲取子View的LayoutParams
//3.通過二者產生新的MeasureSpec然後給子View。
//4.而產生的新的ChildMeasureSpec的規則就是我們前面表格總結過的規則。
/*
PS:下面這段是我寫的程式碼,並不是正確的,因為父容器可能包含多個子View,
所以到某個子View的時候,給它的specSize應該是父容器的剩餘空間,
所以傳入的父容器的可用空間本來是不停的減少的,外加還有margin,padding值也要減去。
我就是主要意思下,讓大家懂得原理。
*/
//先判斷初始時候父容器的大小,因為父容器也是個View,所以也是三步曲。
//設定預設值(可以是0,因為父容器一般預設不會佔有空間)
int defaultWidthSize = 500;
int defaultHeightSize = 500;
//resolveSize處理獲取寬和高
int resultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
int resultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//比如我們這裡以width為例子:
//我們前面提過了,最終給子View的MeasureSpec是由父View的MeasureSpec與子View的LayoutParam共同確定。
//先獲取父View的MeasureSpec的mode和size
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//根據不同的SpecMode及子View的LayoutParams來產生新的ChildMeasureSpec。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
LayoutParams params = view.getLayoutParams();
int childWidthSpec, childHeightSpec;
//先根據父View的MeasureSpec來進行大分類:
switch (specMode) {
case MeasureSpec.EXACTLY:
//說明是固定值,比如100dp等
if (params.width >= 0) {
resultSize = params.width;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = specSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = specSize;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
.....
.....
break;
case MeasureSpec.UNSPECIFIED:
.....
.....
break;
}
childWidthSpec = MeasureSpec.makeMeasureSpec(resultWidthSize, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthSpec, childHeightSpec);
}
/*
可能有人說,生成新的規則我都懂,但是每次都要寫上面一大段的程式碼,
我不想寫自定義ViewGroup了。我還是放棄吧,別急,大家也發現上面的規則的確是固定的。
那有沒有類似我們在上面設定自己寬高時候的類似resolveSize的方法呢。
如果沒有特定的需求,的確我們不需要寫上面一大段。
有二種方法。
*/
//方法1:可以通過呼叫measureChildren()一下子把所有的子View測量好
measureChildren(widthMeasureSpec, heightMeasureSpec);
//方法2:通過measureChild()一個個來測量。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
measureChild(view , widthMeasureSpec,heightMeasureSpec);
}
//設定父容器的大小
setMeasuredDimension(XXXX,XXXX)
}
}
複製程式碼
沒錯,最後我們可以用measureChildren(widthMeasureSpec, heightMeasureSpec);
和measureChild(view , widthMeasureSpec,heightMeasureSpec);
方法來,我們也知道它的內部肯定也是根據相應的規則,生成對應的childMeasureSpec,然後呼叫child的measure方法。
我們可以看下原始碼(PS:不想看還是沒關係,可以跳過):
//measureChildren其實只是幫我們遍歷了所有的View,幫我們把可見的View分別呼叫measureChild方法來處理。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//而measureChild方法裡面就是獲取子View的LayoutParams和傳進來的MeasureSpec,
//把這二者通過getChildMeasureSpec方法獲得一個新的childMeasureSpec,然後傳給child.measure方法。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
如果具體想看getChildMeasureSpec
做了什麼,可有再去看下原始碼,但是他們生成的規則跟我們前面講的還是一樣的。我這裡不多說了。
測量完後獲取View的寬和高
這個就十分簡單了。直接看腦圖即可。
View的位置
這塊比較簡單,我也不多說了。(別吐槽我,這文章太多了。寫太多沒人會耐心看完。)
View的繪製
我們都知道View的大小和位置都確定好了,肯定就差繪畫了。
View 繪畫draw()
我們都知道是通過draw()方法來繪製的。
而draw()方法具體做了什麼呢,我們可以看原始碼這個方法的工作過程的介紹:
draw()原始碼裡面的介紹:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
複製程式碼
分別是先繪製背景,然後繪製自己的內容,然後繪製子View的內容,最後畫裝飾和前景。
推薦大家看扔物線大佬的文章,講的很清楚,我就不花大篇幅寫基礎了。
HenCoder Android 自定義 View 1-5: 繪製順序
Canvas的使用
我們知道不管是onDraw(Canvas canvas)
,dispatchDraw(Canvas canvas)
,onDrawForeground(Canvas canvas)
等都是引數是Canvas(畫布)。所以我們知道了是用Canvas來繪畫。
這裡也是推薦扔物線大佬的相關文章,講的很細,我也不再大篇幅的寫各種基礎使用知識。
HenCoder Android 開發進階: 自定義 View 1-1 繪製基礎
HenCoder Android 開發進階:自定義 View 1-4 Canvas 對繪製的輔助
Canvas怎麼使用呢: 主要分為二大塊:
Canvas繪製類方法
這塊很簡單,直接用Canvas來畫顏色,畫矩形,畫圓形,畫直線等各種圖形。雖然簡單,但畢竟這才是基本的繪製,用的最多。Canvas的輔助類方法
其中幾何變化又分為二維變換和三維變換:
Paint相關
我們知道Paint是畫筆,我們可以設定顏色,畫筆粗細等。
繼續推薦扔物線大佬的相關文章(基礎我就不寫了):
HenCoder Android 開發進階: 自定義 View 1-2 Paint 詳解
顏色相關
效果
繪製文字相關
Paint初始化部分相關
結語
有錯誤的地方,請大家輕點噴,我膽子很小的。。。。