文末有Android進階面試資料免費分享
1. 事件分發機制大家應該都熟記於心,默認事件分發是逆序的,有哪些方法可以修改分發順序?
記得曾經有位朋友做貼紙應用時,有RT 的需求。默認事件分發為逆序,遍歷子 View 為 (childCount ~ 0 ],有哪些方式可以修改這一策略,比如修改遍歷方式為[0,childCount)?
修改事件分發順序的話,在日常開發中基本遇不到,因為現在的逆序遍歷,是跟View的層級顯示相匹配的,隨便更改反而不太合理。
如果非要修改這個順序,很多同學首先會想到:
重寫dispatchTouchEvent方法,然後在裡面一個for循環,從0開始一個個調用子View的dispatchTouchEvent。
這個方法,不是說絕對不行,只是你要做的事情很多,就比如觸摸座標的轉換:
我們都知道,ViewGroup在分派事件的時候,會檢查子View是否應用過屬性動畫的(位移、縮放、旋轉等),如果有的話還要把座標給映射回去 。
接著,還會把相對於這個ViewGroup本身的觸摸座標 轉換成 相對於對應子View的觸摸座標。
這樣說可能有點繞,舉個例子:
比如:當手指在屏幕中按下,ViewGroup中收到的event座標(getX,getY)假設是【500,500】,剛好在這個位置上有個子View,那接下來肯定會把事件傳給這個子View的dispatchTouchEvent,這時候如果座標不轉換直接傳的話,那子View收到的event座標(getX,getY)也是【500,500】,這明顯是不對的,正確的座標應該要分別減去它的left和top。
這看起來好像沒什麼大的影響,但如果你的子View沒有重寫onTouchEvent方法的話(比如子View是常用的ImageView,TextView之類的),你的OnClickListener就會無效了,因為默認的onTouchEvent在處理ACTION_MOVE的時候,會檢查event的座標是否已經脫離了View的邊界範圍,如果在邊界範圍之外的話,pressed將會失效(認為沒有被按下),當ACTION_UP時,如果pressed為false,就不會執行PerformClick。
那難道沒有方法可以完美地做到了嗎?
在ViewGroup的dispatchTouchEvent方法中,雖然它是逆序的for,但是呢,它把子View拿出來的時候,卻不是直接操作的mChildren數組,而是通過一個getAndVerifyPreorderedView方法來獲得,這個方法會把當前索引傳進去,還有一個preorderedList。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
final ArrayList<view> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
}
/<view>
如果傳進去的preorderedList不為空,那麼就會直接從它裡面去取。
preorderedList怎麼來?
通過調用buildOrderedChildList方法獲取的。
buildOrderedChildList方法是怎麼樣的?
ArrayList<view> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
/<view>
它裡面是通過一個getAndVerifyPreorderedIndex方法來獲取對應的子VIew索引,這個方法要傳進去一個叫customOrder的boolean。
這個customOrder,看名字可以知道,是自定義順序的意思,如果它為true的話,接著會通過getChildDrawingOrder(int childCount, int i)方法來獲取對應的索引,而且,這個方法是protected的,所以我們可以通過重寫這個方法並根據參數"i"來決定返回哪一個View所對應的索引,從而改變分發的順序。
protected int getChildDrawingOrder(int childCount, int i) {
return i;
}
那這個customOrder,什麼時候為true呢?
在buildOrderedChildList方法裡可以看到這麼一句:
final boolean customOrder = isChildrenDrawingOrderEnabled();
emmmm,也就是說,如果要自定義這個順序的話,還需要調用setChildrenDrawingOrderEnabled(true)來開啟。
重新捋一捋流程:
1. setChildrenDrawingOrderEnabled(true)來開啟自定義順序;
2. 重寫getChildDrawingOrder方法來決定什麼時候要返回哪個子View;
2. AppCompatTextView 與 TextView 有什麼區別?
1. compat庫是如何將TextView替換為AppCompatTextVew的?
2. 為什麼要進行替換?
3. 根據替換相關原理,我們可以做哪些事情?
先從第二問開始吧:
AppCompatTextView繼承自TextView,是對TextView的一種擴展,因為在5.0中首次推出了MaterialDesign這種設計風格。
但是眾所周知的,5.0推出不可能所有的設備全都一下子更新到最新版本,為了在早期版本上實現新的功能(這些新功能比如從源碼註釋中解讀到比如backgroundTint屬性,根據文本內容自適應大小等).
即為了新特性同樣可以兼容老版本,framework在創建TextView實例的時候,自動幫我們進行了替換。
其它的AppCompatXXX與XXX的關係也是如此。
第一問:
然後第一問,如何完成替換的,我們這裡只拿最直觀的流程舉例,且儘可能的簡化源碼過程,在討論這個問題之前,先了解幾個預備知識:
View是怎麼被解析創建出來的:
1.LayoutInflater:將佈局XML文件實例化為其對應的View對象,我們在Activity中通過setContentView傳入一個Layout的資源文件id,最終該方法最終會調用到PhoneWindow的setContentView方法,這個方法裡面有調用到
mLayoutInflater.inflate(layoutResID, mContentParent);
2.inflate方法,該方法的作用是將指定的XML文件填充到View的層次結構中去,最終無論通過什麼途徑調用到inflate方法,都會走到三個參數的重載方法這裡:
return inflate(parser, root, attachToRoot);
parser你可以認為持有將Layout.XML解析後的數據。後兩個參數的意義如下:
- 1. root為null,attchToRoot無意義,inflate返回的是當前XML對應的根佈局。
- 2. root不為null且attachToRoot為true,則整個XML對應的佈局就設置了根佈局是root。
- 3. root不為null且attachToRoot為false,則會將root的layoutParames設置給當前XML的佈局。
知道了LayoutInflate.inflate做了什麼,再往下,inflate中會調用createViewFromTag,從方法名就能知道,繼續往下走,我們離答案越來越近了。
createViewFromTag做的事情非常有意思:
先看到787行這個if-else,條件是name中有沒有"."字符,如果有我們會執行onCreateView,如果沒有會執行createView。
name啥時候有點?
自定義控件的時候。
當是系統控件的時候,createView會有一個填充了第二個參數的調用:
createView(name, "android.view.", attrs);補上了View控件的全路徑名,而自定義控件則不需要,因為傳入的name就是一個全路徑名。
為什麼要全路徑名?
因為View控件對象的創建是通過反射來實現的:
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
// ...
args[1] = attrs;
final View view = constructor.newInstance(args);
下面對這幾步做一個總結:
XML中保存了ViewTree的結構和View的相關標籤信息(包括View的類型和一些屬性值),然後這些信息會在後面通過反射的方式(如果沒有Factory2和Factory的話)創建實例對象,如果創建的是ViewGroup,則會對它的子View遍歷重複創建步驟,創建完View對象後,會add到對應的ViewGroup中。
其中相關方法的調用流程是:
inflate->rInflate->createViewFromTag->createView。
好像還是沒有看到替換?
還是上一張圖,我們只解釋了後半部分,沒有解釋前半部分,那麼什麼是Factory?
繼續往下看:
createViewFromTag中會先判斷有沒有Factory或者Factory2的對象,如果有,則調用Factory的onCreateView方法。
這兩個類都是接口,其中Factory2是Factory的子接口,都只有唯一一個onCreateView方法。
不同之處在於Factory2的onCreateView方法傳入了parentView。
該方法的作用就是你可以藉助它來改造XML中已經存在了的Tag的值。所以Factory2可以達到改造parentView的目的。
但是我們在日常中根本就沒有任何地方接觸到了Factory(2)呀,那麼它是不是就直接是null呢?
到這裡又是一番源碼調來調去,為了便於理解,只需要知道,這個東西(Factory2),在最開始AppCompatActivity(為了兼容低版本,我們現在Activity默認都是繼承自它)中的onCreate方法中就已經通過層層調用被設置好了。
既然現在Factory2不為空,那麼就應該去走它的onCreateView方法了,這裡又是層層調用,最終來到了AppCompatViewInflater**** 的 createView 方法:
答案就在這裡:
如果創建的是非兼容控件(系統控件那麼多,實現兼容的只是常用的一些控件),那麼就會是143行,在146中通過反射創建View對象。
囉裡囉唆扯了一大堆,還是沒回答第一個問題:
compat庫是如何將TextView替換為AppCompatTextVew的?
個人對這個的理解:在將XML文件解析成包含ViewTree信息之後,開始利用這些信息去創建每一個View節點,在創建View對象的時候,如果發現這個節點是屬於支持兼容的控件比如TextView,那麼就會去調用到new AppCompatTextView()來創建一個兼容的View對象,也就是在創建的時候,及已經實現了替換。
第三問:
根據替換相關原理,我們可以做哪些事情?
整個替換從圖一所示的源碼中可以看到,能夠被替換的關鍵是Factory(2)存在,那麼我覺得,其實問題問的是Factory(2)可以用來做什麼吧?
那麼這個時候,就適合去問站長大人了:
§ 探究 LayoutInflater setFactory
3. getWidth, getMeasuredWidth 有什麼區別?
getWidth和getMeasuredWidth的區別:
getMeasuredWidth方法返回的是測量後的寬度,這個寬度是當setMeasuredDimension方法(measure方法最終會調用setMeasuredDimension)被調用後刷新的。
而getWidth返回的是最終layout出來的寬度,在View代碼中返回的是【mRight - mLeft】,這個mRight和mLeft,是在setFrame方法被調用後賦值的(layout方法最終會調用setFrame)。
也就是說,getMeasuredWidth返回值的大小,取決於setMeasuredDimension,而getWidth,則取決於layout。
傳說中一個是 View 寬度,一個是 View 中的內容寬度,這個解答對嗎?
在常規的View中,比如TextView,ImageView這些,如果沒有明確指定寬度的話,那麼他們的getMeasuredWidth返回的寬度,確實就是實際內容的寬度。
但如果在xml佈局裡或自定義View中故意把寬度設置的很大,或者很小,比如設置寬度為9999999,這種情況就不算了。
所以我的回答是:如果這個View和它所在的ViewGroup(在ViewGroup中的onMeasure也可做手腳),都遵守規矩的話,那麼這句話就是對的。
4.butterknife 中的黑科技
很多時候大家在剖析butterknife源碼的時候,更多的是講解其中的apt等,在library中使用buttterknife的時候,會使用R2.id.xxx
class ExampleActivity extends Activity {
@BindView(R2.id.user) EditText username;
@BindView(R2.id.pass) EditText password;
...
}
而非R.id.xxx.
最後
需要Android面試資料題集的可以
關注+轉發,私聊我【面試】免費領取。閱讀更多 Android進階技術分享 的文章