淺談Android換膚系列一
本篇文章就具體介紹一下Factory在換膚上的具體應用。
在上一篇博文中我們在Factory中打印了一下輸出后的AttributeSet信息如下:
gravity:0x11
background:#ffff0000
layout_width:-1
layout_height:48.0dip
text:@2131361814
這里只是打印出了View的屬性值和屬性名稱,當然了資源名稱和資源類型也可以打印出來,下面是獲取屬性和資源的四個相關方法:
//位于AttributeSet類中
public String getAttributeName(int index);//屬性名稱
public String getAttributeValue(int index);//屬性值
//位于Resources類
public String getResourceEntryName(@AnyRes int resid)//資源名稱
public String getResourceTypeName(@AnyRes int resid)//資源類型
在對每一個需要換膚的View進行操作的時候,資源類型一般就是兩種類型,要么是color要么是一個drawable,但是屬性就比較多了,有可能是一個textColor,有可能是background,如單復選按鈕還可能是一個button,因此我們可以將屬性相關的類設計為一個工廠類SkinFactory,根據不同的屬性設置不同的屬性類,如TextColorAttr或者BackgroundAttr。另外一個需要注意的就是在進行換膚的時候資源應該都是以引用的形式設置,此時輸出的結果都是 一個以@開始的屬性值 ,@后面對應的值就是一個資源的ID,然后我們通過下面兩個方法就可以獲取資源真正的ID了。
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = resources.getIdentifier(resName, “color”, skinPackageName);
換膚的核心就在這里了,只要我們將資源Resources獲取到就可以獲取到資源中相應的資源了,無論是應用內換膚還是插件式換膚都是一樣的,只是獲取以及處理資源的方式不同罷了,本篇重點講解的是插件式換膚,類似QQ方式,可以遠程下載皮膚庫到本地,然后進行換膚。
首先確定屬性的基類,基類中至少需要四個屬性:View屬性名稱和值以及資源名稱和類型,另外由于資源類型確定為只有兩種類型:color和drawable,可以將資源類型設置為兩個常量。當我們獲取到插件包中的資源庫以后,根據不同的屬性實現類必須有不同的實現方法,如果是字體顏色apply()方法中可以是view.setTextColor()方法等等,因此在基類中可以設置一個抽象方法,所有實現該基類的子類必須實現該方法,SkinAttr基類代碼如下:
/**
* android:textColor="@color/text_default"
* View屬性名稱 attrName:textColor
* 資源ID attrValueId R類中對text_default對應的一個整型值
* 資源名稱 attrEntryName:text_default
* 資源類型 attrEntryType:color
*/
public abstract class SkinAttr {
//資源類型color
protected static final String TYPE_NAME_COLOR = "color";
//資源類型drawable
protected static final String TYPE_NAME_DRAWABLE = "drawable";
//屬性名稱
public String attrName;
//屬性引用資源ID
public int attrValueId;
//資源名稱
public String attrEntryName;
//資源類型
public String attrEntryType;
//不同子類必須要實現的方法
public abstract void apply(Viewview);
@Override
public String toString() {
return "SkinAttr [attrName=" + attrName + ", attrValueId=" + attrValueId + ", attrEntryName=" + attrEntryName
+ ", attrEntryType=" + attrEntryType + "]";
}
}
根據不同的屬性可以創建不同的實現類,如textColor對應TextColorAttr,background對應BackgroundAttr等等。
public class TextColorAttr extends SkinAttr {
@Override
public void apply(Viewview) {
if (viewinstanceof TextView) {
TextViewtv = (TextView) view;
if (TYPE_NAME_COLOR.equals(attrEntryType)) {
tv.setTextColor(SkinManager.getInstance().getColorStateList(attrValueId));
}
}
}
}
public class BackgroundAttr extends SkinAttr {
@Override
public void apply(Viewview) {
if (TYPE_NAME_COLOR.equals(attrEntryType)) {
view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueId));
} else if (TYPE_NAME_DRAWABLE.equals(attrEntryType)) {
Drawablebg = SkinManager.getInstance().getDrawable(attrValueId);
view.setBackgroundDrawable(bg);
}
}
}
當所有的屬性實現類完成后,接著設計一個工廠類AttrFactory,根據獲取的屬性名稱不同生成不同的屬性實現類,將來一旦有其它屬性或者自定義屬性需要實現換膚只需在該類中添加相應的實現即可。在AttFactory類中需要增加一個判斷方法,因為在LayoutInflater.Factory中輸出的屬性是View的所有屬性,但是并不是所有屬性都需要實現換膚邏輯,只需要將所有需要實現換膚邏輯的屬性定義為常量,在生成SkinAttr實現類之前判斷一下該屬性是否需要實現換膚邏輯。
public class AttrFactory {
public static final String BACKGROUND = "background";
public static final String TEXT_COLOR = "textColor";
public static final String LIST_SELECTOR = "listSelector";
public static final String DIVIDER = "divider";
public static final String BUTTON = "button";
public static SkinAttrget(String attrName, int attrValueId, String attrEntryName, String attrEntryType) {
SkinAttrmSkinAttr = null;
if (BACKGROUND.equals(attrName)) {
mSkinAttr = new BackgroundAttr();
} else if (TEXT_COLOR.equals(attrName)) {
mSkinAttr = new TextColorAttr();
}else if (BUTTON.equals(attrName)) {
mSkinAttr = new ButtonAttr();
} else if (LIST_SELECTOR.equals(attrName)) {
mSkinAttr = new ListSelectorAttr();
} else if (DIVIDER.equals(attrName)) {
mSkinAttr = new DividerAttr();
} else {
return null;
}
mSkinAttr.attrName = attrName;
mSkinAttr.attrValueId = attrValueId;
mSkinAttr.attrEntryName = attrEntryName;
mSkinAttr.attrEntryType = attrEntryType;
return mSkinAttr;
}
/**
* 判斷是否需要換膚
* @param attrName
* @return
*/
public static boolean isSupportedAttr(String attrName) {
if (BACKGROUND.equals(attrName)) {
return true;
}
if (BUTTON.equals(attrName)) {
return true;
}
if (TEXT_COLOR.equals(attrName)) {
return true;
}
if (LIST_SELECTOR.equals(attrName)) {
return true;
}
if (DIVIDER.equals(attrName)) {
return true;
}
return false;
}
}
屬性的相關類基本封裝好了,一個View在換膚的時候可能需要涉及到多個屬性,如字體顏色、背景色等,因此我們可以將View和需要換膚的屬性再封裝進一個類中,在進行換膚的時候調用一個apply方法,通過資源依次賦值到View的屬性中。
public class SkinView {
public Viewview;
//所有涉及到需要換膚的屬性
public List<SkinAttr> attrs;
public SkinView() {
attrs = new ArrayList<>();
}
//將屬性依次賦值到View
public void apply() {
if (view != null && !ListUtils.isEmpty(attrs)) {
for (SkinAttrattr : attrs) {
attr.apply(view);
}
}
}
//在銷毀時清除
public void clean() {
if (ListUtils.isEmpty(attrs)) {
return;
}
for (SkinAttrat : attrs) {
at = null;
}
}
}
走到這里屬性封裝好了,涉及到換膚的View也封裝完畢,接下來就是邏輯實現了,針對每個屬性的apply()方法該如何實現呢?我們知道在設置字體顏色的時候一般使用的是 resource.getColor(@ColorRes int id) ,背景圖片 resource.getDrawable(@DrawableRes int id) ,只要可以獲取到插件包的Resources就可以使用插件包中的資源文件了。
獲取插件包中Resources,在Resources的一個構造方法中可以傳入AssetManager實例,AssetManager中addAssetPath()方法可以將一個apk中的資源加載到Resources對象中,由于addAssetPath是隱藏API我們無法直接調用,所以只能通過反射。下面是它的聲明,通過注釋我們可以看出,傳遞的路徑可以是zip文件也可以是一個資源目錄,而apk就是一個zip,所以直接將apk的路徑傳給它,資源就加載到AssetManager中了。然后再通過AssetManager來創建一個新的Resources對象,通過這個對象我們就可以訪問插件apk中的資源了,這樣一來問題就解決了。
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
由于AssetManager的構造方法也是隱藏API,所以獲取AssetManager實例也需要使用反射技術,然后調用addAssetPath()方法。將遠程獲取的資源包存入SDcard,然后使用一個異步任務來操作比較耗時的IO操作,下面就是使用AsyncTask獲取資源包中的Resources對象。
new AsyncTask<String, Void, Resources>() {
@Override
protected void onPreExecute() {
if (listener != null) {
listener.onStart();
}
}
@Override
protected ResourcesdoInBackground(String... params) {
try {
if (params.length == 1) {
String skinPkgPath = params[0];
Filefile = new File(skinPkgPath);
if (file == null || !file.exists()) {
return null;
}
PackageManagermgr = context.getPackageManager();
PackageInfoinfo = mgr.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = info.packageName;
AssetManagerassetManager = AssetManager.class.newInstance();
MethodaddAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
ResourcessuperRes = context.getResources();
ResourcesskinResource = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
SkinConfig.saveSkinPath(context, skinPkgPath);
skinPath = skinPkgPath;
isDefaultSkin = false;
return skinResource;
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected void onPostExecute(Resourcesresult) {
resources = result;
if (resources != null) {
if (listener != null) {
listener.onSuccess();
}
notifySkinUpdate();
} else {
isDefaultSkin = true;
if (listener != null) {
listener.onFailed();
}
}
}
}.execute(skinPackagePath);
獲取到資源Resources后,就可以通過Resources得到color或者drawable了。
public int getColor(int resId) {
int originColor = ContextCompat.getColor(context, resId);
if (resources == null || isDefaultSkin) {
return originColor;
}
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = resources.getIdentifier(resName, "color", skinPackageName);
int trueColor = 0;
try {
trueColor = ResourcesCompat.getColor(resources, trueResId, null);
} catch (NotFoundException e) {
e.printStackTrace();
trueColor = originColor;
}
return trueColor;
}
由于換膚涉及到整個應用,所以我們可以為操作換膚的類設計為一個單例模式的類SkinManager,在SkinManager中傳入一個Context對象,可以直接使用ApplicationContext。
public void init(Contextctx) {
context = ctx.getApplicationContext();
}
public static SkinManagergetInstance() {
return InstanceHolder.holder;
}
private static class InstanceHolder {
private static SkinManagerholder = new SkinManager();
}
由于傳入的是ApplicationContext,當我們在Activity中使用進行換膚的時候,因為Application是在應用的整個生命周期內的,所以到某個Activity進行垃圾回收時不能被回收,引起內存溢出,因為該Activity持有Application的引用,所以我們需要再設計兩個方法。
@Override
public void attach(SkinObserverobserver) {
if (skinObservers == null) {
skinObservers = new ArrayList<SkinObserver>();
}
if (!skinObservers.contains(observer)) {
skinObservers.add(observer);
}
}
@Override
public void detach(SkinObserverobserver) {
if (skinObservers == null)
return;
if (skinObservers.contains(observer)) {
skinObservers.remove(observer);
}
}
當資源獲取后,就可以通知Activity進行換膚回調了,如果沒有回調,當我們在Activity任務棧回退操作的時候,導致上一個界面仍然保持了換膚之前的狀態。所以這時候我們就知道了換膚操作實際上采用的是觀察者模式,當Activity進入任務棧的時候SkinManger調用attach()方法,銷毀的時候調用detach()方法。
public interface SkinObservable {
void attach(SkinObserverobserver);
void detach(SkinObserverobserver);
void notifySkinUpdate();
}
public interface SkinObserver {
void onThemeUpdate();
}
public class SkinManager implements SkinObservable {
...
@Override
public void notifySkinUpdate() {
if (skinObservers == null)
return;
for (SkinObserverobserver : skinObservers) {
observer.onThemeUpdate();
}
}
}
public class BaseActivity extends AppCompatActivity implements SkinObserver {
...
@Override
public void onThemeUpdate() {
skinFactory.applySkin();
}
}
本篇博客暫時介紹到這里,在后續博客中我們繼續介紹SkinFactory的設計,如何在SkinFactory中在不影響系統本身創建View的條件下進行換膚操作,當然了換膚操作比實際想象中的要復雜一些,這里講解的都是通過布局文件生成的View,如果通過Java代碼new的View又該如何換膚呢?這里講解的主要是插件式換膚,但是如果是應用內換膚資源又該如何操作呢?還有App中如果使用了WebView加載的網頁等等,如應用市場中網易新聞、開發者頭條、知乎都涉及到了WebView對網頁的換膚操作,在后續文章中都會逐一揭曉。
來自:http://www.sunnyang.com/667.html