ps :在系統(tǒng)的學(xué)習(xí)自定義 view 之前,搞懂本本篇的內(nèi)容會讓你學(xué)習(xí)的過程順序,簡單很多
view 的分類
view 其實就2種:
- 單一視圖 view
我們常用的各種具體控件都是 view ,比如 TextView - 視圖容器 ViewGroup
視圖容器簡單說就是各種 layout 布局,里面用來存放 view ,給 view 定位的。想我們常用的 LinearLayout 就是視圖容器
自定義 view 的3個核心方法
- onMeasure
根據(jù) view 的測量模式計算確定 view 的寬高 - onLayout
ViewGroup 中對所有的子 view 排版,決定子 view 的位置 - onDraw
具體繪制 view
這3個方法就是自定義的核心了,自定義 view 不管我們怎么寫,基本都是圍繞這3個方法玩。view 沒有 onLayout 方法,因為 view 不是容器里面放不了 view ,只有 ViewGroup 才有 onLayout 方法
自定義 view 所有生命周期及回調(diào)函數(shù)
- onFinishInflate()
當(dāng)應(yīng)用從XML加載該組件并用它構(gòu)建界面之后調(diào)用的方法 - onMeasure()
檢測View組件及其子組件的大小 - onLayout()
當(dāng)該組件需要分配其子組件的位置、大小時 - onSizeChange()
當(dāng)該組件的大小被改變時 - onDraw()
當(dāng)組件將要繪制它的內(nèi)容時 - onKeyDown
當(dāng)按下某個鍵盤時 - onKeyUp
當(dāng)松開某個鍵盤時 - onTouchEvent
當(dāng)發(fā)生觸屏事件時 - onWindowFocusChanged(boolean)
當(dāng)該組件得到、失去焦點時 - onAtrrachedToWindow()
當(dāng)把該組件放入到某個窗口時 - onDetachedFromWindow()
當(dāng)把該組件從某個窗口上分離時觸發(fā)的方法 - onWindowVisibilityChanged(int)
當(dāng)包含該組件的窗口的可見性發(fā)生改變時觸發(fā)的方法
自定義 view 需要注意的生命周期及回調(diào)函數(shù)
注意這里,面試會問
void onFinishInflate()
當(dāng)系統(tǒng)解析XML中聲明的View后回調(diào)此方法,調(diào)用順序:內(nèi)層View->外層View,如果是viewgroup,適合在這里獲取子View。
注意點:
如果View沒有在XML中聲明而是直接在代碼中構(gòu)造的,則不會回調(diào)此方法
此時無法獲取到View的寬高和位置void onAttachedToWindow()
當(dāng)view 被添加到window中回調(diào),調(diào)用順序:外層View->內(nèi)層View。在XML中聲明或在代碼中構(gòu)造,并調(diào)用addview(this view)方法都會回調(diào)該方法。
注意點:
此時View僅僅被添加到View,而沒有開始繪制所以同樣獲取不到寬高和位置void onDetachedFromWindow()
看名字就知道是與void onAttachedToWindow();對應(yīng)的方法,在VIew從Window中移除時回調(diào),如執(zhí)行removeView()方法。
注意點:
如果一個View從window中被移除了,那么其內(nèi)層View(如果有)也會被一起移除,都會回調(diào)該方法,且會先回調(diào)內(nèi)層View的onDetachedFromWindow()方法void onWindowFocusChanged(boolean hasWindowFocus)
當(dāng)View所在的Window獲得或失去焦點時被回調(diào)此方法。除了常見的設(shè)置view的onGlobalLayoutListener,也可以通過這個方法取到VIew的寬高和位置;也適合在判斷當(dāng)失去焦點時停止一些工作,如圖片輪播,動畫執(zhí)行等,當(dāng)獲取到焦點后繼續(xù)執(zhí)行。
hasWindowFocus:View所在Window是否獲取到焦點,當(dāng)該Window獲得焦點時,hasWindowFocus等于true,否則等于false。該方法在當(dāng)前View或其祖先的可見性改變時被調(diào)用
更多詳細請看:
自定義 view 的種類
繼承現(xiàn)成 view 控件
比如我們寫一個自定義 view 繼承 textview ,這樣難度小,view 的3個核心方法我么你都不用關(guān)心,不過一般這樣寫都是為了給某個控件添加額外功能,難度小,但是不具有普遍適應(yīng)性。直接繼承 view
直接繼承 view ,view 的 onMeasure 測量 ,onDraw 繪制都需要我們自己來做,很考驗功底的,里面又會涉及到大量的動畫操作,是非常難得,學(xué)好了能大大提升我們的代碼水平繼承現(xiàn)成 ViewGroup 容器
難度小,一般我們都是做組合類型的 view 時用,多用于封裝 app 中的公共基礎(chǔ) UI 組件,雖然難度低,但是具有普遍性直接繼承 ViewGroup 容器
難度最大,ViewGroup 容器的工作在于給子 view 確定位置,給子view 排版,需要大量的計算操作,還要精確考慮 magin,padding 的問題,很難,一般很少這樣做,都是對自己有信心的人才回去嘗試,技術(shù)不熟練的先不要來了
view 的多個構(gòu)造方法
view 的構(gòu)造方法有4個,分別面對不同的使用情況,我們在自定義 view 時要知道在哪個構(gòu)造的方法里做初始化,其實一般我們都是在這4個方法里面都寫初始化方法的
// 如果View是在Java代碼里面new的,則調(diào)用第一個構(gòu)造函數(shù)
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里聲明的,則調(diào)用第二個構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進來的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不會自動調(diào)用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用,不會自動調(diào)用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
注意:即使你在View中使用了Style這個屬性也不會調(diào)用三個參數(shù)的構(gòu)造函數(shù),所調(diào)用的依舊是兩個參數(shù)的構(gòu)造函數(shù)。
1.public View(Context context)
2.public View(Context context, @Nullable AttributeSet attrs)
3.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
4.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
構(gòu)造方法嚴(yán)格來說不算回調(diào),但除了方法一外都不需要我們手動調(diào)用,而且是自定義View僅有的必須要聲明的方法。
- 構(gòu)造方法1
當(dāng)不在布局文件中聲明而在代碼中創(chuàng)建View時調(diào)用的方法 - 構(gòu)造方法2
當(dāng)在布局文件中聲明,且沒有在styles.xml中預(yù)設(shè)主題級或item級的默認(rèn)屬性時調(diào)用。attrs就是一組布局文件中的值(包括默認(rèn)屬性和自定義屬性) - 構(gòu)造方法3
當(dāng)在布局文件中聲明,在attrs.xml中有聲明一個屬性,并在styles.xml中的主題item聲明這個屬性的值(即View的一組默認(rèn)屬性)調(diào)用 - 構(gòu)造方法4
當(dāng)在布局文件中聲明,在styles.xml中的主題里沒有聲明但單獨聲明了View的一組默認(rèn)屬性時調(diào)用。
屬性賦值優(yōu)先級:Xml定義 (方法二)> Xml的style定義(方法二) > defStyleAttr (方法三)> defStyleRes> theme直接定義(方法四)
構(gòu)造方法四要求api21以上,所以我們一般采用構(gòu)造方法二(沒有默認(rèn)屬性)或構(gòu)造方法三(有默認(rèn)屬性)
view 的視圖層級
我們連帶著把 Actvity 的 視圖層級一起寫一下吧,下面這張就是 Actvity 視圖層級

