Android换肤框架Android-skin-support的使用
前段时间给App接入了换肤功能,使用到了Android-skin-support这个换肤框架,所以写这篇文章记录一下。

Android-skin-support集成
依旧使用v7的support,使用以下依赖
implementation 'skin.support:skin-support:3.1.4' // skin-support 基础控件支持implementation 'skin.support:skin-support-design:3.1.4' // skin-support-design material design 控件支持[可选]implementation 'skin.support:skin-support-cardview:3.1.4' // skin-support-cardview CardView 控件支持[可选]implementation 'skin.support:skin-support-constraint-layout:3.1.4' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]
已迁移到AndroidX,则使用以下依赖(注:和上面的support对比,就是版本号升级到了4.04,support是3.1.4喔)
implementation 'skin.support:skin-support:4.0.4' // skin-supportimplementation 'skin.support:skin-support-appcompat:4.0.4' // skin-support 基础控件支持implementation 'skin.support:skin-support-design:4.0.4' // skin-support-design material design 控件支持[可选]implementation 'skin.support:skin-support-cardview:4.0.4' // skin-support-cardview CardView 控件支持[可选]implementation 'skin.support:skin-support-constraint-layout:4.0.4' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]
Activity基类继承SkinCompatActivity。继承的确不太友好,只有继承了SkinCompatActivity,换肤时,才会遍历View树上的控件进行换肤。
public class BaseActivity extends SkinCompatActivity {}
Android-skin-support初始化以及简单封装使用
我对换肤框架初始化和换肤方法封装了一下,提供了一个Api接口以及一个实现类
//Api接口interface SkinApi {/*** 初始化*/fun init(application: Application)/*** 从Assets中加载皮肤apk* @param targetSkinName 目标皮肤名称* @param startBlock 开始换肤时回调* @param successBlock 换肤成功时回调* @param failedBlock 换肤失败时回调*/fun loadSkinFromAssets(targetSkinName: String,startBlock: ((preSkinName: String) -> Unit)? = null,successBlock: ((preSkinName: String, applySkinName: String) -> Unit)? = null,failedBlock: ((errMsg: String) -> Unit)? = null)}//实现类object SkinProxy : SkinApi {override fun init(application: Application) {SkinMaterialManager.init(application)//基础控件换肤初始化SkinCompatManager.withoutActivity(application)//material design 控件换肤初始化[可选].addInflater(SkinMaterialViewInflater())//ConstraintLayout 控件换肤初始化[可选].addInflater(SkinConstraintViewInflater())//CardView v7 控件换肤初始化[可选].addInflater(SkinCardViewInflater())//关闭状态栏换肤,默认打开[可选]//.setSkinStatusBarColorEnable(false)//关闭windowBackground换肤,默认打开[可选]//.setSkinWindowBackgroundEnable(false).loadSkinFromAssets(SkinStorage.getApplyAppSkinName())}override fun loadSkinFromAssets(targetSkinName: String,startBlock: ((preSkinName: String) -> Unit)?,successBlock: ((preSkinName: String, applySkinName: String) -> Unit)?,failedBlock: ((errMsg: String) -> Unit)?) {//获取应用前的皮肤名称,如果重复应用,不继续if (SkinStorage.getApplyAppSkinName() == targetSkinName) {return}SkinCompatManager.getInstance().loadSkinFromAssets(targetSkinName, startBlock, { preSkinName, newApplySkinName ->//保存记录到本地SkinStorage.saveApplySkinName(newApplySkinName)successBlock?.invoke(preSkinName, newApplySkinName)}, failedBlock)}}
loadSkinFromAssets()方法,是从assets文件夹中加载皮肤包的方法,是我对库中SkinCompatManager添加的拓展方法,目的是添加换肤开始、成功、失败的回调。以及提供换肤前应用的皮肤名称。
/*** 插件化方式加载皮肤:从Assets文件夹中加载* @param targetSkinName 本次要应用的皮肤* @param startBlock 开始应用时回调* @param successBlock 应用成功时回调* @param failedBlock 应用失败时回调*/@JvmOverloadsfun SkinCompatManager.loadSkinFromAssets(targetSkinName: String,startBlock: ((preSkinName: String) -> Unit)? = null,successBlock: ((preSkinName: String, newApplySkinName: String) -> Unit)? = null,failedBlock: ((errMsg: String) -> Unit)? = null) {//从sp中,获取应用前的皮肤名称val preSkinName = SkinStorage.getApplyAppSkinName()loadSkin(targetSkinName,object : SkinCompatManager.SkinLoaderListener {override fun onStart() {startBlock?.invoke(preSkinName)}override fun onSuccess() {successBlock?.invoke(preSkinName, targetSkinName)}override fun onFailed(errMsg: String?) {failedBlock?.invoke(errMsg ?: "")}},SkinCompatManager.SKIN_LOADER_STRATEGY_ASSETS)}
在Application中调用初始化换肤框架
SkinProxy.init(it.applicationContext as Application)切换皮肤,皮肤包打包见下面的 打包皮肤包
//目标皮肤包名称val targetSkin = "purple.skin";//开始切换皮肤SkinProxy.loadSkinFromAssets(targetSkin, { preSkinName ->logd("开始换肤,当前应用的皮肤为: $preSkinName")}, { preSkinName, newApplySkinName ->logd("换肤成功,旧皮肤为: ${preSkinName},新皮肤为: $newApplySkinName")}, { errMsg ->logd("换肤失败,errMsg: $errMsg")})
打包皮肤包
build.gradle文件配置多渠道打包
flavorDimensions "default"//多种皮肤包,多渠道打包配置productFlavors {//原始颜色"default" {applicationIdSuffix ".default"}//紫色purple {applicationIdSuffix ".purple"}//蓝色blue {applicationIdSuffix ".blue"}}
例如:我的需求只有将主题色替换掉即可,所以在不同的多渠道文件夹下,放置不同主题的colors.xml文件
<?xml version="1.0" encoding="utf-8"?><resources><color name="base_color_primary">#673AB7</color><color name="base_color_primary2">#7C4DFF</color><color name="base_color_primary_dark">#512DA8</color><color name="base_color_accent">#7C4DFF</color></resources>

打包皮肤包apk,将apk重命名,注意将apk后缀改为skin后缀,然后将皮肤包放置到宿主的assets文件夹下的skins文件夹下。

自定义控件支持换肤
继承需要换肤的控件,实现SkinCompatSupportable接口
在控件的构造方法中,获取需要换肤的自定义属性,获取当前应用的皮肤资源,进行换肤(回显之前的换肤设置)
在SkinCompatSupportable接口的applySkin回调中,再处理应用运行中换肤的处理(启动时不会回调,手动切换皮肤时回调)。
自定义顶部栏,其实使用的是QMUI的TopBar,有兴趣的小伙伴可以去看下。
自定义属性
<!--************ TopBar自定义属性 ***********--><declare-styleable name="TopBar"><!-- 省略其他自定义属性... --><attr name="topbar_bg_color" format="color"/><!-- 省略其他自定义属性... --></declare-styleable>
换肤兼容
//定义支持换肤控件的TopBarpublic class SkinCompatTopBar extends TopBar implements SkinCompatSupportable {private int mTopBarBgColorResId;public SkinCompatTopBar(Context context, AttributeSet attrs) {super(context, attrs);//步骤一:获取需要支持换肤的自定义属性,例如这里顶部栏的背景颜色TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TopBar, 0, 0);mTopBarBgColorResId = array.getResourceId(R.styleable.TopBar_topbar_bg_color, SkinCompatHelper.INVALID_ID);array.recycle();//2。应用之前使用的换肤(回显)applyTopBarBackgroundColor();}//换肤处理private void applyTopBarBackgroundColor() {mTopBarBgColorResId = SkinCompatHelper.checkResourceId(mTopBarBgColorResId);if (mTopBarBgColorResId != SkinCompatHelper.INVALID_ID) {int color = SkinCompatResources.getColor(getContext(), mTopBarBgColorResId);setTopBarBackgroundColor(color);}}@Overridepublic void applySkin() {//3、应用运行间,手动切换换肤回调,再次进行换肤操作applyTopBarBackgroundColor();}}
TabLayout,自定义SkinTabLayout继承于SkinMaterialTabLayout,而SkinMaterialTabLayout继承design包的TabLayout,但是换肤库中的SkinMaterialTabLayout并没有处理TabLayout的背景换肤处理,不太明白为什么其他属性都处理了,这个那么常用的属性不处理。
public class SkinTabLayout extends SkinMaterialTabLayout {private SkinCompatBackgroundHelper mSkinCompatBackgroundHelper;public SkinTabLayout(Context context) {this(context, null);}public SkinTabLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public SkinTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//步骤一:库里其实提供了android:backgroundColor背景颜色属性处理SkinCompatBackgroundHelper类,我们让它去对TabLayout进行背景颜色换肤即可,不需要自己写mSkinCompatBackgroundHelper = new SkinCompatBackgroundHelper(this);mSkinCompatBackgroundHelper.loadFromAttributes(attrs, defStyleAttr);//步骤二:马上处理换肤mSkinCompatBackgroundHelper.applySkin();}@Overridepublic void applySkin() {super.applySkin();//步骤三:应用运行间,手动切换换肤回调,再次进行换肤操作if (mSkinCompatBackgroundHelper != null) {mSkinCompatBackgroundHelper.applySkin();}}}
