編輯:Android資訊
對大多數Android的開發者來說,最經常的操作莫過於對界面進行布局,View中背景圖片的加載是最經常做的。但是我們很少關注這個過程,這篇文章主要解析view中背景圖片加載的流程。了解view中背景圖片的加載(資源的加載)可以讓我們對資源加載的過程進行一些優化,另外當需要進行整個應用的換膚時,也可以更得心應手。
View圖片的加載,我們最常見的就是通過在XML文件當中進行drawable的設置,然後讓Android系統幫我們完成,或者手動寫代碼加載成Bitmap,然後加載到View上。這篇文章主要分析Android在什麼時候以及怎麼幫我們完成背景圖片的加載的,那麼我們就從Activity.setContentView還是LayoutInflater.inflate(…)方法開始分析。
不管是從Activity.setContentView(…)還是LayoutInflater.inflate(…)方法進行View的初始化,最終都會到達LayoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)這個方法中。在這裡我們主要關注View的背景圖片加載,對於XML如何解析和加載就放過了。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; View result = root; try { // Look for the root node. 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!"); } 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, attrs, false); } else { // Temp is the root view that was found in the xml View temp; if (TAG_1995.equals(name)) { temp = new BlinkLayout(mContext, attrs); } else { temp = createViewFromTag(root, name, 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 rInflate(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) { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; } catch (IOException e) { InflateException ex = new InflateException( parser.getPositionDescription() + ": " + e.getMessage()); ex.initCause(e); throw ex; } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; } return result; } }
上面這麼長一串代碼,其實思路很清晰,就是針對XML文件進行解析,然後根據XML解析出的每一個節點進行View的初始化,緊接著將View的Layout參數設置到View上,然後將View添加到它的父控件上。
為了了解View是怎麼被加載出來的,我們只需要了解
temp = createViewFromTag(root, name, attrs);
跟進去看看。
/* * default visibility so the BridgeInflater can override it. */ View createViewFromTag(View parent, String name, AttributeSet attrs) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } if (DEBUG) System.out.println("******** Creating view: " + name); try { View view; if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs); else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs); else view = null; if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, mContext, attrs); } if (view == null) { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } if (DEBUG) System.out.println("Created view is: " + view); return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; } }
上面代碼的重點在於try…Catch裡的內容。try包起來的東西就是對View進行初始化,注意到上面代碼中有幾個Factory,這些Factory可以在View進行初始化,也就是說其實我們可以在這裡干預View的初始化。從上面代碼我們可以知道,如果我們自定義了一個Factory,那麼當前要初始化的View會優先被我們自定義的Factory初始化,而不通過系統默認的Factory初始化。那麼如果我們要自定義Factory,應該在哪裡定義呢?容易想到,Factory必須要趕在資源加載前自定義完成,所以我們應該在onCreate(…)的this.setContentView(…)之前設置LayoutInflater.Factory。
getLayoutInflater().setFactory(factory);
接下來我們看到上面函數裡面的
if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); }
這段函數就是對View進行初始化,有兩種情況,一種是系統自帶的View,它在
if (-1 == name.indexOf('.'))
這裡面進行初始化,因為如果是系統自帶的View,傳入的那麼一般不帶系統的前綴”android.view.”。另一個分支初始化的是我們自定義的View。我們跟進onCreateView看看。
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); } 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; try { if (constructor == 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); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); 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 = 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); } } } Object[] args = mConstructorArgs; args[1] = attrs; final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // always use ourselves when inflating ViewStub later final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(this); } return view; } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassCastException e) { // If loaded class is not a View subclass InflateException ie = new InflateException(attrs.getPositionDescription() + ": Class is not a View " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName())); ie.initCause(e); throw ie; } }
從onCreateView(…)中我們知道,其實createViewFromTag(…)中對View的初始化最終都是通過createView(…)這個函數進行初始化的,不同只在於系統控件需要通過onCreateView(…)加上前綴,以便類加載器(ClassLoader)正確地通過類所在的包初始化這個類。createView(…)這個函數的思路很清晰,不看catch裡面的內容,try裡面開頭的兩個分支就是用來將所要用的類構造函數提取出來,Android系統會對使用過的類構造函數進行緩存,因為像TextView這些常用的控件可能會被使用很多次。接下來,就是通過類構造函數對View進行初始化了。我們注意到傳入構造函數的mConstructorArgs是一個包含兩個元素的數組。
final Object[] mConstructorArgs = new Object[2];
那麼我們就很清楚了,它就是調用系統控件中對應兩個參數的構造函數。為了方便,我們就從最基礎的View進行分析。
public View(Context context, AttributeSet attrs) { this(context, attrs, 0); } public View(Context context, AttributeSet attrs, int defStyle) { this(context); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0); Drawable background = null; int leftPadding = -1; int topPadding = -1; int rightPadding = -1; int bottomPadding = -1; int startPadding = UNDEFINED_PADDING; int endPadding = UNDEFINED_PADDING; int padding = -1; int viewFlagValues = 0; int viewFlagMasks = 0; boolean setScrollContainer = false; int x = 0; int y = 0; float tx = 0; float ty = 0; float rotation = 0; float rotationX = 0; float rotationY = 0; float sx = 1f; float sy = 1f; boolean transformSet = false; int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY; int overScrollMode = mOverScrollMode; boolean initializeScrollbars = false; boolean leftPaddingDefined = false; boolean rightPaddingDefined = false; boolean startPaddingDefined = false; boolean endPaddingDefined = false; final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); switch (attr) { case com.android.internal.R.styleable.View_background: background = a.getDrawable(attr); break; case com.android.internal.R.styleable.View_padding: padding = a.getDimensionPixelSize(attr, -1); mUserPaddingLeftInitial = padding; mUserPaddingRightInitial = padding; leftPaddingDefined = true; rightPaddingDefined = true; break; //省略一大串無關的函數 }
由於我們只關注View中的背景圖是怎麼加載的,注意這個函數其實就是遍歷AttributeSet attrs這個東西,然後對View的各個屬性進行初始化。我們直接進入
background = a.getDrawable(attr);
這裡看看(TypedArray.getDrawable)。
public Drawable getDrawable(int index) { final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (false) { System.out.println("******************************************************************"); System.out.println("Got drawable resource: type=" + value.type + " str=" + value.string + " int=0x" + Integer.toHexString(value.data) + " cookie=" + value.assetCookie); System.out.println("******************************************************************"); } return mResources.loadDrawable(value, value.resourceId); } return null; }
我們發現它調用mResources.loadDrawable(…),進去看看。
/*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d("PreloadDrawable", name); } } boolean isColorDrawable = false; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; } final long key = isColorDrawable ? value.data : (((long) value.assetCookie) << 32) | value.data; Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); if (dr != null) { return dr; } Drawable.ConstantState cs = isColorDrawable ? sPreloadedColorDrawables.get(key) : (sPreloadedDensity == mConfiguration.densityDpi ? sPreloadedDrawables.get(key) : null); if (cs != null) { dr = cs.newDrawable(this); } else { if (isColorDrawable) { dr = new ColorDrawable(value.data); } if (dr == null) { if (value.string == null) { throw new NotFoundException( "Resource is not a Drawable (color or path): " + value); } String file = value.string.toString(); if (TRACE_FOR_MISS_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) + ": " + name + " at " + file); } } if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file); if (file.endsWith(".xml")) { try { XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXml(this, rp); rp.close(); } catch (Exception e) { NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } else { try { InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); dr = Drawable.createFromResourceStream(this, value, is, file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } } } if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); cs = dr.getConstantState(); if (cs != null) { if (mPreloading) { if (verifyPreloadConfig(value, "drawable")) { if (isColorDrawable) { sPreloadedColorDrawables.put(key, cs); } else { sPreloadedDrawables.put(key, cs); } } } else { synchronized (mTmpValue) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); if (isColorDrawable) { mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } else { mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } } } } } return dr; }
就是這個函數了,所有View的背景的加載都在這裡了。這個函數的邏輯就比較復雜了,大體說來就是根據背景的類型(純顏色、定義在XML文件中的,或者是一張靜態的背景),如果緩存裡面有,就直接用緩存裡的。
總結一下,經過上面的分析,我們知道了,Android就是在Activity.setContentView(…)中為我們進行資源文件的加載,精確到具體的函數的話,資源文件的加載就是在每一個被初始化的View的構造函數中進行加載的。
日期和時間是任何手機平台都有的功能,Android也如此。 DatePicker:用來實現日期(年月日) TimePicker:用來實現時間(時分秒) Calen
Android 庫(Library)在結構上與 Android 應用模塊相同。應用模塊所可以包含的東西,在庫中都允許存在,包括代碼文件、資源文件和manifest
常有這種需求,即ListView中數據較多(不涉及分頁),如果都展開,數據量較多,體驗不好,所以需要提供用戶查看更多、收縮數據的交互 截圖如下: 如圖所示,點擊
之前一直都是看別人寫的啟動模式,發現網上大多數的內容都是抄襲來抄襲去,直到最近看了開發藝術這本書,發現之前對啟動模式的理解過於簡單,很多東西都沒有考慮到,為了加深