Android-skin-support 換膚框架使用

前段時(shí)間給App接入了換膚功能,使用到了Android-skin-support這個(gè)換膚框架,所以寫這篇文章記錄一下。

換膚效果.png

Android-skin-support集成

  • 依舊使用v7的support,使用以下依賴
implementation 'skin.support:skin-support:3.1.4'                   // skin-support 基礎(chǔ)控件支持
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對(duì)比,就是版本號(hào)升級(jí)到了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 基礎(chǔ)控件支持
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,換膚時(shí),才會(huì)遍歷View樹上的控件進(jìn)行換膚。
public class BaseActivity extends SkinCompatActivity {
}

Android-skin-support初始化以及簡單封裝使用

  • 我對(duì)換膚框架初始化和換膚方法封裝了一下,提供了一個(gè)Api接口以及一個(gè)實(shí)現(xiàn)類
//Api接口
interface SkinApi {
    /**
     * 初始化
     */
    fun init(application: Application)

    /**
     * 從Assets中加載皮膚apk
     * @param targetSkinName 目標(biāo)皮膚名稱
     * @param startBlock 開始換膚時(shí)回調(diào)
     * @param successBlock 換膚成功時(shí)回調(diào)
     * @param failedBlock 換膚失敗時(shí)回調(diào)
     */
    fun loadSkinFromAssets(
        targetSkinName: String,
        startBlock: ((preSkinName: String) -> Unit)? = null,
        successBlock: ((preSkinName: String, applySkinName: String) -> Unit)? = null,
        failedBlock: ((errMsg: String) -> Unit)? = null
    )
}

//實(shí)現(xiàn)類
object SkinProxy : SkinApi {
    override fun init(application: Application) {
        SkinMaterialManager.init(application)
        //基礎(chǔ)控件換膚初始化
        SkinCompatManager.withoutActivity(application)
            //material design 控件換膚初始化[可選]
            .addInflater(SkinMaterialViewInflater())
            //ConstraintLayout 控件換膚初始化[可選]
            .addInflater(SkinConstraintViewInflater())
            //CardView v7 控件換膚初始化[可選]
            .addInflater(SkinCardViewInflater())
            //關(guān)閉狀態(tài)欄換膚,默認(rèn)打開[可選]
            //.setSkinStatusBarColorEnable(false)
            //關(guān)閉windowBackground換膚,默認(rèn)打開[可選]
            //.setSkinWindowBackgroundEnable(false)
            .loadSkinFromAssets(
                SkinStorage.getApplyAppSkinName()
            )
    }

    override fun loadSkinFromAssets(
        targetSkinName: String,
        startBlock: ((preSkinName: String) -> Unit)?,
        successBlock: ((preSkinName: String, applySkinName: String) -> Unit)?,
        failedBlock: ((errMsg: String) -> Unit)?
    ) {
        //獲取應(yīng)用前的皮膚名稱,如果重復(fù)應(yīng)用,不繼續(xù)
        if (SkinStorage.getApplyAppSkinName() == targetSkinName) {
            return
        }
        SkinCompatManager.getInstance()
            .loadSkinFromAssets(targetSkinName, startBlock, { preSkinName, newApplySkinName ->
                //保存記錄到本地
                SkinStorage.saveApplySkinName(newApplySkinName)
                successBlock?.invoke(preSkinName, newApplySkinName)
            }, failedBlock)
    }
}
  • loadSkinFromAssets()方法,是從assets文件夾中加載皮膚包的方法,是我對(duì)庫中SkinCompatManager添加的拓展方法,目的是添加換膚開始、成功、失敗的回調(diào)。以及提供換膚前應(yīng)用的皮膚名稱。
/**
 * 插件化方式加載皮膚:從Assets文件夾中加載
 * @param targetSkinName 本次要應(yīng)用的皮膚
 * @param startBlock 開始應(yīng)用時(shí)回調(diào)
 * @param successBlock 應(yīng)用成功時(shí)回調(diào)
 * @param failedBlock 應(yīng)用失敗時(shí)回調(diào)
 */
