问题分析
换皮功能有两种,一种是白天黑夜模式,一种是多套皮肤的替换,也就是以插件形式实现资源的获取,白天黑夜的模式是通过更换一个主题(Theme),这里不做扩展,而多套皮肤有两种实现方式,一种是本地存放多套不同样式的图片切换(不太实际),一种则是通过获取一个apk中的资源去做到,本文讨论的就是这种方式,实现之前有以下几个问题解决
1.系统怎么实现图片替换
2.怎么获取apk中资源
3.获取之后怎么设置到app中实现换肤
解决问题
系统实现图片替换
ImageView的src设置,怎么实现?
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
从ImageView的构造函数进来,看到以下源码
initImageView();
// ImageView is not important by default, unless app developer overrode attribute.
if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
}
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ImageView,
attrs, a, defStyleAttr, defStyleRes);
//这个地方重点,点进去看方法实现
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
从getDrawable进去,可以清楚看到 mResources.loadDrawable的方法
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
if (density > 0) {
// If the density is overridden, the value in the TypedArray will not reflect this.
// Do a separate lookup of the resourceId with the density override.
mResources.getValueForDensity(value.resourceId, density, value, true);
}
//资源获取,重点
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
既然资源加载是通过Resources调用loadDrawable去读取,那我们能否自己实例化一个去实现?
获取apk中的资源
由源码可知,Resource创建有两种
1.无参
/**
* Only for creating the System resources.
*/
@UnsupportedAppUsage
private Resources() {
this(null);
final DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
final Configuration config = new Configuration();
config.setToDefaults();
mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
new DisplayAdjustments());
}
2.带参
/**
* Create a new Resources object on top of an existing set of assets in an
* AssetManager.
*
* @deprecated Resources should not be constructed by apps.
* See {@link android.content.Context#createConfigurationContext(Configuration)}.
*
* @param assets Previously created AssetManager.
* @param metrics Current display metrics to consider when
* selecting/computing resource values.
* @param config Desired device configuration to consider when
* selecting/computing resource values (optional).
*/
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
而我们需要实现带参的Resource,metrics以及config用系统的即可,再看看AssetManager 源码怎么实例化
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
final int userApkAssetCount = mUserApkAssets.size();
for (int i = 0; i < userApkAssetCount; i++) {
apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
}
// Calling this constructor prevents creation of system ApkAssets, which we took care
// of in this Builder.
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
//重点,设置资源路劲,也就是apk地址
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
return assetManager;
}
因此,我们完全可以自己构造一个Resource
app中实现换肤
既然参数可以自己实例化,那换肤可以通过以下方法实现
int drawableId = resource.getIdentifier("图片名","drawable","包名");
Drawable drawable = resource.getDrawable(drawableId);
mImageIv.setImageDrawable(drawable);
总结
所有的资源通过Resource类加载,构建的时候直接new,需要传入参数为AssetManager,最终通过AssetManager实例化
try {
// 读取本地的一个 .skin里面的资源
Resources superRes = getResources();
// 创建AssetManager
AssetManager asset = AssetManager.class.newInstance();
// 添加本地下载好的资源皮肤,反射实现
Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
// method.setAccessible(true); 如果是私有的
// 反射执行方法
method.invoke(asset, Environment.getExternalStorageDirectory().getAbsolutePath()+
File.separator + "red.skin");
Resources resource = new Resources(asset,superRes.getDisplayMetrics(),
superRes.getConfiguration());
// 获取资源 id
int drawableId = resource.getIdentifier("图片名","drawable","包名");
Drawable drawable = resource.getDrawable(drawableId);
mImageIv.setImageDrawable(drawable);
} catch (Exception e) {
e.printStackTrace();
}