您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何Android項目中創建一個View,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
從布局文件到LayoutParams
首先從Activity的setContentView(int)
方法開始,只要設置了R.layout的布局文件,那么界面上就會顯示出來對應的內容。所以以這個方法為初發點,然后往后跟蹤代碼。
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
通過以上代碼發現調用了Window類的setContentView方法,那么這個Window對象mWindow又是怎么初始化的?在Activity中搜索發現是在Activity的attach方法中初始化的,構造了一個PhoneWindow對象。
如下代碼所示:
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this); // 這里創建了Window對象 mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); // ... 中間部分代碼省略 mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); if (mParent != null) { mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config; }
在PhoneWindow的setContentView(int)
方法中,發現是調用了LayoutInflater的inflate(int, View)
方法,對這個布局文件進行轉換成View,并添加到后面View這個參數中。
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { // ... } else { mLayoutInflater.inflate(layoutResID, mContentParent); } // ... }
這里面順帶穿插說一下View的根節點是怎樣初始化出來的。
這里有一個關鍵的地方是這個installDecor()
方法,在這個方法中通過調用generateDecor()
方法創建了這個mDecor的對象,通過調用generateLayout(DecorView)
方法初始化出來了mContentParent對象。其中,mDecor是PhoneWindow.DecorView
的類實例,mContentParent是展示所有內容的,是通過com.android.internal.R.id.contentID
來找到這個View。
具體代碼如下所示:
protected ViewGroup generateLayout(DecorView decor) { // ... View in = mLayoutInflater.inflate(layoutResource, null); // layoutResource是根據對當前顯示View的Activity的theme屬性值來決定由系統加載對應的布局文件 decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } // ... return contentParent; }
那么在哪里面可以看到這個DecorView呢?看下面圖。
下面這張圖是在debug模式下連接手機調試App,使用Layout Inspector工具查看得到的圖:
PhoneWindow.DecorView
其中從1位置可以看出,整個View的根節點的View是PhoneWindow.DecorView
實例;從2位置和3位置的mId可以推斷出來,上面的mContentParent就是ContentFrameLayout類的實例;位置4中的藍色區域是mContentParent所表示的位置和大小。
以上圖是在AS 2.2.3版本上使用Android Monitor Tab頁中的Layout Inspector工具(參考位置5)生成。
緊接著跟蹤上面LayoutInflater中的inflate()
方法中調用,發現最后調用到了inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
方法中,在這個方法中構造了XmlResourceParser對象,而這個parser對象構造代碼如下所示:
XmlResourceParser loadXmlResourceParser(int id, String type) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { mTmpValue = value = new TypedValue(); } getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { return loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type); } throw new NotFoundException( "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + Integer.toHexString(value.type) + " is not valid"); } } XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException { if (id != 0) { // ... // These may be compiled... synchronized (mCachedXmlBlockIds) { // First see if this block is in our cache. final int num = mCachedXmlBlockIds.length; for (int i=0; i<num; i++) { if (mCachedXmlBlockIds[i] == id) { //System.out.println("**** REUSING XML BLOCK! id=" // + id + ", index=" + i); return mCachedXmlBlocks[i].newParser(); } } // Not in the cache, create a new block and put it at // the next slot in the cache. XmlBlock block = mAssets.openXmlBlockAsset( assetCookie, file); if (block != null) { int pos = mLastCachedXmlBlockIndex+1; if (pos >= num) pos = 0; mLastCachedXmlBlockIndex = pos; XmlBlock oldBlock = mCachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } mCachedXmlBlockIds[pos] = id; mCachedXmlBlocks[pos] = block; //System.out.println("**** CACHING NEW XML BLOCK! id=" // + id + ", index=" + pos); return block.newParser(); } } // ... } // ... }
其中getValue()
方法調用到本地frameworks/base/core/jni/android_util_AssetManager.cpp文件中的static jint android_content_AssetManager_loadResourceValue
函數,而在這個函數中java層的TypeValue的類對象value對象屬性通過JNI形式被賦值。
在構建這個parser時,經歷了很復雜的過程,從TypeValue中的assetCookie屬性,到XmlBlock中的xmlBlock屬性,再到XmlBlock.Parser中的mParseState屬性,都是用來保存JNI層的數據,因為最終操作這些資源數據是在native層,所以必不可少通過JNI這種方式,在java層和native層周旋。這里沒有深入到native層去分析資源是如何被加載解析的,后面有機會再說。
最后跟到了這個public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
方法中,代碼如下所示:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; View result = root; // ... // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); // 這里將布局文件中的名稱反射成具體的View類對象 ViewGroup.LayoutParams params = null; if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); // 這里將尺寸轉換成了LayoutParams if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } // Inflate all children under temp against its context. rInflateChildren(parser, temp, attrs, true); // 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); // 將布局文件中根的View添加到mContentParent中 } // ... return result; }
接著看View的generateLayoutParams(AttributeSet)
方法,因為這里返回了params。查看代碼最后發現LayoutParams的width和height屬性賦值的代碼如下所示:
public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { width = a.getLayoutDimension(widthAttr, "layout_width"); height = a.getLayoutDimension(heightAttr, "layout_height"); }
通過查看TypedArray類中的getLayoutDimension()
方法發現,獲取的值是通過index在mData這個成員數組中獲取的。這個mData的值是在創建TypedArray對象時被賦的值,具體參見TypedArray的obtain方法。這個數組是在Resources的obtainStyledAttributes()
方法中通過調用AssetManager.applyStyle()
方法被初始化值的。applyStyle()
方法是一個native方法,對應frameworks/base/core/jni/android_util_AssetManager.cpp文件中的android_content_AssetManager_applyStyle
函數。在這個函數中發現,傳入的Resrouces類中的mTheme成員以及XmlBlock.Parse
類的mParseState成員都是一個C++對象的指針,在java類中以整型或長整型值保存。
至此,布局文件中的尺寸值已經被轉換成了具體的int類型值。
從布局文件到View
從上面的public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
方法中看到View是通過這行代碼final View temp = createViewFromTag(root, name, inflaterContext, attrs);
創建出來的,而這個name就是XML文件在解析時遇到的標簽名稱,比如TextView。此時的attrs也就是上面分析的XmlBlock.Parser
的對象。最后發現View是在createViewFromTag()
方法中創建的,代碼如下所示:
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(); } // ... 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); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; // ... }
這里要注意一下,mConstructorArgs的第一個值是一個Context,而這個Context有可能已經不是當前Activity的Context。
看到這里,下面這樣的代碼片段是不是很熟悉?
if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); }
上面Factory這個接口對象在LayoutInflater類中就有三個屬性,分別對應:factory、factory2、mPrivateFactory。很明顯,弄清了這三個對象,也就知道了View的初始化流程。下面代碼是對這三個屬性的值的輸出:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LayoutInflater inflater = getLayoutInflater(); LayoutInflater inflater1 = LayoutInflater.from(this); Field f = null; try { f = LayoutInflater.class.getDeclaredField("mPrivateFactory"); f.setAccessible(true); } catch (NoSuchFieldException e) { e.printStackTrace(); } Log.d("may", "the same object: " + (inflater == inflater1)); Log.d("may", "inflater factory: " + inflater.getFactory() + ", factory2: " + inflater.getFactory2()); Log.d("may", "inflater1 factory: " + inflater1.getFactory() + ", factory2: " + inflater1.getFactory2()); if (f != null) { try { Log.d("may", "inflater mPrivateFactory: " + f.get(inflater)); Log.d("may", "inflater1 mPrivateFactory: " + f.get(inflater1)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
輸出的LOG如下所示:
// 當前Activiy繼承的是android.support.v7.app.AppCompatActivity the same object: true inflater factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0} inflater1 factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0} inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70 inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70 // 當前Activity繼承的是android.app.Activity the same object: true inflater factory: null, factory2: null inflater1 factory: null, factory2: null inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28 inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28
首先看到mPrivateFactory是當前的Activity實例,因為Activity也實現的Factory2接口。首先看LayoutInflater的創建過程,如下圖所示:
LayoutInflater初始化流程
而生成的PhoneLayoutInflater對象是緩存在ContextImpl類的屬性SYSTEM_SERVICE_MAP中,所以通過Context.LAYOUT_INFLATER_SERVIC去取,始終是同一個對象,當然僅限于當前Context中。
mPrivateFactory屬性的賦值是在Activity的attach()方法中,通過調用mWindow.getLayoutInflater().setPrivateFactory(this);
,因此調用Factory2的onCreateView()
方法時,實際是調用Activity中的onCreateView()
方法。而Activity中的onCreateView()
實際返回的是null,所以最后創建View的是if判斷中的onCreateView(parent, name, attrs)
方法,最后View是在LayoutInflater類中的public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException
方法中創建:
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; // ... // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); // prefix傳的值是android.view. // ... constructor = clazz.getConstructor(mConstructorSignature); // Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class}; constructor.setAccessible(true); sConstructorMap.put(name, constructor); // ... Object[] args = mConstructorArgs; args[1] = attrs; // 這個值是XmlBlock.Parser對象 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; }
關于如何Android項目中創建一個View就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。