Android-skin-support 换肤框架使用

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

换肤效果.png

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-support
implementation '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 应用失败时回调
 */
@JvmOverloads
fun 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")
    }
)

打包皮肤包

打包皮肤包,库文档并没有细说,但其实很简单,新建一个Application的Module模块(可运行模块),在res资源目录下放置同名资源,打包为apk包,放到宿主的assets文件夹下的skins文件夹下。

主题可以有很多,我们不必每个皮肤包都新建一个Module,我们只需要使用Gradle做多渠道打包即可。

注意,皮肤包的包名不能和宿主包名一致,所以使用applicationIdSuffix,给每个皮肤包都加一个后缀即可。

  • 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>
多渠道打包皮肤包.png
  • 打包皮肤包apk,将apk重命名,注意将apk后缀改为skin后缀,然后将皮肤包放置到宿主的assets文件夹下的skins文件夹下。
放置皮肤包到宿主assets文件夹下.png

自定义控件支持换肤

换肤框架,提供了v7、design库的控件的换肤版本,但是我们自己的自定义控件则自己去适配了,下面我给出自定义顶部栏、TabLayout的换肤适配。(还有其他适配方案,可以看官方Github文档),基本步骤如下:

  1. 继承需要换肤的控件,实现SkinCompatSupportable接口
  2. 在控件的构造方法中,获取需要换肤的自定义属性,获取当前应用的皮肤资源,进行换肤(回显之前的换肤设置)
  3. 在SkinCompatSupportable接口的applySkin回调中,再处理应用运行中换肤的处理(启动时不会回调,手动切换皮肤时回调)。
  • 自定义顶部栏,其实使用的是QMUI的TopBar,有兴趣的小伙伴可以去看下。

  • 自定义属性

<!--************ TopBar自定义属性 ***********-->
<declare-styleable name="TopBar">
    <!-- 省略其他自定义属性... -->
    
    <attr name="topbar_bg_color" format="color"/>
    
    <!-- 省略其他自定义属性... -->
</declare-styleable>
  • 换肤兼容
//定义支持换肤控件的TopBar
public 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);
        }
    }

    @Override
    public 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();
    }

    @Override
    public void applySkin() {
        super.applySkin();
        //步骤三:应用运行间,手动切换换肤回调,再次进行换肤操作
        if (mSkinCompatBackgroundHelper != null) {
            mSkinCompatBackgroundHelper.applySkin();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349