1. 程式人生 > >10老司機的Android換膚筆記

10老司機的Android換膚筆記

EDA theme clas 效果 mst indexof psc resid valid

效果預覽

文章開始,我們先來預覽一下換膚功能所能實現的效果吧。如下圖所示:

換膚原理

本文所講述的換膚是通過幹預 xml 的解析實現的。在解析xml時,我們可以收集需要換膚的 view,並記錄下 view 的一些換膚信息,等要需要換膚的時候,從皮膚資源包中加載皮膚,設置到記錄的 view 上。

Activity加載xml文件

新建一個android項目,在MainActivity中覆寫onCreate()方法,代碼如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_setting);
...
}
為了能夠將xml顯示出來,我們必須調用 setContentView() 方法。

該方法源碼如下:

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
調用了 window 的 setContentView()。這裏 window 的實現是 PhoneWindow。

@Override
public void setContentView(int layoutResID) {

// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
installDecor() 是加載我們在activity上設置的theme信息。

重點是 mLayoutInflater.inflate(layoutResId, mContentParent);

調用了 LayoutInflater 的 inflate 方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
繼續調用同名方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("

  • Integer.toHexString(resource) + ")");
    }
    final XmlResourceParser parser = res.getLayout(resource);
    try {
    return inflate(parser, root, attachToRoot);
    } finally {
    parser.close();
    }
    }
    這裏就開始解析xml了。繼續跟蹤 inflate 方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
......
try {
......
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, inflaterContext, attrs, false);
    } else {
    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, inflaterContext, 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 against its context.
    rInflateChildren(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 (Exception 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;
    }
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    return result;
    }
    }
    當我們編寫一個xml給activity使用的時候,根節點肯定不會是 merge ,所以這裏走 else 裏面的代碼。首先,調用 createViewFromTag 創建根節點,然後調用 rInflateChildren 創建子節點。最後根據參數,判斷是否將根節點添加到 root 上。我們先看 createViewFromTag 的代碼:

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
調用同名方法:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let‘s party like it‘s 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf(‘.‘)) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()

  • ": Error inflating class " + name);
    ie.initCause(e);
    throw ie;
    } catch (Exception e) {
    final InflateException ie = new InflateException(attrs.getPositionDescription()
  • ": Error inflating class " + name);
    ie.initCause(e);
    throw ie;
    }
    }
    重點看 try 裏面的這段代碼:

    View view;
    if (mFactory2 != null) {
    view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
    view = mFactory.onCreateView(name, context, attrs);
    } else {
    view = null;
    }
    if (view == null && mPrivateFactory != null) {
    view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    if (view == null) {
    final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;
    try {
    if (-1 == name.indexOf(‘.‘)) {
    view = onCreateView(parent, name, attrs);
    } else {
    view = createView(name, null, attrs);
    }
    } finally {
    mConstructorArgs[0] = lastContext;
    }
    }
    如果 mFactory2 mFactory 其中一個有值,會是調用其 onCreateView 方法。

mFactor2 優先級高於 mFactory。如果都沒有值,使用 mPrivateFactory 的 onCreateView 方法。如果 mPrivateFactory 也為空,則使用自己的 onCreateView 或者 createView 方法。

PS:LayoutInflater 的 onCreateView 方法也會調到 createView 方法。

這3個Factory賦值都是在構造函數,以及 set 方法中。

再回到 PhoneWindow 的 setContentView 中:

@Override
public void setContentView(int layoutResID) {
......
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
......
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
......
}
查看 mLayoutInflater 是如何賦值的:

public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
查看 from 方法做了什麽:

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;
}
Context 的實現是 ContextImpl:

@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
往下調用:

public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
調用 ServiceFetcher 的 getService:

static abstract interface ServiceFetcher<T> {
T getService(ContextImpl ctx);
}
是一個接口,有抽象類(CachedServiceFetcher)實現了這個接口:

static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex;
public CachedServiceFetcher() {
mCacheIndex = sServiceCacheSize++;
}
@Override
@SuppressWarnings("unchecked")
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
synchronized (cache) {
// Fetch or create the service.
Object service = cache[mCacheIndex];
if (service == null) {
service = createService(ctx);
cache[mCacheIndex] = service;
}
return (T)service;
}
}
public abstract T createService(ContextImpl ctx);
}
先從cache裏面去找,找不到再去創建,創建的方法是抽象的。這裏先放著,看這個類的靜態代碼塊:

