Android换肤原理和Android-Skin-Loader框架解析
换肤类型
1.白天/黑夜主题切换 一般通过本地theme来做。
使用相同的资源id,但在不同的Theme下自定义不同的资源,通过主动切换不同的Theme从而切换界面元素创建时使用的资源,这种方案适合代码量不多的情况。缺点是对于已创建界面的皮肤,必须重新加载界面元素。
2.多种主题切换 一般作为线上服务,皮肤资源在下载后进行使用。
方案
1.拦截系统创建View的过程,交由自己来创建
2.收集需要换肤的View
如何辨别那个View需要换肤?
可以用自定义view属性来标记,需要的话,将其保存起来
3.加载外部资源包,进行换肤
加载资源包
换肤相应的API
Resources提供了可以通过@+id、Type、PackageName这三个参数就可以在AssetManager中寻找相应的PackageName中有没有Type类型并且id值都能与参数对应上的id,进行返回,然后通过这个id再调用Resource的获取资源的api就可以得到相应的资源。
这里我们需要注意的一点是getIdentifier(String name,String defType,String defPackage)方法和getString(int id)方法所调用Resources对象的mAssets对象必须是同一个,并且包含有PackageName这个资源包。
AssetManager构造
AssetManager的构造函数来看有{@hide}的注解,所以在其他类里面是直接创建AssetManager实例,但不要忘记Java中还有反射机制可以创建类对象。
AssetManager assetManager = AssetManager.class.newInstance();
如何让创建的assetManager包含特定的PackageName的资源信息
需要使用@hide注解的addAssetPath()方法
只能通过反射的方法来调用
String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";
AssetManager assetManager = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath,String.class).invoke(assetManager,apkPath);
} catch(Throwable th) {
th.printStackTrace();
}
换肤Resources构造
public Resources getSkinResources(Context context){
/**
* 插件apk路径
*/
String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk"; AssetManager assetManager = null;
try {
AssetManager assetManager =AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
} catch (Throwable th) {
th.printStackTrace();
}
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); }
使用资源包中的资源换肤
public Resources getSkinResources(Context context){
/**
* 插件apk路径
*/
String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";
AssetManager assetManager = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
} catch (Throwable th) {
th.printStackTrace();
}
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView imageView = (ImageView) findViewById(R.id.imageView);
TextView textView = (TextView) findViewById(R.id.text);
/**
* 插件资源对象
*/
Resources resources = getSkinResources(this);
/**
* 获取图片资源
*/
Drawable drawable = resources.getDrawable(resources.getIdentifier("night_icon", "drawable","com.tzx.skin"));
/**
* 获取Color资源
*/
int color = resources.getColor(resources.getIdentifier("night_color","color","com.tzx.skin"));
imageView.setImageDrawable(drawable); textView.setText(text);
}
LayoutInflater.Factory
Android给我们在View生产的时候做修改提供了法门。
public abstract class LayoutInflater {
/***部分代码省略****/
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
/***部分代码省略****/ }
我们可以给当前页面的Windos对象在创建的时候设置Factory,那么在Window中的View进行创建时候就会通过自己设置的Factory进行创建。
Android-Skin-Loader解析
初始化
初始化换肤框架,导入需要换肤的资源包(当前为一个apk文件,其中只有资源文件)。
public class SkinApplication extends Application {
public void onCreate() {
super.onCreate();
initSkinLoader();
}
/*Must call init first*/
private void initSkinLoader() {
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}
黑夜模式的简单流程
styles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="clockBackground">@android:color/white</item>
<item name="clockTextColor">@android:color/black</item>
</style>
attrs.xml
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<attr name="clockBackground" format="color" />
<attr name="clockTextColor" format="color" />
</resources>
TextView里的android:textColor="?attr/clockTextColor"是让字体颜色跟随所设置的Theme.
setTheme(R.style.NightTheme);
TypedValue background = new TypedValue();
TypedValue textColor = new TypedValue();
Resources.Theme theme = getTheme();
theme.resolveAttribute(R.attr.clockBackground,background,true);
theme.resolveAttribute(R.attr.clockTextColor,textColor,true);
mHeaderLayout.setBackgroundResource(background.resourceId);
获取截屏的相关操作
View view = getWindow.getDecorView();
view.setDrawingCacheEnable(true);
final Bitmap drawingCache = view.getDrawingCahe();
将截图显示上去,演示动画
final View view = new View(this);
view.setBackground(new BitmapDrawable(getResources(),cacheBitmap));
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
((ViewGroup) decorView).addView(view,layoutParams);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view,"alpha",1f,0f);
objectAnimator.setDuration(300);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
((ViewGroup) decorView).removeView(view);
}
});
objectAnimator.start();
让RecyclerView缓存的Item失效
/*
* 让RecylerView 缓存在Pool中的Item失效
* 那么,如果是ListView,要怎么做呢?这里的思路是通过反射拿到AbsListView类中的RecycleBin对象
* 然后同样再用反射去调用clear方法
* */
Class<RecyclerView> recyclerViewClass = RecyclerView.class;
try {
Field declaredField = recyclerViewClass.getDeclaredField("mRecycler");
declaredField.setAccessible(true);
Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear",(Class<?>[]) new Class[0]);
declaredMethod.setAccessible(true);
declaredMethod.invoke(declaredField.get(mRecyclerView),new Object[0]);
RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool();
recycledViewPool.clear();
} catch (Exception e) {
e.printStackTrace();
}
代码的分析
skintest
MyApplication
onCreate()方法中调用,super.onCreate()语句之后
SkinManager.get().init(this);
get()方法,用于创建SkinManager对象
@MainThread
public void init(Context context) {
//使用传进来的应用程序的上下文对象
mContext = context.getApplicationContext();
mSkinResourceManager = new SkinResourceManagerImpl(mContext, null, null);
/*
*public SkinResourceManagerImpl(Context context, String pkgName, Resources resources) {
mDefaultResources = context.getResources();
mSkinPluginPackageName = pkgName;
mSkinPluginResources = resources;
}
*/
//加载已经用户默认设置的皮肤资源
load();
/*
String skinApkPath = SkinConfig.getSkinPath();
此处返回的是String类型的mSaveSkinFilePath = "/storage/emulated/0/Android/skins/default_skin.txt";
if (TextUtils.isEmpty(skinApkPath)) {
restoreToDefaultSkin();
/*
mSkinResourceManager.setPluginResourcesAndPkgName(null, null);
notifySkinChanged();//更换皮肤时,通知view更换资源
*/
} else {
loadNewSkin(skinApkPath);//加载新皮肤
}
*/
}