我們在 xml 布局文件中聲明的布局根節(jié)點并不是 Activity 的視圖根節(jié)點,是上圖中的 contentView 的位置,contentView 上面的都是 Activity 內(nèi)部添加的,我們控制不了,但是我們需要了解,一些頁面效果我們需要操作 DecorView
這張圖是常見的 view 視圖層級

ViewGroup 里面還可以再放 ViewGroup,但是 view 里面就不能放任何view 了
無論是measure過程、layout過程還是draw過程,永遠都是從View樹的根節(jié)點開始測量或計算(即從樹的頂端開始),一層一層、一個分支一個分支地進行(即樹形遞歸),最終計算整個View樹中各個View,最終確定整個View樹的相關(guān)屬性
上面圖中有一層就表示視圖有一個層級,視圖層級越多就會加重 cpu 計算負(fù)荷,這個不是線性的關(guān)系,是幾何層級的關(guān)系。ViewGroup 寬高采用 warp_content 時,會跑2次這個 ViewGroup 所屬子 view 的 onMeasur 方法,會大大增加任務(wù)量。所以我們在寫布局時,層級過多或是 warp_content 應(yīng)用過多,都會造成頁面加載計算大,頁面卡頓,這是我們需要優(yōu)化的一個點。
Android 坐標(biāo)系
- 屏幕的左上角為坐標(biāo)原點
- 向右為x軸增大方向
- 向下為y軸增大方向
詳細看下圖:

和數(shù)學(xué)坐標(biāo)系的 Y 軸方向是不同的
view 的坐標(biāo)
view 有3套描述坐標(biāo)位置的方式:
- Left,Top,Right,Bottom
- x,y,translationX、translationY
- rawX ,rawY
1. Left,Top,Right,Bottom
這4個描述的是 view 的左右上下到 view 所在父控件左上角的位置
- Top:子View上邊界到父view上邊界的距離
- Left:子View左邊界到父view左邊界的距離
- Bottom:子View下邊距到父View上邊界的距離
- Right:子View右邊界到父view左邊界的距離
相關(guān)的 API :
getTop(); //獲取子View左上角距父View頂部的距離
getLeft(); //獲取子View左上角距父View左側(cè)的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側(cè)的距離
詳細看下圖:

需要注意的是 right = left + view 的 width , bottom = top + view 的 height
2. rawX ,rawY
描述的是 view 左上角到屏幕左上角的距離
這個可以用 MotionEvent 中 get 和 getRaw 的區(qū)別來學(xué)習(xí)
相關(guān) API :
event.getX(); //觸摸點相對于其所在組件坐標(biāo)系的坐標(biāo)
event.getY();
event.getRawX(); //觸摸點相對于屏幕默認(rèn)坐標(biāo)系的坐標(biāo)
event.getRawY();
詳細看下圖:

