本篇文章是针对Android端换肤框架Android-Skin-Loader的源码解析
整个框架的架构
从加载皮肤说起
SkinManager.getInstance( ).load
一行代码便实现了换肤功能,那么进入该方法看看具体是怎样实现的
SkinManager.java
public void load(String skinPackagePath, final ILoaderListener callback) {
new AsyncTask<String, Void, Resources>() {
protected void onPreExecute() {
if (callback != null) {
callback.onStart(); //对外通知 开始加载皮肤
}
};
@Override
protected Resources doInBackground(String... params) {
try {
if (params.length == 1) {
String skinPkgPath = params[0];
File file = new File(skinPkgPath);
if(file == null || !file.exists()){
return null;
}
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES); //获取apk包信息
skinPackageName = mInfo.packageName;
//反射构造AssetManager实例,将皮肤包加载进内存,然后通过新构造的Resources去加载皮肤资源
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
Resources skinResource = 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(Resources result) {
mResources = result; //得到异步处理结果,然后在SkinManager实例中维持Resources
if (mResources != null) {
if (callback != null) callback.onSuccess(); //对外通知 加载皮肤成功
notifySkinUpdate(); //通知观察者改变皮肤
}else{
isDefaultSkin = true;
if (callback != null) callback.onFailed(); //对外通知 加载皮肤失败
}
};
}.execute(skinPackagePath);
}
第一个参数代表外部皮肤包的绝对路径(通过gradle打包),第二个参数是加载皮肤成功与否的回调接口;
load方法主要构造了一个AsyncTask执行异步任务,通过PackageManager获取皮肤的包信息(皮肤本质上是个apk),利用反射构造AssetManager实例,调用addAssetPath方法将皮肤包加载进内存
addAssetPath方法中的关键代码
public int addAssetPath(String path) {
return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}
private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
......
final ApkAssets assets;
try {
if (overlay) {
assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
} else {
assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
}
} catch (IOException e) {
return 0;
}
mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
mApkAssets[count] = assets;
......
}
}
android源码中对ApkAssets的解释:The loaded, immutable, in-memory representation of an APK.
也就是说,是Apk包在内存中的类实例
接着load方法分析,利用新构造的AssetManager去new Resources目的是通过Resources来动态加载资源文件,执行完异步任务后,就在onPostExecute方法中将新的Resources赋值给字段mResources,这样加载皮肤时使用mResources,恢复默认皮肤时使用context.getResources( )加载资源,最后notifySkinUpdate( )通知view改变皮肤
在进入notifySkinUpdate( )分析之前,我们先分析一下SkinManager类
SkinManager
为了便于分析逻辑,将部分代码省略
public class SkinManager implements ISkinLoader{
......
private List<ISkinUpdate> skinObservers;
private String skinPackageName;
private Resources mResources;
private String skinPath;
private boolean isDefaultSkin = false;
......
public void restoreDefaultTheme(){
......
}
public void load(){
......
}
public void load(ILoaderListener callback){
......
}
public void load(String skinPackagePath, final ILoaderListener callback) {
......
}
@Override
public void attach(ISkinUpdate observer) {
if(skinObservers == null){
skinObservers = new ArrayList<ISkinUpdate>();
}
if(!skinObservers.contains(observer)){
skinObservers.add(observer);
}
}
@Override
public void detach(ISkinUpdate observer) {
if(skinObservers == null) return;
if(skinObservers.contains(observer)){
skinObservers.remove(observer);
}
}
@Override
public void notifySkinUpdate() {
if(skinObservers == null) return;
for(ISkinUpdate observer : skinObservers){
observer.onThemeUpdate();
}
}
public int getColor(int resId){
......
}
public Drawable getDrawable(int resId){
......
}
}
在Application中初始化SkinManager,类中skinObservers字段存储所有的观察者,这个观察者是你实现了ISkinUpdate接口的Activity或Fragment
public interface ISkinUpdate {
void onThemeUpdate();
}
注意该类中的attach与detach方法,分别将 观察者(activity和Fragment) 建立订阅或解除订阅关系
接下来我们再来分析下notifySkinUpdate( )方法,遍历所有的观察者,并调用观察者的onThemeUpdate方法完成皮肤切换,那么我们看看观察者(Activity或Fragment)的onThemeUpdate需要如何实现
@Override
public void onThemeUpdate() {
if(!isResponseOnSkinChanging){
return;
}
mSkinInflaterFactory.applySkin();
}
进入SkinInflaterFactory的applySkin( )看看
public void applySkin(){
if(ListUtils.isEmpty(mSkinItems)){
return;
}
for(SkinItem si : mSkinItems){
if(si.view == null){
continue;
}
si.apply();
}
}
遍历list中的SkinItem,并调用apply( )方法,那么SkinItem又是什么东西呢?
public class SkinItem {
public View view;
public List<SkinAttr> attrs;
public SkinItem(){
attrs = new ArrayList<SkinAttr>();
}
public void apply(){
if(ListUtils.isEmpty(attrs)){
return;
}
for(SkinAttr at : attrs){
at.apply(view);
}
}
public void clean(){
if(ListUtils.isEmpty(attrs)){
return;
}
for(SkinAttr at : attrs){
at = null;
}
}
}
SkinItem的代码十分简洁,我们看看它的成员变量,SkinItem中持有了需要换肤的View,以及更换资源的属性(封装在SkinAttr中),apply( )方法实际上的任务是遍历并调用SkinAttr.apply(view)
public abstract class SkinAttr {
protected static final String RES_TYPE_NAME_COLOR = "color";
protected static final String RES_TYPE_NAME_DRAWABLE = "drawable";
/**
* name of the attr, ex: background or textSize or textColor
*/
public String attrName;
/**
* id of the attr value refered to, normally is [2130745655]
*/
public int attrValueRefId;
/**
* entry name of the value , such as [app_exit_btn_background]
*/
public String attrValueRefName;
/**
* type of the value , such as color or drawable
*/
public String attrValueTypeName;
/**
* Use to apply view with new TypedValue
* @param view
* @param tv
*/
public abstract void apply(View view);
}
SkinAttr中每个字段的含义就不赘述了,注释还是比较详尽;
下面我们分析具体的SkinAttr实现类,以框架中的BackgroundAttr类为例
public class BackgroundAttr extends SkinAttr {
@Override
public void apply(View view) {
if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){ //当属性type为color
view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){ //当属性type为drawable
Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
view.setBackground(bg);
}
}
}
为view的属性设置新的资源文件,我们进入kinManager.getInstance( ).getColor( )中看看具体实现
public int getColor(int resId){
int originColor = context.getResources().getColor(resId);
if(mResources == null || isDefaultSkin){
return originColor;
}
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "color", skinPackageName); //根据加载到内存中的皮肤包(apk)的packageName,得到皮肤包中的资源id
int trueColor = 0;
try{
trueColor = mResources.getColor(trueResId);
}catch(NotFoundException e){
e.printStackTrace();
trueColor = originColor;
}
return trueColor;
}
如果是默认皮肤,则使用context.getResources().getColor(resId)加载,如果是使用其他皮肤,则使用mResources.getColor(trueResId)加载,完成view属性的改变,即 完成皮肤切换
但你是否还有疑问,究竟什么时候确定哪些view是需要换肤的?什么时候将需要换肤的view封装进了SkinItem中,并使SkinInflaterFactory持有SkinItem?
请看下面的解析?
SkinInflaterFactory
为了便于分析逻辑,将部分代码省略
public class SkinInflaterFactory implements Factory {
/**
* Store the view item that need skin changing in the activity
*/
private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
......
}
private View createView(Context context, String name, AttributeSet attrs) {
......
}
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
......
}
public void applySkin(){
......
}
......
public void addSkinView(SkinItem item){
mSkinItems.add(item);
}
public void clean(){
......
}
}
SkinInflaterFactory实现了LayoutInflater的Factory接口,为什么要实现这个接口?
因为不自定义Factory,那么LayoutInflater就会交给系统去完成由xml布局到View实例的创建,自定义Factory后,在观察者(Activity或Fragment)中设置自定义的Factory
//Activity
getLayoutInflater().setFactory(mSkinInflaterFactory);
//Fragment
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(getLayoutInflater(), false);
mSkinInflaterFactory = new SkinInflaterFactory();
getLayoutInflater().setFactory(mSkinInflaterFactory);
这样我们就代理了由xml布局创建View实例的过程,当由xml布局文件创建View实例时,会调用SkinInflaterFactory的onCreateView( )方法
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// 判断xml布局中,需要换肤的控件是否将SkinConfig.NAMESPACE 命名空间下的SkinConfig.ATTR_SKIN_ENABLE属性设置为true
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
if (!isSkinEnable){
return null;
}
View view = createView(context, name, attrs); //由xml创建View实例
if (view == null){
return null;
}
parseSkinAttr(context, attrs, view);
return view;
}
private View createView(Context context, String name, AttributeSet attrs) {
View view = null;
try {
if (-1 == name.indexOf('.')){
if ("View".equals(name)) {
view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
}
}else {
view = LayoutInflater.from(context).createView(name, null, attrs);
}
L.i("about to create " + name);
} catch (Exception e) {
......
}
return view;
}
通过LayoutInflater的createView方法,根据xml标签中的控件名name和AttributeSet完成View实例的创建
LayoutInflater的createView关键源码如下
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException {
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
......
constructor = clazz.getConstructor(mConstructorSignature);
......
args[1] = attrs;
final View view = constructor.newInstance(args);
......
}
通过类加载和反射创建了View实例,创建View实例后,将调用parseSkinAttr将需要换肤的view和属性封装在SkinItem中
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
for (int i = 0; i < attrs.getAttributeCount(); i++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(!AttrFactory.isSupportedAttr(attrName)){
continue;
}
if(attrValue.startsWith("@")){
try {
int id = Integer.parseInt(attrValue.substring(1));
String entryName = context.getResources().getResourceEntryName(id);
String typeName = context.getResources().getResourceTypeName(id);
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); //封装需要切换资源的view属性
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}
}
}
if(!ListUtils.isEmpty(viewAttrs)){
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mSkinItems.add(skinItem); //在这里,将需要换肤的SkinItem(封装了View)添加到SkinInflaterFactory的list中
if(SkinManager.getInstance().isExternalSkin()){
skinItem.apply();
}
}
}
属性是如何封装的呢?
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 SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
SkinAttr mSkinAttr = null;
if(BACKGROUND.equals(attrName)){
mSkinAttr = new BackgroundAttr();
}else if(TEXT_COLOR.equals(attrName)){
mSkinAttr = new TextColorAttr();
}else if(LIST_SELECTOR.equals(attrName)){
mSkinAttr = new ListSelectorAttr();
}else if(DIVIDER.equals(attrName)){
mSkinAttr = new DividerAttr();
}else{
return null;
}
mSkinAttr.attrName = attrName;
mSkinAttr.attrValueRefId = attrValueRefId;
mSkinAttr.attrValueRefName = attrValueRefName;
mSkinAttr.attrValueTypeName = typeName;
return mSkinAttr;
}
/**
* Check whether the attribute is supported
*/
public static boolean isSupportedAttr(String attrName){
......
}
}
将资源的属性名,id,资源名,资源类型封装在SkinAttr中
最后在SkinInflaterFactory中持有一个list,list包含需要换肤SkinItem,SkinItem封装了View和属性
总结:SkinManager持有观察者(Activity或Fragment),当切换皮肤时,利用Resources动态加载皮肤资源,加载成功后,对外通知切换皮肤,遍历观察者,每个观察者中持有SkinFactory,每个SkinFactory中持有需要换肤的View,也就是List,调用每个SkinAttr中的apply( )方法,完成view属性资源的切换