static {
.......

   registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, 
           new CachedServiceFetcher<LayoutInflater>() { 
       @Override 
       public LayoutInflater createService(ContextImpl ctx) { 
           return new PhoneLayoutInflater(ctx.getOuterContext()); 
       }}); 

....... 

}
從這裏可以看到,使用匿名內部類實現了上面的抽象方法,即 PhoneLayoutInflater 就是我們調用 LayoutInflater.from(context) 得到的。

看其構造函數:

public PhoneLayoutInflater(Context context) {
super(context);
}
PhoneLayoutInflater 繼承了 LayoutInflater :

protected LayoutInflater(Context context) {
mContext = context;
}
即 PhoneLayoutInflater 中並沒有給 mFractory mFractory2 mPrivateFactory 復制。所及加載xml的時候,使用的是內部的 createView 方法。

到這裏就不往下分析了,換膚的一個難點就已經被解決了—即如何幹預 xml 的解析。很顯然,通過分析源碼,只要給 LayoutInflater 設置一個 Factory 就好了,只不過我們需要自己實現 Factory 接口。

如何實現一個 Factory 這裏先放置一下,activity 的 xml 加載搞定了,那麽 fragment 的 xml 加載又是什麽樣的呢?

首先編寫一個 fragment ,如下:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_article_list, container, false);
initView(v);
return v;
}
可以看出加載xml是由inflate完成的。那這個參數是誰傳遞的呢?

想想我們平時使用fragment的方式:

private void initFragment() {
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = ArticleListFragment.newInstance();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
看看 FragmentManager 做了什麽,FragmentManager 的實現是FragmentManagerImpl(這裏我就只給出最終的代碼了,懶得分析了):

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {

       ...... 

    switch (f.mState) { 
        case Fragment.INITIALIZING: 

...... 

            f.onAttach(mHost.getContext()); 
...... 

           if (!f.mRetaining) { 
                f.performCreate(f.mSavedFragmentState); 
            } 
            f.mRetaining = false; 
            if (f.mFromLayout) { 
                // For fragments that are part of the content view 
                // layout, we need to instantiate the view immediately 
                // and the inflater will take care of adding it. 
                f.mView = f.performCreateView(f.getLayoutInflater( 
                        f.mSavedFragmentState), null, f.mSavedFragmentState); 
                if (f.mView != null) { 

                    ...... 

                    f.onViewCreated(f.mView, f.mSavedFragmentState); 
                } else { 
                    f.mInnerView = null; 
                } 
            }

重點是

f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), null, f.mSavedFragmentState);

這句代碼,inflater 參數就是 f.getLayoutInflater 這個方法得來的。

perfromCreateView 調用了 onCreateView。這裏需要註意的就是,getLayoutInflater方法是不可見的,但是我們依然可以覆蓋。

所以如果我們想要實現換膚,那麽 BaseActivity 和 BaseFragment 的代碼如下:

public class BaseActivity extends Activity {
private SkinInflaterFactory mSkinInflaterFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSkinInflaterFactory = new SkinInflaterFactory();
getLayoutInflater().setFactory(mSkinInflaterFactory);
}
@Override
protected void onDestroy() {
super.onDestroy();
mSkinInflaterFactory.clean();
}
}

public class BaseFragment extends Fragment {
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
return getActivity().getLayoutInflater();
}

}

實現Factory接口

