Android换肤笔记

Android换肤笔记


今日科技快讯

近日,江苏南京的颜女士先后买了两部iPhone X手机,却均能被一位非亲属关系的女同事面部解锁。她担心手机的安全问题。苹果店工作人员表示这是个案,如果对产品不满意可选择退货。

作者简介

本篇文章来自 aprz512 的投稿。主要介绍了Android换肤功能实现的原理,以及提供了一个换肤的框架,希望对大家有所帮助!

aprz512 的博客地址:

http://aprz512.github.io/

效果预览

文章开始,我们先来预览一下换肤功能所能实现的效果吧。如下图所示:

Android换肤笔记

换肤原理

本文所讲述的换肤是通过干预 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 = http://www.gunmi.cn/v/@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;  }

这样换肤就实现了。

github地址如下:

http://github.com/aprz512/Android-Skin-Loader

欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

Android换肤笔记

Android换肤笔记