效果预览
文章开始,我们先来预览一下换肤功能所能实现的效果吧。如下图所示:
换肤原理
本文所讲述的换肤是通过干预 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;
}
这样换肤就实现了。