@JvmOverloads
fun SkinCompatManager.loadSkinFromAssets(
    targetSkinName: String,
    startBlock: ((preSkinName: String) -> Unit)? = null,
    successBlock: ((preSkinName: String, newApplySkinName: String) -> Unit)? = null,
    failedBlock: ((errMsg: String) -> Unit)? = null
) {
    //從sp中,獲取應(yīng)用前的皮膚名稱
    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中調(diào)用初始化換膚框架
SkinProxy.init(it.applicationContext as Application)
  • 切換皮膚,皮膚包打包見下面的 打包皮膚包
//目標(biāo)皮膚包名稱
val targetSkin = "purple.skin";
//開始切換皮膚
SkinProxy.loadSkinFromAssets(
    targetSkin, { preSkinName ->
        logd("開始換膚,當(dāng)前應(yīng)用的皮膚為: $preSkinName")
    }, { preSkinName, newApplySkinName ->
        logd("換膚成功,舊皮膚為: ${preSkinName},新皮膚為: $newApplySkinName")
    }, { errMsg ->
        logd("換膚失敗,errMsg: $errMsg")
    }
)

打包皮膚包

打包皮膚包,庫文檔并沒有細(xì)說,但其實(shí)很簡單,新建一個(gè)Application的Module模塊(可運(yùn)行模塊),在res資源目錄下放置同名資源,打包為apk包,放到宿主的assets文件夾下的skins文件夾下。

主題可以有很多,我們不必每個(gè)皮膚包都新建一個(gè)Module,我們只需要使用Gradle做多渠道打包即可。

注意,皮膚包的包名不能和宿主包名一致,所以使用applicationIdSuffix,給每個(gè)皮膚包都加一個(gè)后綴即可。

  • build.gradle文件配置多渠道打包
flavorDimensions "default"
//多種皮膚包,多渠道打包配置
productFlavors {
    //原始顏色
    "default" {
        applicationIdSuffix ".default"
    }
    //紫色
    purple {
        applicationIdSuffix ".purple"
    }
    //藍(lán)色
    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. 繼承需要換膚的控件,實(shí)現(xiàn)SkinCompatSupportable接口
  2. 在控件的構(gòu)造方法中,獲取需要換膚的自定義屬性,獲取當(dāng)前應(yīng)用的皮膚資源,進(jìn)行換膚(回顯之前的換膚設(shè)置)
  3. 在SkinCompatSupportable接口的applySkin回調(diào)中,再處理應(yīng)用運(yùn)行中換膚的處理(啟動(dòng)時(shí)不會(huì)回調(diào),手動(dòng)切換皮膚時(shí)回調(diào))。
  • 自定義頂部欄,其實(shí)使用的是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。應(yīng)用之前使用的換膚(回顯)
        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、應(yīng)用運(yùn)行間,手動(dòng)切換換膚回調(diào),再次進(jìn)行換膚操作
        applyTopBarBackgroundColor();
    }
}
  • TabLayout,自定義SkinTabLayout繼承于SkinMaterialTabLayout,而SkinMaterialTabLayout繼承design包的TabLayout,但是換膚庫中的SkinMaterialTabLayout并沒有處理TabLayout的背景換膚處理,不太明白為什么其他屬性都處理了,這個(gè)那么常用的屬性不處理。
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);
        //步驟一:庫里其實(shí)提供了android:backgroundColor背景顏色屬性處理SkinCompatBackgroundHelper類,我們讓它去對(duì)TabLayout進(jìn)行背景顏色換膚即可,不需要自己寫
        mSkinCompatBackgroundHelper = new SkinCompatBackgroundHelper(this);
        mSkinCompatBackgroundHelper.loadFromAttributes(attrs, defStyleAttr);
        //步驟二:馬上處理換膚
        mSkinCompatBackgroundHelper.applySkin();
    }

    @Override
    public void applySkin() {
        super.applySkin();
        //步驟三:應(yīng)用運(yùn)行間,手動(dòng)切換換膚回調(diào),再次進(jìn)行換膚操作
        if (mSkinCompatBackgroundHelper != null) {
            mSkinCompatBackgroundHelper.applySkin();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容