public class SkinInflaterFactory implements Factory {
/**

  • 用一個集合將需要換膚的view,以及view的信息存起來
    */
    private List<SkinItem> mSkinItems = new ArrayList<>();
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
    // 我們自定義了一個屬性,每個view都可以使用
    // 在xml裏使用如下 skin:enable="true" 表示換膚
    // 如果不換膚,直接返回空,這裏返回null,會走原來的xml解析邏輯
    // 源碼裏面分析過了
    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    if (!isSkinEnable) {
    return null;
    }
    View view = createView(context, name, attrs);
    if (view == null) {
    return null;
    }
    // 解析要換膚view的信息
    parseSkinAttr(context, attrs, view);
    return view;
    }
    // 這個方法就是創建view,還是走layoutInflater的createView邏輯
    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) {
    L.e("error while create 【" + name + "】 : " + e.getMessage());
    view = null;
    }
    return view;
    }
    // 代碼中的註釋很詳細了
    // attrName 表示要換膚的屬性 textColor 等
    // id 表示屬性對應的資源id
    // entryName 表示屬性對應的資源名字
    // entryType 表示屬性對應的資源類型 color 還是 drawable 等
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
    List<SkinAttr> viewAttrs = new ArrayList<>();
    for (int i = 0; i < attrs.getAttributeCount(); i++) {
    /**
    • get 出來的是這樣的東西:
    • attrName = divider, attrValue = @2131099656
    • attrName = textColor, attrValue = @2131099660
    • attrName = background, attrValue = @2131099658
      */
      String attrName = attrs.getAttributeName(i);
      String attrValue = attrs.getAttributeValue(i);
      if (!AttrFactory.isSupportedAttr(attrName)) {
      continue;
      }
      // xml 編譯之後 attrValue 值是 @ + 數值的形式
      if (attrValue.startsWith("@")) {
      try {
      int id = Integer.parseInt(attrValue.substring(1));
      /**
      • get 出來的是這樣的:
      • entryName typeName
      • news_item_text_color_selector color
      • news_item_selector drawable
        */
        String entryName = context.getResources().getResourceEntryName(id);
        String typeName = context.getResources().getResourceTypeName(id);
        SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
        if (mSkinAttr != null) {
        viewAttrs.add(mSkinAttr);
        }
        } catch (NumberFormatException | NotFoundException e) {
        e.printStackTrace();
        }
        }
        }
        if (!ListUtils.isEmpty(viewAttrs)) {
        SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;
        mSkinItems.add(skinItem);
        if (SkinManager.getInstance().isExternalSkin()) {
        skinItem.apply();
        }
        }
        }
        public void applySkin() {
        if (ListUtils.isEmpty(mSkinItems)) {
        return;
        }
        for (SkinItem si : mSkinItems) {
        if (si.view == null) {
        continue;
        }
        si.apply();
        }
        }
        public void addSkinView(SkinItem item) {
        mSkinItems.add(item);
        }
        public void clean() {
        if (ListUtils.isEmpty(mSkinItems)) {
        return;
        }
        for (SkinItem si : mSkinItems) {
        if (si.view == null) {
        continue;
        }
        si.clean();
        }
        }
        }
        收集了所有的換膚信息,我們就要討論第二個問題了。

加載皮膚中的資源

想想我們在開發時,是如何使用資源的:

getResource().getString(id);
getResource().getColor(id);
getResource().getDrawable(id);
看看getString源碼:

public String getString(@StringRes int id) throws NotFoundException {
final CharSequence res = getText(id);
if (res != null) {
return res.toString();
}
throw new NotFoundException("String resource ID #0x"

  • Integer.toHexString(id));
    }
    繼續 getText(id):

public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"

  • Integer.toHexString(id));
    }
    可以看到資源實際上是由 mAssets 加載的。查看 getColor 和 getDrawable 源碼同樣如此。

從這裏就要開始往上追溯了,因為我們需要知道 AssetManager 是如何創建的,它是怎麽對應的app的資源。

再次回到 ContextImpl,查看 getResource 方法:

@Override
public Resources getResources() {
return mResources;
}
直接返回成員變量,看看在哪裏賦值的:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {

    ...... 

    mResourcesManager = ResourcesManager.getInstance(); 
    ...... 
    Resources resources = packageInfo.getResources(mainThread); 
    if (resources != null) { 
        if (displayId != Display.DEFAULT_DISPLAY 
                || overrideConfiguration != null 
                || (compatInfo != null && compatInfo.applicationScale 
                        != resources.getCompatibilityInfo().applicationScale)) { 
            resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(), 
                    packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), 
                    packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, 
                    overrideConfiguration, compatInfo); 
        } 
    } 
    mResources = resources; 
    ...... 
}

