1. View唯一標(biāo)識(shí)
id組成:ActivityName_LayoutFileName_idName
對(duì)應(yīng)源碼:ResourceHelper#getGlobalIdName
public static String getGlobalIdName(@NonNull View view) {
int id = view.getId();
...
try {
Context context = view.getContext();
// 獲取activityName
String activityName = context.getClass().getSimpleName();
// 獲取布局文件名
String layoutFileName = getLayoutFileName(view);
String idName;
...
// 獲取id資源名
idName = getResourceEntryName(context, id);
...
return String.format("%s_%s_%s", activityName, layoutFileName, idName);
...
}
}
2. 定位交互控件:TouchTarget方案
TouchTarget如何賦值?
一次簡單的單點(diǎn)觸控交互流程是這樣的:
ACTION_DOWN(手指落下)
ACTION_MOVE(移動(dòng))
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
...
ACTION_UP(離開)
當(dāng)用戶觸發(fā)ACTION_DOWN事件時(shí),會(huì)執(zhí)行如下邏輯,尋找消費(fèi)當(dāng)前事件的TouchTarget。
if (actionMasked == MotionEvent.ACTION_DOWN){
//如果是down事件,遍歷child,找到TouchTarget
..
..
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndexchildrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
..
..
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// child 消費(fèi)了觸摸事件
..
..
// 根據(jù)消費(fèi)了觸摸事件的View創(chuàng)建TouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
..
..
break;
}
}
假設(shè)ACTION_DOWN事件分發(fā)路徑如下:
ViewGroup1
ViewGroup2
ViewGroup3
View

路徑中的每個(gè)ViewGroup都維護(hù)一個(gè)mFirstTouchTarget。
// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;
ACTION_DOWN事件分發(fā)過程中,路徑中各ViewGroup成員變量mFirstTouchTarget賦值流程如下:

何時(shí)讀取目標(biāo)控件?
【得到Android】通過在Activity的window上調(diào)用window.setCallback() 接管窗口的事件派發(fā),并在dispatchTouchEvent處理函數(shù)中添加讀取目標(biāo)控件的處理邏輯。如果接收到up事件,執(zhí)行處理邏輯,通過ViewGroup TouchTarget鏈表,找到本次交互行為的目標(biāo)控件。
讀取目標(biāo)控件的處理邏輯核心代碼如下:
private View findActionTargets() {
ViewGroup decorView = (ViewGroup) getWindow().getDecorView();
int content_id = android.R.id.content;
ViewGroup content = (ViewGroup) decorView.findViewById(content_id);
if (content == null) {
content = decorView; //對(duì)于非Activity DecorView 的情況處理
}
View touchTarget;
ViewGroup vg = content;
while (true) {
//獲取指定vg的mFirstTouchTarget.child
touchTarget = ViewHelper.findTouchTarget(vg);
//無法找到touchTarget 相關(guān)信息
if (touchTarget == null) return null;
//已經(jīng)找到touchTarget
if (touchTarget == vg) break;
boolean isVG = touchTarget instanceof ViewGroup;
//已經(jīng)找到touchTarget
if (!isVG) break;
//未找到touchTarget
vg = (ViewGroup) touchTarget;
}
return touchTarget;
}
其中,ViewHelper.findTouchTarget(ViewGroup)通過反射獲取指定ViewGroup的mFirstTouchTarget.child,源碼如下:
public static View findTouchTarget(@NonNull ViewGroup ancestor) {
Preconditions.checkNotNull(ancestor);
try {
Field firstTouchTargetField = CoreUtils.getDeclaredField(ancestor, "mFirstTouchTarget");
if (firstTouchTargetField == null) {
logReflectException("mFirstTouchTarget");
return ancestor;
}
firstTouchTargetField.setAccessible(true);
Object firstTouchTarget = firstTouchTargetField.get(ancestor);
if (firstTouchTarget == null) return ancestor;
Field firstTouchViewField = firstTouchTarget.getClass().getDeclaredField("child");
if (firstTouchViewField == null) {
logReflectException("child");
return ancestor;
}
firstTouchViewField.setAccessible(true);
View firstTouchView = (View) firstTouchViewField.get(firstTouchTarget);
if (firstTouchView == null) return ancestor;
return firstTouchView;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
再來看一下ViewGroup內(nèi)mFirstTouchTarget賦值流程圖,結(jié)合上面的算法與賦值流程圖,可以反過來獲取目標(biāo)控件。

下述偽代碼解釋了目標(biāo)控件獲取過程:
ViewGroup2 = ViewHelper.findTouchTarget(ViewGroup1);
ViewGroup3 = ViewHelper.findTouchTarget(ViewGroup2);
View = ViewHelper.findTouchTarget(ViewGroup3);
View即為目標(biāo)控件。
3. 布局文件名的獲取
對(duì)應(yīng)源碼:ResourceHelper#getLayoutFileName
private static String getLayoutFileName(@NonNull View view) {
String idNameSpace = (String) view.getTag(R.id.id_namespace_tag);
if (!TextUtils.isEmpty(idNameSpace)) return idNameSpace;
View tmp = view;
while (tmp.getParent() != null && (tmp.getParent() instanceof View)) {
View parent = (View) tmp.getParent();
String space = (String) parent.getTag(R.id.id_namespace_tag);
if (!TextUtils.isEmpty(space)) return space;
tmp = parent;
}
return "";
}
算法思路:從view出發(fā),向上回溯,讀取ancestor的tag,讀到則跳出循環(huán)。其中tag即為布局文件名。
Tag是如何塞進(jìn)去的呢?
復(fù)習(xí):
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
當(dāng)root不為null,attachToRoot為true時(shí),表示將resource指定的布局添加到root中,添加的過程中resource所指定的的布局的根節(jié)點(diǎn)的各個(gè)屬性都是有效的;
如果root不為null,而attachToRoot為false的話,表示不將第一個(gè)參數(shù)所指定的View添加到root中;
當(dāng)root為null時(shí),不論attachToRoot為true還是為false,顯示效果都是一樣的。當(dāng)root為null表示我不需要將第一個(gè)參數(shù)所指定的布局添加到任何容器中,同時(shí)也表示沒有任何容器來來協(xié)助第一個(gè)參數(shù)所指定布局的根節(jié)點(diǎn)生成布局參數(shù)。
方案:包裝系統(tǒng)的mInflater,在調(diào)用inflate時(shí),設(shè)置tag。
對(duì)應(yīng)源碼:LayoutInflaterWrapper#inflate
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
View view = inflate(parser, root, attachToRoot);
attachToRoot = (attachToRoot && root != null);
if (!attachToRoot) {
view.setTag(R.id.id_namespace_tag, ResourceHelper.getResourceEntryName(getContext(), resource));
return view;
}
int childCount = root.getChildCount();
View tagedView = root.getChildAt(childCount - 1);
tagedView.setTag(R.id.id_namespace_tag, ResourceHelper.getResourceEntryName(getContext(), resource));
return view;
} finally {
parser.close();
}
}
算法思路:
調(diào)用系統(tǒng)inflate,返回一個(gè)view;
attachToRoot為false,resource指定的布局對(duì)應(yīng)的view即為返回的view,將resource指定的布局文件名稱設(shè)置為該view的tag;
attachToRoot為true,resource指定的布局對(duì)應(yīng)的view為返回的view的最后一個(gè)child;將resource指定的布局文件名稱設(shè)置為該child的tag。