編輯:關於Android編程
現在越來越多的APP都加入了主題切換功能或者是日間模式和夜間模式功能切換等,這些功能不僅增加了用戶體驗也增強了用戶好感,眾所周知QQ和網易新聞的APP做的用戶體驗都非常好,它們也都有日間模式和夜間模式的主題切換功能。體驗過它們的主題切換後你會發現大部分效果是更換相關背景圖片、背景顏色、字體顏色等來完成的,網上這篇文章對主題切換講解的比較不錯,今天我們從源碼的角度來學習一下主題切換功能,如果你對這塊非常熟悉了,請跳過本文(*^__^*) …
在開始講解主題切換之前我們先看一下LayoutInflater吧,大家都應該對LayoutInflater的使用非常熟悉了(如果你對它的使用還不是很清楚請自行查閱)。LayoutInflater的使用場合非常多,常見的比如在Adapter的getView()方法中,在Fragment中的onCreateView()中使用等等,總之如果我們想要把對應的layout.xml文件渲染成對應的View層級視圖,離開LayoutInflater是不行的,那麼我們如何獲取LayoutInflater實例並用其來渲染成對應的View實例對象呢?一般有以下幾種方式:
調用Context.getSystemService()方法LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(R.layout.view_layout, null);直接使用LayoutInflater.from()方法
LayoutInflater inflater = LayoutInflater.from(context); View rootView = inflater.inflate(R.layout.view_layout, null);在Activity下直接調用getLayoutInflater()方法
LayoutInflater inflater = getLayoutInflater(); View rootView = inflater.inflate(R.layout.view_layout, null);使用View的靜態方法View.inflate()
rootView = View.inflate(context, R.layout.view_layout, null);以上4種方式都可以渲染出一個View實例出來但也都是借助LayoutInflater的inflate()方法來完成的,我們先看一下方式2中LayoutInflater.from()是怎麼做的,代碼如下:
/** * Obtains the LayoutInflater from the given context. */ 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; }LayoutInflater.from()方法只不過是對方式1的一層包裝,最終還是通過調用Context的getSystemService()方法獲取到LayoutInflater實例對象,然後通過返回的LayoutInflater實例對象調用其inflate()方法來完成對xml布局文件的渲染並生成相應的View對象。通過和方式1對比你會發現,這兩種方式中的Context如果是相同的那麼獲取的LayoutInflater對象應該是同一個。然後我們在看一下方式3中的實現部分,方式3是在Activity中直接調用Activity的getLayoutInflater()方法,源碼如下:
/** * Convenience for calling * {@link android.view.Window#getLayoutInflater}. */ public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); }通過源碼發現Activity的getLayoutInflater()方法輾轉調用到了getWindow()的getLayoutInflater()方法,getWindow()方法返回一個Window類型的對象,其中Window為抽象類在Android中該類的實現類是PhoneWindow,也就是說getWindow().getLayoutInflater()方法最終調用的是PhoneWindow的getLayoutInflater()方法,我們看一下PhoneWindow類中該方法的實現過程,代碼如下:
/** * Return a LayoutInflater instance that can be used to inflate XML view layout * resources for use in this Window. * * @return LayoutInflater The shared LayoutInflater. */ @Override public LayoutInflater getLayoutInflater() { return mLayoutInflater; }在PhoneWindow類中直接返回了mLayoutInflater對象,那麼mLayoutInflater是在何時何地完成初始化的呢?我們繼續查看mLayoutInflater的初始化在哪完成的,通過查看代碼發現是在PhoneWindow的構造方法中完成初始化的,代碼如下:
public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); }我們暫且不關心PhoneWindow是何時何地完成初始化的,我們只關心mLayoutInflater的初始化也是直接調用LayoutInflater.from()方法來完成的,這種方式和方式2是一樣的,都是借助傳遞進來的context調用其getSystemService()方法獲取到LayoutInflater實例,也就是說只要PhoneWindow中傳遞進來的context和方式1、方式2是相同的,那麼可以確定獲取到的mLayoutInflater的實例就是同一個。接著我們看方式4的通過調用View的靜態方法inflate()的內部流程是怎樣的,代碼如下:
/** * Inflate a view from an XML resource. This convenience method wraps the {@link * LayoutInflater} class, which provides a full range of options for view inflation. * * @param context The Context object for your activity or application. * @param resource The resource ID to inflate * @param root A view group that will be the parent. Used to properly inflate the * layout_* parameters. * @see LayoutInflater */ public static View inflate(Context context, int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); }在View的inflate()靜態方法中先是根據傳遞進來的context通過LayoutInflater.from()方法來獲取一個LayoutInflater實例對象,然後調用LayoutInflater的inflate()方法來完成把layout.xml布局文件渲染成對應的View層級視圖然後返回。 通過對以上代碼的分析我們可以得以下出結論:前邊說的無論以哪種方式來渲染View視圖都會先獲取到LayoutInflater的實例,然後通過調用該實例的inflate()方法把xml布局文件渲染出相應的View層級視圖,而獲取LayoutInflater實例是需要Context的,那也就是說如果傳入的Context對象是同一個那麼獲取的LayoutInflater實例也是相同的。這也是我用不小的篇幅從源碼的角度說明這一點的原因所在。 現在我們已經清楚了渲染View是由LayoutInflater來完成的,那麼在Activity的onCreate()方法中通過調用setContentView()為當前Activity設置顯示內容是不是也是通過LayoutInflater的inflater()方法完成的呢?我們接著看代碼,看看Activity的setContentView()裡是如何操作的,代碼如下:
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initActionBar(); }setContentView()方法中只是做了一個中轉,接著是調用Window實例的setContentView()方法,剛剛也說過Window為抽象類,它的實現類為PhoneWindow,那也就是最終調用的是PhoneWindow的setContentView()方法,我們看一下PhoneWindow的setContentView()方法,源碼如下:
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } // 這裡同樣是調用了LayoutInflater的inflate()方法 mLayoutInflater.inflate(layoutResID, mContentParent); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }從源碼可以看到在PhoneWindow的setContentView()方法中也同樣使用的是LayoutInflater的inflate()方法。到這裡我們就可以總結出結論:無論是我們自己渲染View還是說為Activity設置顯示內容都是借助LayoutInflater來完成的,而獲取LayoutInflater最終都是通過Context.getSystemService()來得到的,如果Context相同,那麼獲取的LayoutInflater的實例是相同的。 好了,用了不少篇幅講解了有關LayoutInflater的知識都是給主題切換功能做鋪墊的,那怎麼利用LaoutInflater來完成主題切換功能呢?別著急,我們再看一下LayoutInflater的源碼,打開LayoutInflater的源碼你會發現,其內部定義了Factory,Factory2等接口,這兩個接口是干嘛的了?其實他們倆功能是一樣的,Factory2是對Factory的完善,先看Factory的定義說明,代碼如下:
public interface Factory { /** * Hook you can supply that is called when inflating from a LayoutInflater. * You can use this to customize the tag names available in your XML * layout files. * *
* Note that it is good practice to prefix these custom names with your * package (i.e., com.coolcompany.apps) to avoid conflicts with system * names. * * @param name Tag name to be inflated. * @param context The context the view is being created in. * @param attrs Inflation attributes as specified in XML file. * * @return View Newly created view. Return null for the default * behavior. */ public View onCreateView(String name, Context context, AttributeSet attrs); }
接口Factory中定義了onCreateView()方法,該方法返回一個View實例。我們看看該方法的說明,大致意思是說:當我們使用LayoutInflater來渲染View的時候此方法可以支持做Hook操作,我們可以在xml布局文件中使用自定義標簽,需要注意的是不要使用系統名字。那麼這裡究竟該如何使用了?我們先梳理一下使用LayoutInflater渲染View的流程,以方式2為例子做說明吧,在方式2中rootView是由inflater.inflate()方法生成的,我們進入inflate()方法中看一下其內部的執行流程,代碼如下:
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* R.layout.main_page
)
* @param root Optional view to be the parent of the generated hierarchy.
* @return The root View of the inflated hierarchy. If root was supplied,
* this is the root View; otherwise it is the root of the inflated
* XML file.
*/
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
inflate()方法中什麼都沒做直接調用了其同名的重載方法inflate(),我們接著往裡跟進,代碼如下:
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* R.layout.main_page
)
* @param root Optional view to be the parent of the generated hierarchy (if
* attachToRoot is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if attachToRoot is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在該inflate()方法中通過調用getContext().getResource().getLayout()的方式根據傳遞進來的布局資源ID生成一個XmlResourceParser實例對象parser,這個parser就是用來解析布局文件的(有關在Java中如何解析xml文件,請自行查閱,這裡不再介紹),根據資源ID獲取到解析器parser後調用了參數有XmlPullParser的重載方法inflate(),我們繼續進入該代碼中看一下執行流程,代碼如下:
/** * Inflate a new view hierarchy from the specified XML node. Throws * {@link InflateException} if there is an error. *
* Important For performance * reasons, view inflation relies heavily on pre-processing of XML files * that is done at build time. Therefore, it is not currently possible to * use LayoutInflater with an XmlPullParser over a plain XML file at runtime. * * @param parser XML dom node containing the description of the view * hierarchy. * @param root Optional view to be the parent of the generated hierarchy (if * attachToRoot is true), or else simply an object that * provides a set of LayoutParams values for root of the returned * hierarchy (if attachToRoot is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to * the root parameter? If false, root is only used to create the * correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ 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("
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; } }方法createViewFromTag()主要流程就是通過標簽名name來創建相應View實例對象並返回。在該方法中首先根據Factory實例對象來創建View,如果創建成功就直接返回,否則執行系統默認創建View流程。這裡需要強調一點,LayoutInflater內部定義了一個boolean類型的mFactorySet開關,其值默認值為false,當我們調用過setFactory()或者是setFactory2()後mFactorySet為true,若我們再次調用這倆方法時會拋出異常,也就是說每一個LayoutInflater實例對象只能賦值一次Factory,若再想賦成其他值只能通過反射先把mFactorySet的值置為false防止拋異常。系統默認創建View流程是先通過判斷標簽名稱中有沒有包含".",如果沒有包含就把標簽名添加前綴"android.view.",最終調用LayoutInflater的createView()方法,注意該方法是public並且是final類型的,是系統默認的創建View的方式,創建完成之後返回該view。 到這裡我們已經清楚了LayoutInflater根據xml布局文件來渲染View視圖的主要流程:先是通過布局文件的資源ID創建一個XmlResourceParser解析器對象parser,再是利用parser遞歸解析xml布局文件,然後根據解析出的標簽名來創建相關View,最終返回層級視圖View。如果LayoutInflater中設置了Factory,那麼在創建每一個View時都會調用該Factory的onCreateView()方法,這個方法就是我們的入口點,如果想在每一個View創建之前做點處理,只需要在Factory的onCreateView()方法中做相關邏輯操作... 既然已經找到了創建View的切入口,那怎麼樣才能實現主題切換功能呢?主題切換通常是更改背景以及文字顏色等,在做更改之前要先知道哪些View需要更改,那我們怎麼才能知道布局文件中的View需要做主題切換了?自定義屬性是推薦的做法,當布局文件中使用了自定義屬性就表示該View是做主題切換功能的,在該View創建後把它裝入集合中,當需要主題切換時循環遍歷該集合更改View相關屬性就好了... 好了,由於篇幅的緣故,本篇博文先到這裡,我會在下一篇文章中以案例的形式演示如何利用LayoutInflater的Factory接口實現切換主題的功能,敬請期待……
發現現在好多App都使用了透明狀態欄,眼紅了好久但是又懶得寫,但是。這高大上的狀態欄真的好有逼格啊,對於有點強迫症的我來說簡直不能忍,所以還是寫篇博客記錄一下,方便以後使
上一節,已經完成了來電管家的界面設計,那麼下面就要實現具體的功能了,如何將添加的黑白名單顯示呢?這裡用到了ListView,那麼,如果需要刪除黑白名單呢,是一個個長按彈出
問題說明:其實這個問題有可能是沒有問題需要多試幾次 解決辦法:重新用手機連接電腦,並且重新打開USB調試模式(部分ROM默認打開的,如果找不到),再次自動
PS:最近看到很多人都開始寫年終總結了,時間過得飛快,又到年底了,又老了一歲。學習內容:1.進度條2.縮放3.ControllerBuilder,ControllerLi