您好,登錄后才能下訂單哦!
Android應用中如何使用LayoutInflater加載布局?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
需要從layout中加載View的時候,會調(diào)用這個方法
LayoutInflater.from(context).inflater(R.layout.main_activity,null);
1.如何創(chuàng)建LayoutInflater?
這有什么值得說的?如果你打開了LayoutInflater.Java你自然就明白了,LayoutInflater是一個抽象類,而抽象類是不能直接被實例化的,也就是說我們創(chuàng)建的對象肯定是LayoutInflater的某一個實現(xiàn)類。
我們進入LayoutInflater.from方法中可以看到
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; }
好吧,是獲取的系統(tǒng)服務!是從context中獲取,沒吃過豬肉還沒見過豬跑么,一說到context對象十有八九是說得ContextImpl對象,于是我們直接去到ContextImpl.java中,找到getSystemService方法
@Override public Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name); }
額。。。又要去SystemServiceRegistry.java文件中
/** * Gets a system service from a given context. */ public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null; }
由代碼可知,我們的Service是從SYSTEM_SERVICE_FETCHERS這個HashMap中獲得的,而稍微看一下代碼就會發(fā)現(xiàn),這個HashMap是在static模塊中賦值的,這里注冊了很多的系統(tǒng)服務,什么ActivityService,什么AlarmService等等都是在這個HashMap中。從LayoutInflater.from方法中可以知道,我們找到是Context.LAYOUT_INFLATER_SERVICE對應的Service
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, new CachedServiceFetcher<LayoutInflater>() { @Override public LayoutInflater createService(ContextImpl ctx) { return new PhoneLayoutInflater(ctx.getOuterContext()); }});
好啦,主角終于登場了——PhoneLayoutInflater,我們獲取的LayoutInflater就是這個類的對象。
那么,這一部分的成果就是我們找到了PhoneLayoutInflater,具體有什么作用,后面再說。
2.inflater方法分析
這個才是最重要的方法,因為就是這個方法把我們的layout轉換成了View對象。這個方法直接就在LayoutInflater抽象類中定義
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); }
傳入的參數(shù)一個是layout的id,一個是是否指定ParentView,而真正的實現(xiàn)我們還得往下看
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) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
我們先從context中獲取了Resources對象,然后通過res.getLayout(resource)方法獲取一個xml文件解析器XmlResourceParser(關于在Android中的xml文件解析器這里就不詳細講了,免得扯得太遠,不了解的同學可以在網(wǎng)上查找相關資料閱讀),而這其實是把我們定義layout的xml文件給加載進來了。
然后,繼續(xù)調(diào)用了另一個inflate方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final Context inflaterContext = mContext; //快看,View的構造函數(shù)中的attrs就是這個!?。? final AttributeSet attrs = Xml.asAttributeSet(parser); //這個數(shù)組很重要,從名字就可以看出來,這是構造函數(shù)要用到的參數(shù) mConstructorArgs[0] = inflaterContext; View result = root; try { // 找到根節(jié)點,找到第一個START_TAG就跳出while循環(huán), // 比如<TextView>是START_TAG,而</TextView>是END_TAG int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } //獲取根節(jié)點的名稱 final String name = parser.getName(); //判斷是否用了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"); } //解析 rInflate(parser, root, inflaterContext, attrs, false); } else { // 這里需要調(diào)用到PhoneLayoutInflater中的方法,獲取到根節(jié)點對應的View final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; //如果指定了parentView(root),則生成layoutParams, //并且在后面會將temp添加到root中 if (root != null) { params = root.generateLayoutParams(attrs); if (!attachToRoot) { temp.setLayoutParams(params); } } // 上面解析了根節(jié)點,這里解析根節(jié)點下面的子節(jié)點 rInflateChildren(parser, temp, attrs, true); if (root != null && attachToRoot) { root.addView(temp, params); } if (root == null || !attachToRoot) { result = temp; } } } catch (Exception e) { } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; } return result; } }
這個就稍微有點長了,我稍微去除了一些跟邏輯無關的代碼,并且添加了注釋,如果有耐心看的話應該是能看懂了。這里主要講兩個部分,首先是rInflateChildren這個方法,其實就是一層一層的把所有節(jié)點取出來,然后通過createViewFromTag方法將其轉換成View對象。所以重點是在如何轉換成View對象的。
3.createViewFromTag
我們一層層跟進代碼,最后會到這里
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ...... ...... try { ...... if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { //不含“.” 說明是系統(tǒng)自帶的控件 if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; ...... } }
為了方便理解,將無關的代碼去掉了,我們看到其實就是調(diào)用的createView方法來從xml節(jié)點轉換成View的。如果name中不包含'.' 就調(diào)用onCreateView方法,否則直接調(diào)用createView方法。
在上面的PhoneLayoutInflater中就復寫了onCreateView方法,而且不管是否重寫,該方法最后都會調(diào)用createView。唯一的區(qū)別應該是系統(tǒng)的View的完整類名由onCreateView來提供,而如果是自定義控件在布局文件中本來就是用的完整類名。
4. createView方法
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { //1.通過傳入的類名,獲取該類的構造器 Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; try { if (constructor == null) { clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { if (mFilter != null) { Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } //2.通過獲得的構造器,創(chuàng)建View實例 Object[] args = mConstructorArgs; args[1] = attrs; final View view = constructor.newInstance(args); if (view instanceof ViewStub) { final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } return view; } catch (NoSuchMethodException e) { ...... } }
這段代碼主要做了兩件事情
第一,根據(jù)ClassName將類加載到內(nèi)存,然后獲取指定的構造器constructor。構造器是通過傳入?yún)?shù)類型和數(shù)量來指定,這里傳入的是mConstructorSignature
static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};
即傳入?yún)?shù)是Context和AttributeSet,是不是猛然醒悟了!?。?strong>這就是為什么我們在自定義View的時候,必須要重寫View(Context context, AttributeSet attrs)則個構造方法,才能在layout中使用我們的View。
第二,使用獲得的構造器constructor來創(chuàng)建一個View實例。
5.回答問題
還記得上面我們提到的三個問題嗎?現(xiàn)在我們來一一解答:
1.LayoutInflater為什么可以加載layout文件?
因為LayoutInflater其實是通過xml解析器來加載xml文件,而layout文件的格式就是xml,所以可以讀取。
2.加載layout文件之后,又是怎么變成供我們使用的View的?
LayoutInflater加載到xml文件中內(nèi)容之后,通過反射將每一個標簽的名字取出來,并生成對應的類名,然后通過反射獲得該類的構造器函數(shù),參數(shù)為Context和AttributeSet。然后通過構造器創(chuàng)建View對象。
3.我們定義View的時候,如果需要在布局中使用,則必須實現(xiàn)帶AttributeSet參數(shù)的構造方法,這又是為什么呢?
因為LayoutInflater在解析xml文件的時候,會將xml中的內(nèi)容轉換成一個AttributeSet對象,該對象中包含了在xml文件設定的屬性值。需要在構造函數(shù)中將這些屬性值取出來,賦給該實例的屬性。
關于Android應用中如何使用LayoutInflater加載布局問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業(yè)資訊頻道了解更多相關知識。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。