3. x,y,translationX、translationY
從android3.0開始,View增加了額外幾個參數(shù):x,y,translationX、translationY。其中x和y是View左上角的坐標(biāo),translationX和translationY是View左上角相對于父容器的偏移量,它們默認(rèn)值是0。這些參數(shù)也是相對于View父容器。具體關(guān)系見下圖:

x = left + translationX,y = top + translationY
x和left不同體現(xiàn)在:left是View的初始坐標(biāo),在繪制完畢后就不會再改變;而x是View偏移后的實時坐標(biāo),是實際坐標(biāo)。y和top的區(qū)別同理。
如何獲取 view 的寬高
獲取 view 的寬高有2套 API:
- getWidth() / getHeight():獲得View最終的寬 / 高
- getMeasuredWidth() / getMeasuredHeight():獲得 View測量的寬 / 高
他們的區(qū)別:

getMeasuredWidth() 方法可以在 view 的 onLayout 方法里使用,onLayout 在 onMeasure 之后跑,這時候 measuredWidth view 的寬是計算出來的,但是我們要考慮 view 申請的大小超過父控件最大值的問題。
我們可以考慮在 onSizeChange 方法內(nèi)記錄 view 的大小,這也是一種辦法。這2套獲取寬高的 API 最終的結(jié)果值都一樣,區(qū)別在于產(chǎn)生數(shù)據(jù)的時機不同。
getWidth() / getHeight() 只有在 view 計算完并顯示之后才能返回具體的值,其他時候返回的都是 0,所以 getWidth() / getHeight() 一般我們都是做延遲使用,等待 view 計算顯示完畢
獲取 view 寬高的時機不同,所依賴的方法也是不同的,具體的我就不寫了,大家看這個:
另外還有一點要清楚:getMeasuredXXX() 有時并不 = getXXX() ,下面這段話足以解釋
getMeasuredXXX() 與 getXXX() 的區(qū)別和聯(lián)系所在。說得直白一點,measuredWidth 與 width 分別對應(yīng)于視圖繪制 的 measure 與 layout 階段。很重要的一點是,我們要明白,View 的寬高是由 View 本身和 parent 容器共同決定的,要知道有這個 MeasureSpec 類的存在。
比如,View 通過自身 measure() 方法向 parent 請求 100x100 的寬高,那么這個寬高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 階段,通過 childview.layout() 方法只分配給 childview 50x50 的寬高。那么,這個 50x50 寬高就是 childview 實際繪制并顯示到屏幕的寬高,也就是 width 和 height 值。
如果你對自定義 View 過程很熟練的話,理解這部分內(nèi)容就比較輕松一些。事實上,開發(fā)過程中,getWidth() 和 getHeight() 方法用的更多一些。
Android 的角度 (angle) 與弧度 (radian)
android 的角度和弧度有其需要說上一說, android 里的角度和我們平時的習(xí)慣是嫌煩的,這點很坑爹
另外這塊涉及到畫布的相關(guān)操作(旋轉(zhuǎn))、正余弦函數(shù)計算等,即會涉及到角度(angle)與弧度(radian)的相關(guān)知識。
另外記住縮寫:
- deg --> 角度
- rad --> 弧度
角度,弧度的詳細描述:

android 角度方向是順時針的:

Android 中顏色部分
Android 支持一下幾種顏色模式:

ARGB 表示4位顏色通道,RGB 表示3位顏色通道,RGB 相比 ARGB 少了透明的顏色通道,需要注意的是 ARGB8888 4位通道的圖片若是轉(zhuǎn)成 RGB565 3位通道的圖片格式,是會造成圖片色差的,A 透明顏色通道用的越多色差越嚴(yán)重
4位顏色通道含義:

- java中定義顏色
//java中使用Color類定義顏色
int color = Color.GRAY; //灰色
//Color類是使用ARGB值進行表示
int color = Color.argb(127, 255, 0, 0); //半透明紅色
int color = 0xaaff0000; //帶有透明度的紅色
- xml文件中定義顏色
<?xml version="1.0" encoding="utf-8"?>
<resources>
//定義了紅色(沒有alpha(透明)通道)
<color name="red">#ff0000</color>
//定義了藍色(沒有alpha(透明)通道)
<color name="green">#00ff00</color>
</resources>
- java文件中引用xml中定義的顏色
//方法1
int color = getResources().getColor(R.color.mycolor);
//方法2(API 23及以上)
int color = getColor(R.color.myColor);
- xml文件(layout或style)中引用或者創(chuàng)建顏色
<!--在style文件中引用-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
<!--在layout文件中引用在/res/values/color.xml中定義的顏色-->
android:background="@color/red"
<!--在layout文件中創(chuàng)建并使用顏色-->
android:background="#ff0000"