从一个控件不响应点击事件引出View Inflater以及findViewById的过程
一个bug引出的思考
在使用NavigationView时,通常是这样在Xml文件中设置Headlayout对应的XML文件
app:headerLayout="@layout/nav_header"
然后在Activity或者Fragment中
要通过找到NavigationView的控件后再通过NavigationView的控件才能找到headerLayout里面的控件并设计点击事件
如果直接使用findViewById然后去设置点击事件会出现该控件不响应点击事件的情况发生
原因
1.findViewById的原理
首先看看findViewById的原理
#AppCompatActivity
@Override
public <T extends View> T findViewById(@IdRes int id) {
return getDelegate().findViewById(id);
}
getDelegate()返回了AppCompatDelegateImpl
#AppCompatDelegateImpl
@Nullable
@Override
public <T extends View> T findViewById(@IdRes int id) {
ensureSubDecor();
return (T) mWindow.findViewById(id);
}
#Window
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
DecorView是一个Activity的根View,在DecorView类中没有findViewById方法,那么肯定在它的父类里面,Decor继承自FrameLayout,FrameLayout继承自ViewGroup,ViewGrouo继承自View,最终在View类中找到了findViewById方法
#View
public static final int NO_ID = -1;
@Nullable
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
#View
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
return null;
}
如果是一个View被调用findViewTraversal就会停止递归调用,如果查询的是自己就返回this
在ViewGroup中重写了findViewTraversal方法
#ViewGroup
@Override
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return (T) v;
}
}
}
return null;
}
可以看到首先拿到了ViewGroup的所有子View,然后遍历子View再去调用子View的findViewById方法
也就是说Android使用过dfs深度优先搜索的方式从DecorView开始通过Id搜索到对应的View
2.Android LayoutInflater的原理
在Activity中直接调用findViewById是通过从以DecorView为根节点的View树dfs深度优先搜索查询来找到对应的View的,那如果我们所要找的View不在这颗View树上那不就找不到了吗?从而不就会导致点击事件失效?我们来看看Android是怎么生成这颗View树的
从MainActivity的setContentView看起
#MainActivity
setContentView(R.layout.activity_main);
//最终会走到到AppCompatDelegateImpl的setContentView方法
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
ensureSubDecor()里面创建了DecorView并且将DecorView与Window进行了绑定
重点在
LayoutInflater.from(mContext).inflate(resId, contentParent);
在这里把我们的resId传了进去
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
可以看到这里通过context.getSystemService获取系统服务拿到了LayoutInflater
我们接下来看它的inflate方法
inflate有多种重载,最终都会调用到一个Inflate方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
通过
XmlResourceParser parser = res.getLayout(resource);
我们可以知道Android是通过PULL方式解析布局XML文件的
下面看到最终的inflate方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
可以看到通过createViewFromTag创建了View
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
createViewFromTag由调用了tryCreateView方法并且通过反射最终完成了View的创建
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
//这个mFactory2可以帮助我们Hook创建View的过程
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
return view;
}
创建完根View后,又会递归不断创建最终完成View树的建立
rInflateChildren(parser, temp, attrs, true);
从上面的分析可以得出View树的建立是通过LayoutInflater 通过PULL解析XML布局文件然后递归建立的,那如果XML布局文件的节点中没有这个View,那这个View岂不是就不会被插入在View树中?从而不就无法通过以DecorView为根节点的这颗View树找到这个节点
3.NavigationView相关源码
来看看NavigationView和headerLayout相关联的源码
在NavigationView的构造方法里
if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
}
public View inflateHeaderView(@LayoutRes int res) {
return presenter.inflateHeaderView(res);
}
#NavigationMenuPresenter
public View inflateHeaderView(@LayoutRes int res) {
View view = layoutInflater.inflate(res, headerLayout, false);
addHeaderView(view);
return view;
}
public void addHeaderView(@NonNull View view) {
headerLayout.addView(view);
// The padding on top should be cleared.
menuView.setPadding(0, 0, 0, menuView.getPaddingBottom());
}
headerLayout =
(LinearLayout)
layoutInflater.inflate(R.layout.design_navigation_item_header, menuView, false);
总结
给NavigationView添加headerLayout是通过在NavigationView的属性里设置headerLayout的xml文件,而在Activity里使用findViewById是去dfs深度优先搜索以DecorView为根节点的View树,View是通过LayoutInflater创建的,LayoutInflater通过递归解析XML文件中的节点最终得到View树,而headerLayout不属于该XML文件的节点因此不存在在以DecorView为根节点的View树上,因此查找不到。如果要找headerLayout里面的控件,要先通过找到NavigationView后再通过NavigationView找到headerLayout,在调用headerLayout的findViewById方法
标题:从一个控件不响应点击事件引出View Inflater以及findViewById的过程
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2022/01/14/1648378541971.html