文章目录
代码下载:
https://download.youkuaiyun.com/download/hongxue8888/11367790
Github:
https://github.com/345166018/WangyiSkin/tree/master/HxSkin
setContentView源码分析可以查看:
https://blog.youkuaiyun.com/hongxue8888/article/details/95494195
参考:
https://github.com/fengjundev/Android-Skin-Loader
https://github.com/ximsfei/Android-skin-support
https://www.jianshu.com/p/af7c0585dd5b
这里只实现了背景的替换,后续会添加 字体,状态栏换肤,自定义控件,fragment换肤。
换肤模式:
模式 | 案例 | |
---|---|---|
内置换肤 | 在Apk包中存在多种资源(图片、颜色值)用于换肤时候切换。自由度低,apk文件大。 | 一般用于没有其他需求的日间/夜间模式app(高德地图) |
动态换肤 | 通过运行时动态加载皮肤包。 | 网易云音乐 |
如上图所示,换肤主要包括三个步骤:
- 采集需要换肤的控件
- 加载皮肤包
- 控件换肤
先实现一个背景图片的替换,其他功能之后再慢慢完善。
看效果:
1 采集需要换肤的控件
新建MyApplication,在onCreate方法中实现SkinManager初始化操作,代码如下:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
}
}
1.1 SkinManager换肤管理类
public class SkinManager{
private static SkinManager instance;
private Application application;
public static void init(Application application){
synchronized (SkinManager.class) {
if(null == instance){
instance = new SkinManager(application);
}
}
}
public static SkinManager getInstance() {
return instance;
}
private SkinManager(Application application) {
this.application = application;
/**
* 提供了一个应用生命周期回调的注册方法,用来对应用的生命周期进行集中管理,
* 这个接口叫registerActivityLifecycleCallbacks,可以通过它注册
* 自己的ActivityLifeCycleCallback,每一个Activity的生命周期都会回调到这里的对应方法。
*/
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());//3
}
}
1.2 SkinActivityLifecycle
使用ActivityLifecycleCallbacks对应用的生命周期进行集中管理。
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
HashMap<Activity , SkinLayoutFactory> factoryHashMap = new HashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//添加自定义创建View 工厂
SkinLayoutFactory factory = new SkinLayoutFactory(activity);
layoutInflater.setFactory2(factory);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
1.3 SkinLayoutFactory
SkinLayoutFactory 实现了 LayoutInflater.Factory2接口。在SkinLayoutFactory中去采集需要换肤的控件和控件中的属性。
public class SkinLayoutFactory implements LayoutInflater.Factory2{
//控件包名的前缀
private static final String[] mClassPrefixlist = {
"android.widget.",
"android.view.",
"android.webkit."
};
//
private static final Class[] mConstructorSignature =
new Class[]{Context.class, AttributeSet.class};
private static final HashMap<String, Constructor<? extends View>> mConstructor =
new HashMap<String, Constructor<? extends View>>();
//属性处理类
private SkinAttribute skinAttribute;
private Activity activity;
public SkinLayoutFactory(Activity activity) {
this.activity = activity;
skinAttribute = new SkinAttribute();
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 反射 classLoader
View view = createViewFromTag(name, context, attrs);//1
// 自定义View
if(null == view){
view = createView(name, context, attrs);//2
}
//筛选符合属性View
skinAttribute.load(view, attrs);//3
return view;
}
private View createViewFromTag(String name, Context context, AttributeSet attrs) {
//包含自定义控件
if (-1 != name.indexOf(".")) {
return null;
}
//
View view = null;
for (int i = 0; i < mClassPrefixlist.length; i++) {
view = createView(mClassPrefixlist[i] + name, context, attrs);
if(null != view){
break;
}
}
return view;
}
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = mConstructor.get(name);
if (constructor == null) {
try {
//通过全类名获取class
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
//获取构造方法
constructor = aClass.getConstructor(mConstructorSignature);
mConstructor.put(name, constructor);
} catch (Exception e) {
e.printStackTrace();
}
}
if (null != constructor) {
try {
return constructor.newInstance(context, attrs);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
注释1:通过反射获取到系统提供的View
注释2:获取到自定义的View
注释3:调用SkinAttribute的load方法,将获取到的所有View以及View中的属性传递给SkinAttribute类中进行筛选和保存
1.4 SkinAttribute
load方法用于筛选和保存需要换肤的控件和控件中的属性。
public class SkinAttribute {
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
private List<SkinView> skinViews = new ArrayList<>();
public void load(View view, AttributeSet attrs) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获取属性名字
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
//获取属性对应的值
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
//判断前缀字符串 是否是"?"
//attributeValue = "?2130903043"
if (attributeValue.startsWith("?")) { //系统属性值
//字符串的子字符串 从下标 1 位置开始
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//@1234564
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
if (!skinPains.isEmpty()) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin();
skinViews.add(skinView);
}
}
static class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
}
static class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
}
以上就是完整的采集需要换肤控件的过程。所有需要换肤的View和对应的属性都保存在了SkinAttribute的skinViews当中。
2 加载皮肤包并换肤
新建一个Phone Module,在drawable中添加一张换肤背景图,make project后生成apk,将生成的apk复制到 app中的assets中。
如下图所示:
app和和皮肤包app_skin都有一张叫t_window_bg的背景图。
查看app-intermediates-javac下面的R.class 和skin-intermediates-javac下面的R.class ,会发现里面的t_window_bg的值相同,如下:
public static final int t_window_bg = 2131099747;
获取到app中t_window_bg的值,就可以通过这个值去app_skin中查找了。
2.1 下载apk并加载
在MainActivity中模拟apk的下载(这里是复制assets中的apk到本地),并保存apk下载后所在的路径。
public class MainActivity extends AppCompatActivity {
String apkName = "app_skin-debug.apk";
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
Utils.extractAssets(newBase, apkName);
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File extractFile = this.getFileStreamPath(apkName);
String apkPath = extractFile.getAbsolutePath();
//保存apk路径
MyApplication.getApplication().setApkPath(apkPath);
}
/**
* 进入换肤
*/
public void skinSelect(View view) {
startActivity(new Intent(this, SkinActivity.class));
}
}
具体下载的方法如下代码所示:
public class Utils {
/**
* 把Assets里面得文件复制到 /data/data/files 目录下
*/
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
private static void closeSilently(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (Throwable e) {
// ignore
}
}
}
2.2 SkinActivity
SkinActivity为换肤页面,点击换肤按钮进行换肤,点击还原恢复原始状态。
public class SkinActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_skin);
}
public void change(View view) {
String path = MyApplication.getApplication().getApkPath();
SkinManager.getInstance().loadSkin(path);
}
public void restore(View view) {
SkinManager.getInstance().loadSkin(null);
}
}
2.3 SkinManager
又回到SkinManager类中。换肤操作的方法为loadSkin方法。在实现换肤之前,需要获取到换肤包中的资源。要获取换肤包的资源,除了需要包的路径,还需要包对应Resources。
public class SkinManager extends Observable {
private static SkinManager instance;
private Application application;
public static void init(Application application){
synchronized (SkinManager.class) {
if(null == instance){
instance = new SkinManager(application);
}
}
}
public static SkinManager getInstance() {
return instance;
}
private SkinManager(Application application) {
this.application = application;
//共享首选项 用于记录当前使用的皮肤
SkinPreference.init(application);
//资源管理类 用于从app/皮肤 中加载资源
SkinResources.init(application);
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
loadSkin(SkinPreference.getInstance().getSkin());
}
public void loadSkin(String path) {
if(TextUtils.isEmpty(path)){
// 记录使用默认皮肤
SkinPreference.getInstance().setSkin("");
//清空资源管理器, 皮肤资源属性等
SkinResources.getInstance().reset();
} else {
try {
//反射创建AssetManager
AssetManager manager = AssetManager.class.newInstance();
// 资料路径设置 目录或者压缩包
Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(manager, path);
Resources appResources = this.application.getResources();//1
Resources skinResources = new Resources(manager,
appResources.getDisplayMetrics(), appResources.getConfiguration());//2
//记录
SkinPreference.getInstance().setSkin(path);
//获取外部Apk(皮肤薄) 包名
PackageManager packageManager = this.application.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = packageArchiveInfo.packageName;//3
SkinResources.getInstance().applySkin(skinResources,packageName);//4
} catch (Exception e) {
e.printStackTrace();
}
}
//采集的view 皮肤包
setChanged();
//通知观者者
notifyObservers();
}
}
注释1:获取app的Resources
注释2:获取皮肤包的Resources
注释3:获取皮肤包的应用包名
注释4:将皮肤包的Resources和应用包名添加到SkinResources中
SkinResources中的applySkin方法
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用默认皮肤
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
SkinManager是一个被观察者,在保存了皮肤包的Resources和pkgName后,通知观察者(SkinLayoutFactory)去更换皮肤
2.4 SkinLayoutFactory
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
...
@Override
public void update(Observable o, Object arg) {
//更换皮肤
skinAttribute.applySkin();
}
}
2.5 SkinActivityLifecycle
需要在SkinActivityLifecycle中注册和删除观察者
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
HashMap<Activity , SkinLayoutFactory> factoryHashMap = new HashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
/**
* 更新状态栏
*/
SkinThemeUtils.updataStatusBarColor(activity);
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//添加自定义创建View 工厂
SkinLayoutFactory factory = new SkinLayoutFactory(activity);
layoutInflater.setFactory2(factory);
//注册观察者
SkinManager.getInstance().addObserver(factory);
factoryHashMap.put(activity, factory);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
//删除观察者
SkinLayoutFactory remove = factoryHashMap.remove(activity);
SkinManager.getInstance().deleteObserver(remove);
}
}
2.6 SkinAttribute
public class SkinAttribute {
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
private List<SkinView> skinViews = new ArrayList<>();
public void load(View view, AttributeSet attrs) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获取属性名字
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
//获取属性对应的值
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
//判断前缀字符串 是否是"?"
//attributeValue = "?2130903043"
if (attributeValue.startsWith("?")) { //系统属性值
//字符串的子字符串 从下标 1 位置开始
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//@1234564
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
if (!skinPains.isEmpty()) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin();
skinViews.add(skinView);
}
}
static class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
//1
public void applySkin() {
for (SkinPain skinPair : skinPains) {//2
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(
skinPair.resId);
//Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);//3
} else {
ViewCompat.setBackground(view, (Drawable) background);//4
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPair
.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
}
static class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
/**
* 换皮肤
*/
public void applySkin() {
for (SkinView mSkinView : skinViews) {
mSkinView.applySkin();
}
}
}
注释1:内部类SkinView的applySkin为真正执行换肤操作的方法。
注释2:skinPains保存了所有需要换肤的View及对应的属性。
注释3:背景是颜色
注释4:背景是图片
3 总结
- 在SkinManager中注册SkinActivityLifecycle。
- SkinActivityLifecycle中创建SkinLayoutFactory,并将工厂添加到LayoutInflater中。需要注意的是LayoutInflater的setFactory2方法只能使用一次,所以需要先对mFactorySet值进行修改。
- 在SkinLayoutFactory中通过反射获取到所有需要换肤的控件及属性。
- 获取控件的属性具体是在SkinAttribute的load()方法中实现的,并将所有控件和属性保存在skinViews当中。
- 下载皮肤apk到本地。
- SkinActivity中点击按钮调用SkinManager的loadSkin方法,方法中需传入apk路径。
- 反射创建AssetManager,通过AssetManager和app的Resources(appResources)获取到皮肤包的Resources(skinResources)。
- 通过PackageManager获取到皮肤包的packageName。
- 再将skinResources和packageName通过SkinResources的applySkin方法添加到SkinResources中。
- 此时就可以通知SkinLayoutFactory去更换皮肤,具体换肤在SkinAttribute的applySkin()中完成。所有需要换肤的控件和属性都保存在SkinAttribute的skinViews中。循环skinViews,拿到resId(app的资源的值),使用resId去皮肤包中查找对应的资源(查找皮肤资源在SkinResources实现),然后将皮肤包中的资源替换掉app的资源并给对应的属性赋值。