一卫枝、inflate的基本使用
inflate方法非臣灞基礎(chǔ)且常用,但是好像很多人都用錯了校赤,比如說自定義view的時候多了一層父布局等吆玖。剛好再處理inflate的優(yōu)化,所以總結(jié)一下我理解的inflate()方法马篮,(如有內(nèi)容錯誤沾乘,還麻煩指出,大家一起進(jìn)步~)
好像除了activity的onCreate()方法內(nèi)可以調(diào)用setContentView()之外浑测,加載一個布局都需要使用Inflate()方法翅阵。
LayoutInflater.from(context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View.inflate(Context context, @LayoutRes int resource, ViewGroup root)
Activity.setContentView(@LayoutRes int layoutResID)
其實這三個方法底層邏輯都是LayoutInflater#inflate方法。
二、inflate 詳細(xì)解析
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
...
}
2.1參數(shù)解釋
resource:加載的layoutId
root 和attachToRoot 結(jié)合起來理解:當(dāng)root 可以為null掷匠,表示直接加載layout读慎,不做任何處理,attachToRoot沒有任何意義槐雾。如果不為null 且 attachToRoot為true,那么就會把解析的layout添加到root里面幅狮。如果attachToRoot 為false募强,那么只是限制了這個根節(jié)點的部分屬性(換句話說xml中根節(jié)點的屬性不一定全部都會生效,具體要看root支持哪些)
接下來一行一行看代碼:
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) + ")");
}
//這個方案android還不支持崇摄,具體可以看我之前的一篇分析擎值。所以這個view一定是null
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
//拿到parser 進(jìn)入關(guān)鍵函數(shù)
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
這里面有兩個知識點
三、涉及知識點
1逐抑、tryInflatePrecompiled(resource, res, root, attachToRoot)
這個方案android還不支持鸠儿,具體可以看我之前的一篇分析。所以這個view一定是null
2厕氨、XmlResourceParser
XmlResourceParser是一個xml解析工具进每,通過res.getLayout(resource)獲取,通過調(diào)用next()方法遍歷XmlResourceParser命斧,可以獲取xml中所有內(nèi)容田晚。parser內(nèi)部有個類似于指針的東西,執(zhí)行一次next()方法后国葬,指針就會指向下一個節(jié)點贤徒,通過demo驗證,他是一個一個標(biāo)簽深度遍歷的汇四。
具體可以看這篇文章http://www.reibang.com/p/d3c801584f8f
[圖片上傳失敗...(image-5f8f2c-1663308292830)]
接下來看最重要的方法inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
//3:拿到attrs
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
//直接進(jìn)入根節(jié)點接奈,因為一份xml可能存在一些其他標(biāo)簽,執(zhí)行這個之后parser指針指向根節(jié)點
advanceToRootNode(parser);
//拿到根節(jié)點的標(biāo)簽名
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//根節(jié)點是merge
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");
}
//4:解析根節(jié)點為merge的layout
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//5:實例化根節(jié)點view
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
//6:獲取根節(jié)點的attr
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//設(shè)置根節(jié)點的params
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
//7:解析根節(jié)點內(nèi)部的子view
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) {
//將根節(jié)點添加到root上通孽,使用的布局參數(shù)是layout中定義的序宦。
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;
}
}
這個方法很重要,只要稍微看漏一點就理解錯了利虫。
3挨厚、final AttributeSet attrs = Xml.asAttributeSet(parser)
public static AttributeSet asAttributeSet(XmlPullParser parser) {
return (parser instanceof AttributeSet)
? (AttributeSet) parser
: new XmlPullAttributes(parser);
}
這個方法其實返回的就是他自己。但是AttributeSet相對與XmlResourceParser來說糠惫,少了next()方法疫剃,通過這個attr,只能獲取xml中某個節(jié)點硼讽。這邊需要了解的一點是attrs 和parse是同一個對象巢价,當(dāng)parse執(zhí)行next()方法時,通過attr解析的節(jié)點就不是同一個了。
4壤躲、merge標(biāo)簽
解析根節(jié)點有兩種情況城菊,一種是以<merge>開頭的,一種是其他類型的碉克。
1凌唬、<merge>開頭的 root 不能為 null 且attachToRoot要為true
merge簡單來理解就是跳過根節(jié)點標(biāo)簽,將子view全部添加到root里漏麦。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//獲取parser的深度
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//while循環(huán)遍歷xml:當(dāng)下一個節(jié)點不是</> 或者 當(dāng)前指針指向的節(jié)點在內(nèi)部
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
//只過濾節(jié)點
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
//4.1 遍歷節(jié)點
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//初始化name所對應(yīng)的節(jié)點view
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
//根據(jù)viewGroup解析該節(jié)點上的attr生成params設(shè)置給view
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//解析子view
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
4.1遍歷節(jié)點
從merge這邊過來客税,parser.next()之后就已經(jīng)指向第二個節(jié)點了。節(jié)點開始的標(biāo)簽支持三個特殊標(biāo)簽"requestFocus"撕贞、"tag"更耻、"include"和其他標(biāo)簽。其中"include"場景使用較多捏膨。
其他標(biāo)簽只得就是view了 就是這三步驟
- 初始化name所對應(yīng)的節(jié)點view
- 使用viewGroup解析該節(jié)點上的attr生成params設(shè)置給view秧均,generateLayoutParams會放到后面重點說明
- 使用遞歸方式解析子view
5、其他標(biāo)簽
其他標(biāo)簽就是view的了号涯,直接看createViewFromTag目胡。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//如果標(biāo)簽名為view,name真實的值為class所對應(yīng)的值链快。
//ps:好像很少看到這樣的寫法
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
//ignoreThemeAttr = false 解析xml中的theme標(biāo)簽
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 {
//5.1:對外暴露的鉤子
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//5.2:原生view讶隐,比如ImageView
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
//5.3:自定義及第三方view
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;
}
}
5.1 tryCreateView(parent, name, context, attrs);
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!
//一個不停閃爍的view,比如時鐘上的:閃爍,可以用這個久又。
//但是只要被添加到窗口巫延,就會開始閃爍,無法控制他的開始和暫停等地消,如果需要更多功能的炉峰,可以模仿他寫一個自定義的。
return new BlinkLayout(context, attrs);
}
View view;
if (mFactory2 != null) {
//mFactory2和mFactory是可以由有外部傳入的脉执,這個也是對外暴露的方法疼阔。
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//沒有設(shè)置
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
return view;
}
factory有暴露接口設(shè)置進(jìn)來,但是factory只能被設(shè)置一次半夷,使用AppCompatActivity都有設(shè)置婆廊,詳情可參考:
https://blog.51cto.com/u_15064646/2575022
factory處理這些方法create方法有幾個好處:
- 一個是不需要走到系統(tǒng)的方法再通過反射去創(chuàng)建view,如果找到相關(guān)的view巫橄,直接new淘邻。
- 可以創(chuàng)建view的時候統(tǒng)一處理一下,比如xml定義了一個<TextView>,使用AppCompatActivity都會給轉(zhuǎn)成<AppCompatTextView>湘换,也可以改改背景等宾舅,網(wǎng)易云之前的換膚方案用的就是這個统阿。
- 可以打印onCreateView的時間。
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
ps:我使用的appcompat 1.5.0版本筹我,已經(jīng)是setFactory2了扶平。
5.2 createView(context, name, null, attrs)
如果view為null,就會走原生的方式解析view蔬蕊。view只有前面的factory沒有匹配上時為null结澄。
原生處理方式分兩種:-1 == name.indexOf('.'),表示name標(biāo)簽沒有.,也就是那些不用寫包名的控件岸夯。其實這個在后面會自動加上前綴:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
//先從緩存里找是否有已經(jīng)有name對應(yīng)的view
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//反射找到對應(yīng)的view
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs) + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
走原生的方式就是通過反射去實例化name對應(yīng)的view概而,從mConstructorSignature可以看出來,調(diào)用的構(gòu)造方法是兩個參數(shù)的囱修。到這里,view就創(chuàng)建完成了王悍。
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
6破镰、root.generateLayoutParams(attrs)
這個方法見過很多次了。這里需要理解的有兩個點
attr所對應(yīng)的內(nèi)容不是固定的压储,他隨著parse指針的變化鲜漩,獲取到的attr也是變化的。按照上面的流程集惋,可以確定attr和當(dāng)前name隨對應(yīng)的節(jié)點是一一對應(yīng)的孕似。
-
attr中的屬性并不是所有的都會生效,取決于root的generateLayoutParams方法刮刑,root支持解析哪些屬性喉祭,那么就只有那些屬性會生效。
比如FrameLayout只會解析寬高layout_gravity雷绢、layout_width泛烙、layout_height,LinearLayout還會解析layout_weight翘紊,其他的viewGroup解析的就更多了蔽氨。
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout); gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY); a.recycle(); }
public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout); weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0); gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1); a.recycle(); }
7、rInflateChildren(parser, temp, attrs, true);
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
嵌套調(diào)用解析子view帆疟。