Resource 由 ResourceManager 的 getTopLevelResources 方法創建:

Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {

  ...... 

  Resources r; 
  synchronized (this) { 
      // Resources is app scale dependent. 
      if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale); 
      WeakReference<Resources> wr = mActiveResources.get(key); 
      r = wr != null ? wr.get() : null; 
      //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate()); 
      if (r != null && r.getAssets().isUpToDate()) { 
          if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir 
                  + ": appScale=" + r.getCompatibilityInfo().applicationScale 
                  + " key=" + key + " overrideConfig=" + overrideConfiguration); 
          return r; 
      } 
  } 
  AssetManager assets = new AssetManager(); 
  // resDir can be null if the ‘android‘ package is creating a new Resources object. 
  // This is fine, since each AssetManager automatically loads the ‘android‘ package 
  // already. 
  if (resDir != null) { 
      if (assets.addAssetPath(resDir) == 0) { 
          return null; 
      } 
  } 
  if (splitResDirs != null) { 
      for (String splitResDir : splitResDirs) { 
          if (assets.addAssetPath(splitResDir) == 0) { 
              return null; 
          } 
      } 
  } 
  if (overlayDirs != null) { 
      for (String idmapPath : overlayDirs) { 
          assets.addOverlayPath(idmapPath); 
      } 
  } 
  if (libDirs != null) { 
      for (String libDir : libDirs) { 
          if (libDir.endsWith(".apk")) { 
              // Avoid opening files we know do not have resources, 
              // like code-only .jar files. 
              if (assets.addAssetPath(libDir) == 0) { 
                  Log.w(TAG, "Asset path ‘" + libDir + 
                          "‘ does not exist or contains no resources."); 
              } 
          } 
      } 
  } 
  ...... 

  r = new Resources(assets, dm, config, compatInfo); 
  if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " 
          + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); 
  synchronized (this) { 
      WeakReference<Resources> wr = mActiveResources.get(key); 
      Resources existing = wr != null ? wr.get() : null; 
      if (existing != null && existing.getAssets().isUpToDate()) { 
          // Someone else already created the resources while we were 
          // unlocked; go ahead and use theirs. 
          r.getAssets().close(); 
          return existing; 
      } 
      // XXX need to remove entries when weak references go away 
      mActiveResources.put(key, new WeakReference<>(r)); 
      if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size()); 
      return r; 
  } 

}
直接new AssetManager 並且調用 addAssetPath 方法將資源路傳進去。如果有 lib (.apk),也添加進去。看到這裏就很清楚了,我們要去加載的皮膚包可以是一個 apk 文件。所以讀取皮膚包資源的思路就清晰了,我們可以新建一個工程,裏面只放皮膚資源,最後打包成apk,我們的app拿到這個apk就可以加載出裏面的資源。

創建資源包的 Resource

我們自己的apk只能記載自己應用下的資源目錄,要想去加載別的資源目錄,我們就可以創建一個 Resource 對象,替換裏面的 AssetManager,讓 AssetManager 裏面的 path 對應為資源包(.apk)的路徑。

具體代碼如下:

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.exists()) {
return null;
}
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
// 創建資源包的 assetManager
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;
if (mResources != null) {
if (callback != null) callback.onSuccess();
notifySkinUpdate();
} else {
isDefaultSkin = true;
if (callback != null) callback.onFailed();
}
}
}.execute(skinPackagePath);
}
有了 Resource 之後,就可以加載皮膚資源了,下面是一段加載不同皮膚下的drawable代碼。

public Drawable getDrawable(int resId) {
Drawable originDrawable = context.getResources().getDrawable(resId);
if (mResources == null || isDefaultSkin) {
return originDrawable;
}
// 拿到id對應的資源名字
String resName = context.getResources().getResourceEntryName(resId);
// 根據資源名字找到皮膚包中的id
int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
Drawable trueDrawable = null;
try {
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
trueDrawable = mResources.getDrawable(trueResId);
} else {
trueDrawable = mResources.getDrawable(trueResId, null);
}
} catch (NotFoundException e) {
e.printStackTrace();
trueDrawable = originDrawable;
}
return trueDrawable;
}
這樣換膚就實現了。

10老司機的Android換膚筆記