轉載請註明出處:http://blog.csdn.net/llew2011/article/details/51287391
在上篇文章Android 源碼系列之<四>從源碼的角度深入理解LayoutInflater.Factory之主題切換(上)中我們主要講解了LayoutInflater渲染xml佈局文件的流程,文中講到如果在渲染之前爲LayoutInflater設置了Factory,那麼在渲染每一個View視圖時都會調用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口實現主題切換功能。如果你不清楚LayoutInflater的渲染流程,請點擊這裏。今天我們就從實戰出發來實現自己的主題切換功能。
既然主題切換是依賴Factory的,那麼就需要定義自己的Factory了,自定義Factory其實就是實現系統的Factory接口,代碼如下:
public class SkinFactory implements Factory {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
Log.e("SkinFactory", "==============start==============");
int attrCounts = attrs.getAttributeCount();
for(int i = 0; i < attrCounts; i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
Log.e("SkinFactory", "attrName = " + attrName + " attrValue = " + attrValue);
}
Log.e("SkinFactory", "==============end==============");
return null;
}
}
自定義SkinFactory什麼都沒有做,僅僅在onCreateView()方法中循環打印了attrs包含的屬性名和對應的屬性值,然後返回了null。創建完SkinFactory之後就是如何使用它了,上篇文章中我們講過在Activity中可以通過getLayoutInflater()方法獲取LayoutInflater實例對象,獲取到該對象之後就可以給該其賦值Factory了,代碼如下:public class MainActivity extends Activity {
private LayoutInflater mInflater;
private SkinFactory mFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFactory = new SkinFactory();
mInflater = getLayoutInflater();
mInflater.setFactory(mFactory);
setContentView(R.layout.activity_skin);
}
}
需要注意的是給Activity的LayoutInflater設置Factory時一定要在調用setContentView()方法之前,否則不起作用。設置好Factory之後,我們看看一下activity_skin.xml佈局文件是如何定義的,代碼如下:<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_app_bg" >
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Factory的小練習"
android:textColor="@color/color_title_bar_text" />
</FrameLayout>
佈局文件居中顯示了一個TextView,並且給TextView設置文本爲"Factory的小練習",運行一下程序,打印結果如下:這裏只貼出了TextView的打印數據,從打印出的數據可以發現如果屬性值是以@開頭就表示該屬性值是一個應用(以後可以通過@符號來判斷當前屬性是否是引用)。因爲我們可以在attrs中拿到View在佈局文件中定義的所有屬性,所以可以猜想:如果給View添加自定義屬性,在onCreateView()方法中通過解析這個自定義屬性就可以判別出要做主題切換的View了。這個猜想正不正確,我們來試驗一下。
在values文件夾下創建attrs.xml屬性文件,定義屬性名爲enable,屬性值爲boolean類型(true表示需要主題切換,false表示不需要主題切換),代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="enable" format="boolean" />
</resources>
定義完屬性後,若要使用該屬性需要先申明命名空間,比如系統自帶的:xmlns:android="http://sckemas.android.com/apk/res/android",申明命名空間有兩種方法:xmlns:skin="http://schemas.android.com/apk/包名"或者是xmlns:skin="http://schemas.android.com/apk/res-auto"。我們採用第二種寫法,代碼如下:<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:skin="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_app_bg" >
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Factory的小練習"
skin:enable="true"
android:textColor="@color/color_title_bar_text" />
</FrameLayout>
在activity_skin.xml佈局文件中給TextView添加了自定義的enable屬性並把值設爲true,添加完屬性後編譯器報錯提示說TextView沒有該屬性,只要手動清理一下就好了。然後運行代碼,打印結果如下:看到打印結果我們心裏好happy呀,採用給View添加自定義的屬性這種方式是OK的,接下來我們就可以根據該屬性區分出哪些View需要做主題切換了。做主題切換的前提是緩存那些需要做主題切換的View,但是View做主題切換可能需要更改背景,文字等。也就說一個View可能要更改多個屬性,那這個屬性就要求在不同的場景下對應不同的類型,所以可以抽象出代表屬性的類BaseAttr,BaseAttr類有屬性名,屬性值,屬性類型等成員變量,還要有一個抽象方法(該方法在不同的場景下有不同的實現,比如當前屬性爲background,那在BackgroundAttr實現中就應該是設置背景;若當前屬性爲textColor,那在TextColorAttr實現中就應該是設置文字顏色)。所以BaseAttr可以抽象如下:
public abstract class BaseAttr {
public String attrName;
public int attrValue;
public String entryName;
public String entryType;
public abstract void apply(View view);
}
定義好BaseAttr類之後就可以定義具體的實現類了,比如背景屬性類BackgroundAttr,字體顏色改變類TextColorAttr等,BackgroundAttr代碼如下:
public class BackgroundAttr extends BaseAttr {
@Override
public void apply(View view) {
if(null != view) {
view.setBackgroundXXX();
}
}
}
抽象出屬性類BaseAttr之後我們還要考慮緩存View的問題,因爲一個View可能要對應多個BaseAttr,所以我們還要封裝一個類SkinView,該類表示一個View對應多個BaseAttr,它還要提供更新自己的方法,所以代碼如下:
public class SkinView {
public View view;
public List<BaseAttr> viewAttrs;
public void apply() {
if(null != view && null != viewAttrs) {
for(BaseAttr attr : viewAttrs) {
attr.apply(view);
}
}
}
}
抽象屬性類BaseAttr和SkinView定義完了,接下來就可以在SkinFactory中做緩存邏輯了,代碼如下:public class SkinFactory implements Factory {
private static final String DEFAULT_SCHEMA_NAME = "http://schemas.android.com/apk/res-auto";
private static final String DEFAULT_ATTR_NAME = "enable";
private List<SkinView> mSkinViews = new ArrayList<SkinView>();
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
final boolean skinEnable = attrs.getAttributeBooleanValue(DEFAULT_SCHEMA_NAME, DEFAULT_ATTR_NAME, false);
if(skinEnable) {
view = createView(name, context, attrs);
if(null != view) {
parseAttrs(name, context, attrs, view);
}
}
return view;
}
public final View createView(String name, Context context, AttributeSet attrs) {
View view = null;
if(-1 == name.indexOf('.')) {
if("View".equalsIgnoreCase(name)) {
view = createView(name, context, attrs, "android.view.");
}
if(null == view) {
view = createView(name, context, attrs, "android.widget.");
}
if(null == view) {
view = createView(name, context, attrs, "android.webkit.");
}
} else {
view = createView(name, context, attrs, null);
}
return view;
}
View createView(String name, Context context, AttributeSet attrs, String prefix) {
View view = null;
try {
view = LayoutInflater.from(context).createView(name, prefix, attrs);
} catch (Exception e) {
}
return view;
}
private void parseAttrs(String name, Context context, AttributeSet attrs, View view) {
int attrCount = attrs.getAttributeCount();
final Resources temp = context.getResources();
List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>();
for(int i = 0; i < attrCount; i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(isSupportedAttr(attrName)) {
if(attrValue.startsWith("@")) {
int id = Integer.parseInt(attrValue.substring(1));
String entryName = temp.getResourceEntryName(id);
String entryType = temp.getResourceTypeName(id);
BaseAttr viewAttr = createAttr(attrName, attrValue, id, entryName, entryType);
if(null != viewAttr) {
viewAttrs.add(viewAttr);
}
}
}
}
if(viewAttrs.size() > 0) {
SkinView skinView = new SkinView();
skinView.view = view;
skinView.viewAttrs = viewAttrs;
mSkinViews.add(skinView);
}
}
// attrName:textColor attrValue:2130968576 entryName:common_bg_color entryType:color
private BaseAttr createAttr(String attrName, String attrValue, int id, String entryName, String entryType) {
BaseAttr viewAttr = null;
if("background".equalsIgnoreCase(attrName)) {
viewAttr = new BackgroundAttr();
} else if("textColor".equalsIgnoreCase(attrName)) {
viewAttr = new TextColorAttr();
}
if(null != viewAttr) {
viewAttr.attrName = attrName;
viewAttr.attrValue = id;
viewAttr.entryName = entryName;
viewAttr.entryType = entryType;
}
return viewAttr;
}
private boolean isSupportedAttr(String attrName) {
if("background".equalsIgnoreCase(attrName)) {
return true;
} else if("textColor".equalsIgnoreCase(attrName)) {
return true;
}
return false;
}
public void applaySkin() {
if(null != mSkinViews) {
for(SkinView skinView : mSkinViews) {
if(null != skinView.view) {
skinView.apply();
}
}
}
}
}
SkinFactory中定義了裝載SkinView類型的mSkinViews緩存集合,當解析到符合條件的View時就會緩存到該集合中。在onCreateView()方法中調用AttributeSet的getAttributeBooleanValue()方法檢測是否含有enable屬性,如果有enable屬性並且屬性值爲true時我們自己調用系統API來創建View,如果創建成功就解析該View,分別獲取其attrName,attrValue,entryName,entryType值取完之後創建對應的BaseAttr,然後加入緩存集合mSkinViews中,否則返回null。
創建完SkinFactory之後還需要創建一個主題資源管理器SkinManager,主題切換就是通過該管理器來決定的。所以其主要有以下功能:實現讀取額外主題資源功能,恢復默認主題功能,更新主題功能等。
先看一下如何讀取額外主題資源問題。做主題切換需要準備多套主題,這些主題其實就是一些圖片,顏色等。有了素材之後我們還要考慮如何提供給APP素材的形式,是直接提供一個Zip包文件還是說做成一個apk文件的形式提供給APP?如果提供Zip包接下來的處理是解壓該Zip包得到裏邊的素材然後解析讀取,理論上來說這種方式是可行的,但是操作起來有點複雜。所以我們採用apk的形式,若希望訪問素材apk中的資源如同在APP中訪問資源一樣,我們得獲取到素材apk的Resources實例,下面我直接提供一種通用的可以獲取apk的Resources實例代碼,代碼如下:
public final Resources getResources(Context context, String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, apkPath);
Resources r = context.getResources();
Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
return skinResources;
} catch (Exception e) {
}
return null;
}
這段代碼可以有效的獲取到apk中的Resources實例,然後通過該Resources實例訪問資源就如同我們在APP中直接訪問自己資源一般,如果你對Android的資源訪問機制很熟悉的話,很清楚這段代碼爲什麼要這麼寫。不清楚也沒關係,先暫時這麼用,我會在後續文章中從源碼的角度分析一下Android的資源訪問機制並解釋這麼寫的原因。
好了,現在我們已經解決了訪問素材資源的問題,那接下來就是編寫我們的SkinManager類了,SkinManager類的功能是來加載素材資源文件的,在加載文件時可能有失敗的情況,所以需要給APP回調來通知加載資源的結果,我們定義接口ILoadListener,代碼如下:
public interface ILoadListener {
void onStart();
void onSuccess();
void onFailure();
}
ILoadListener接口有三個方法,分別表示資源開始加載的回調,加載成功後的回調和加載失敗後的回調。我們接着完成我們SkinManager代碼,如下所示:public final class SkinManager {
private static final Object mClock = new Object();
private static SkinManager mInstance;
private Context mContext;
private Resources mResources;
private String mSkinPkgName;
private SkinManager() {
}
public static SkinManager getInstance() {
if(null == mInstance) {
synchronized (mClock) {
if(null == mInstance) {
mInstance = new SkinManager();
}
}
}
return mInstance;
}
public void init(Context context) {
enableContext(context);
mContext = context.getApplicationContext();
}
public void loadSkin(String skinPath) {
loadSkin(skinPath, null);
}
public void loadSkin(final String skinPath, final ILoadListener listener) {
enableContext(mContext);
if(TextUtils.isEmpty(skinPath)) {
return;
}
new AsyncTask<String, Void, Resources>() {
@Override
protected void onPreExecute() {
if(null != listener) {
listener.onStart();
}
}
@Override
protected Resources doInBackground(String... params) {
if(null != params && params.length == 1) {
String skinPath = params[0];
File file = new File(skinPath);
if(null != file && file.exists()) {
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, 1);
if(null != packageInfo) {
mSkinPkgName = packageInfo.packageName;
}
return getResources(mContext, skinPath);
}
}
return null;
}
@Override
protected void onPostExecute(Resources result) {
if(null != result) {
mResources = result;
if(null != listener) {
listener.onSuccess();
}
} else {
if(null != listener) {
listener.onFailure();
}
}
}
}.execute(skinPath);
}
public Resources getResources(Context context, String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, apkPath);
Resources r = context.getResources();
Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
return skinResources;
} catch (Exception e) {
}
return null;
}
public void restoreDefaultSkin() {
if(null != mResources) {
mResources = null;
mSkinPkgName = null;
}
}
public int getColor(int id) {
enableContext(mContext);
Resources originResources = mContext.getResources();
int originColor = originResources.getColor(id);
if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
return originColor;
}
String entryName = mResources.getResourceEntryName(id);
int resourceId = mResources.getIdentifier(entryName, "color", mSkinPkgName);
try {
return mResources.getColor(resourceId);
} catch (Exception e) {
}
return originColor;
}
public Drawable getDrawable(int id) {
enableContext(mContext);
Resources originResources = mContext.getResources();
Drawable originDrawable = originResources.getDrawable(id);
if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
return originDrawable;
}
String entryName = mResources.getResourceEntryName(id);
int resourceId = mResources.getIdentifier(entryName, "drawable", mSkinPkgName);
try {
return mResources.getDrawable(resourceId);
} catch (Exception e) {
}
return originDrawable;
}
private void enableContext(Context context) {
if(null == context) {
throw new NullPointerException();
}
}
}
SkinManager我們採用了單例模式保證應用中只有一個實例,在使用的時候需要先進行初始化操作否則會拋異常。SkinManager不僅定義了屬性mContext和mResources(mContext表示APP的運行上下文環境,mResources代表資源apk的Resources實例對象,如果爲空表示使用默認APP主題資源),而且它還對外提供了一系列方法,比如讀取資源的getColor()和getDrawable()方法,加載資源apk的方法loadSkin()等。
現在主題切換的核心邏輯都有了,我們看一下程序包結構圖是怎樣的,切圖如下:
主題切換的核心邏差不多已經然完成了,接下來就是要練習使用一下看看效果能不能成了,首先修改activity_skin.xml佈局文件,修改如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:skin="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/common_bg_color"
android:orientation="vertical"
skin:enable="true" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="@color/common_title_bg_color"
skin:enable="true" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="主題切換標題"
android:textColor="@color/common_title_text_color"
android:textSize="18sp"
skin:enable="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center_vertical"
android:onClick="updateSkin"
android:text="切換主題" />
</FrameLayout>
</LinearLayout>
在activity_skin.xml佈局文中給需要做主題切換的View節點添加了enable屬性並且設置其值爲true。接下來就是要做一個主題apk包了,做主題包的簡單方式就是新建一個工程,裏邊不添加Activity等,然後在資源文件夾下創建對應的資源等,需要注意的是資源文件名一定要和APP中的資源名一致。然後編譯打包成一個apk文件,這裏就不再演示了。打包完apk後我們導入到模擬器根目錄下,然後修改MainActivity,添加updateSkin()方法,代碼如下:public void updateSkin(View view) {
String skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "skin.apk";
SkinManager.getInstance().loadSkin(skinPath, new ILoadListener() {
@Override
public void onSuccess() {
mFactory.applaySkin();
}
@Override
public void onStart() {
}
@Override
public void onFailure() {
}
});
}
添加完updateSkin()方法之後,就可以實現切換主題了,爲了方便我直接把skin.apk文件直接導入了SD卡根目錄下,需要注意有的手機沒有外置存儲卡需要做個判斷,別忘了在配置文件添加文件的讀寫權限,然後運行程序,效果如下:好了,現在在當前頁面進行主題切換看起來是OK的,但是還存在不足,當頁面進行跳轉比如從A→B→C→D然後在D中進行主題切換,這時候ABC是沒有效果的,另外代碼的通用性也不強,所以在下篇文章Android 源碼系列之<六>從源碼的角度深入理解LayoutInflater.Factory之主題切換(下)處理這些問題,敬請期待...