Skinnable
东方证券 Android 动态换肤 Kotlin 实现方案。
Skinnable 是一套轻量级、无侵入、高可用、生产级的 Android 动态换肤框架。支持应用内多主题(内置后缀/前缀)、Assets 插件包换肤、SDCard 本地插件换肤、用户动态调色盘自定义主题及 Jetpack Compose 深度无缝换肤,并全面适配 AppCompat、Material Design 及 ConstraintLayout 控件。
内部内置 SafeSkinResources 三级安全资源代理 与 SkinFallbackManager 事务熔断兜底引擎,保证在大规模商业级应用中绝不因皮肤包坏包、缺失 ID 或 OOM 解码失败而导致崩溃或黑屏。
🌟 核心特性
- 零侵入换肤:只需在 XML 中为控件声明
app:skinnable="true"即可自动完成主题颜色、Drawable、Tint 的实时替换。 - 三级安全代理 (SafeSkinResources):代理 Layer 1 (用户调色盘) -> Layer 2 (外置/内置皮肤包) -> Layer 3 (宿主原生默认资源)。捕获一切外置包解析异常、ID 映射丢失、图片 OOM 解码失败等,严禁向上抛出未捕获异常。
- 事务与熔断机制 (SkinFallbackManager):换肤切换基于
SkinTransaction原子事务,失败自动 Rollback 回退;单会话连续加载失败达阈值后自动触发熔断锁死,恢复默认主题并清除损坏缓存。 - 异常监控与 APM 埋点 (ISkinErrorTracker):抽象
ISkinErrorTracker契约,支持捕获上报MISSING_RESOURCE、OOM_DECODE、CORRUPTED_PACKAGE、TIMEOUT、ROLLBACK_TRIGGERED及CIRCUIT_BREAKER_TRIPPED事件。 - Jetpack Compose 深度换肤 (
SkinTheme):- Design Tokens 体系:基于
@Stable的AppColors和@Immutable的AppTypography/AppShapes令牌。 - 增量局部重组:通过
collectSkinStateAsState()订阅换肤状态流,配合skinColorResource与skinPainterResource实现局部高性能重组。 - Material 3 自动映射:自动将
AppColors同步桥接至 Compose Material 3MaterialTheme(darkColorScheme/lightColorScheme)。
- Design Tokens 体系:基于
- 丰富的控件支持:
- SkinSupportCompat:支持原生 View、
TextView、ImageView、Button、EditText、SeekBar、RatingBar、Spinner、AutoCompleteTextView、ProgressBar、Toolbar以及SkinLottieAnimationView(Lottie 矢量动画换肤) 等。 - SkinSupportDesign:支持
MaterialCardView、FloatingActionButton、TabLayout、AppBarLayout、BottomNavigationView、TextInputLayout等。 - SkinSupportConstraintLayout:支持
ConstraintLayout换肤。
- SkinSupportCompat:支持原生 View、
- 灵活的换肤策略 (SkinStrategy):
BuildIn("night"):应用内后缀资源换肤(如color_primary_night)。PrefixBuildIn("night"):应用内前缀资源换肤(如night_color_primary)。Assets("skin.night"):Assets 目录插件包换肤。SDCard("/sdcard/skins/skin.night"):SD 卡本地插件包换肤。
- 用户动态调色盘 (
SkinUserThemeManager):支持在运行时自由修改颜色与图片(无需制作/加载皮肤 APK 资源包),配置自动持久化保存。 - 无缝平滑淡出动画 (
loadSkinWithTransition):自动捕捉窗口画面遮罩并在后台切皮肤,渐变淡出遮罩消除画面白色闪烁。 - Kotlin 协程支持 (
loadSkinSuspend):内置loadSkinSuspend挂起函数,支持后台异步解压/解析皮肤资源,主线程平滑刷新 UI。 - 代码 View DSL 绑定 (
bindSkinColor/bindSkinCustom):针对代码动态创建的 View,支持链式 DSL 动态绑定换肤逻辑。 - 生命周期感知 (Lifecycle-Aware):提供
SkinLifecycleObserver,自动监听 Fragment/Dialog 的生命周期并在重绘或恢复时实时同步皮肤。
📦 依赖引入
在项目的 build.gradle / build.gradle.kts 中添加:
dependencies {
// 核心库
implementation("com.github.Orientsec.Skinnable:SkinLibrary:2.3.0")
// 支持 AppCompat 控件换肤
implementation("androidx.appcompat:appcompat:1.8.0")
implementation("com.github.Orientsec.Skinnable:SkinSupportCompat:2.3.0")
// 支持 Material Design 控件换肤
implementation("com.google.android.material:material:1.14.0")
implementation("com.github.Orientsec.Skinnable:SkinSupportDesign:2.3.0")
// 支持 ConstraintLayout 换肤
implementation("androidx.constraintlayout:constraintlayout:2.2.2")
implementation("com.github.Orientsec.Skinnable:SkinSupportConstraintLayout:2.3.0")
}
🚀 快速上手
1. 初始化框架与异常监控配置
在 Application.onCreate() 中配置换肤 Factory、异常监控与熔断阈值:
class App : Application() {
override fun onCreate() {
super.onCreate()
SkinConfig.builder()
.addInflater(SkinAppCompatViewFactory())
.addInflater(SkinConstraintViewFactory())
.addInflater(SkinMaterialViewFactory())
.setEnableAutoFallback(true) // 开启异常自动降级
.setMaxErrorThreshold(5) // 连续 5 次加载/缺失异常自动触发熔断降级
.setErrorListener(object : SkinErrorListener {
override fun onError(type: SkinErrorType, message: String, cause: Throwable?) {
Log.e("SkinMonitor", "Skin Exception [$type]: $message", cause)
}
override fun onFallbackToDefault(reason: String) {
Toast.makeText(this@App, "换肤降级: $reason", Toast.LENGTH_LONG).show()
}
})
.build()
.initManager(this)
.loadSkin() // 加载上次保存的皮肤状态
}
}
2. 在 XML 中声明换肤属性
在需要换肤的控件上加上 app:skinnable="true" 属性:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/bg_color"
android:textColor="@color/text_color"
app:skinnable="true" />
🎨 换肤策略与高级调用
1. 应用内换肤 (后缀 / 前缀模式)
推荐将应用内换肤资源放到单独的源码目录(例如 src/main/res-night),并在 build.gradle.kts 中配置:
android {
sourceSets {
getByName("main") {
res.srcDirs("src/main/res", "src/main/res-night")
}
}
}
- 后缀加载:将夜间模式资源命名为
bg_color_night,调用:SkinManager.loadSkin(SkinStrategy.BuildIn("night")) - 前缀加载:将夜间模式资源命名为
night_bg_color,调用:SkinManager.loadSkin(SkinStrategy.PrefixBuildIn("night"))
2. 用户动态调色盘自定义主题 (SkinUserThemeManager)
无需打包加载皮肤 APK,直接在代码中修改指定颜色资源或图片路径:
// 设置品牌主色为粉色
SkinUserThemeManager.addColorState(R.color.demo_primary, "#E91E63")
SkinUserThemeManager.apply() // 保存并全局生效(支持与 Compose 实时同步变色)
// 重置自定义主题
SkinUserThemeManager.clearColors()
3. Jetpack Compose 深度无缝换肤 (SkinTheme & AppTheme)
在 Compose 界面中使用 SkinTheme 包装器,结合 AppTheme 动态 Design Tokens 和 skinColorResource:
@Composable
fun MyComposeScreen() {
SkinTheme(
colorResourceMapping = mapOf(
"primary" to R.color.demo_primary,
"surface" to R.color.demo_surface,
"textPrimary" to R.color.demo_text
)
) {
// 使用 AppTheme 响应式 Design Tokens
val primaryColor = AppTheme.colors.primary
val dynamicIcon = skinPainterResource(R.drawable.ic_logo)
Surface(color = primaryColor) {
Row {
dynamicIcon?.let { Image(painter = it, contentDescription = null) }
Text(text = "Hello Compose Skinnable!", color = AppTheme.colors.textPrimary)
}
}
}
}
4. APM 告警埋点契约接入 (ISkinErrorTracker)
绑定全局 APM 异常埋点采集契约:
SkinManager.errorTracker = object : ISkinErrorTracker {
override fun trackError(event: SkinErrorEvent) {
APM.reportEvent("skin_error", mapOf(
"category" to event.category.name,
"message" to event.message,
"resId" to event.resId,
"resName" to event.resName,
"strategyKey" to event.strategyKey
))
}
override fun trackRollback(strategyKey: String, reason: String) {
APM.reportEvent("skin_rollback", mapOf(
"strategyKey" to strategyKey,
"reason" to reason
))
}
}
5. 带无缝平滑过渡动画的换肤 (loadSkinWithTransition)
消除换肤瞬间画面白色闪烁:
SkinManager.loadSkinWithTransition(
activity = this,
strategy = SkinStrategy.BuildIn("night"),
duration = 300L
)
6. 协程异步换肤 (loadSkinSuspend)
结合 Kotlin Coroutines 进行非阻塞的后台加载与 UI 更新:
lifecycleScope.launch {
val success = SkinManager.loadSkinSuspend(SkinStrategy.BuildIn("night"))
if (success) {
// 换肤成功处理
}
}
🧩 进阶用法与混排支持
1. View + Compose 混排界面无缝换肤 (ComposeMixedActivity)
在包含传统 Android View 与嵌合 ComposeView 的传统 Activity 中,点击 View 层控件发起换肤或动态调色盘切换,原生 View 与 ComposeView 均可增量实时无闪烁重绘:
class ComposeMixedActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_compose_mixed)
// 原生 View 按钮交互触发换肤
findViewById<Button>(R.id.btnSwitchPink)?.setOnClickListener {
SkinUserThemeManager.addColorState(R.color.demo_primary, "#E91E63")
SkinUserThemeManager.apply() // 原生 View 与 Compose 实时秒级变色
}
// 嵌合 ComposeView 绑定
findViewById<ComposeView>(R.id.composeViewInNative)?.setContent {
SkinTheme {
SkinComposeDemoCard(
title = "Compose 卡片组件",
subtitle = "实时响应 View 层换肤事件!",
primaryColorRes = R.color.demo_primary,
textColorRes = R.color.demo_text
)
}
}
}
}
2. Fragment 与 Dialog 自动换肤
通过绑定 SkinLifecycleObserver,使 Fragment、Dialog 或自定义 LifecycleOwner 自动感知换肤并在恢复或显示时重绘:
class TestFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycle.addObserver(SkinLifecycleObserver(requireContext()))
}
}
📝 更新日志 (Changelog)
v2.3.0 (Lottie 矢量动画换肤与全量表单控件增强升级)
- 新增: 新增
SkinLottieAnimationView控件与 Lottie 换肤体系,支持按 KeyPath/属性注入主题色 ColorFilter、按图层动态替换ImageAssetDelegate资源以及关联合约 Raw Lottie 资源包/动画拦截器无缝换肤。 - 新增: 扩展表单与选择控件全量换肤支持,全面覆盖
SeekBar(滑块与进度 Tint)、RatingBar(星级评分 Tint)、Spinner(下拉弹框与背景)、AutoCompleteTextView/MultiAutoCompleteTextView及CheckedTextView。 - 修复: 彻底修复 View 构造阶段 (
super(...)) 触发子类重写 Setter 方法时,因SkinHelper尚未完成初始化而引发的InflateException: Error inflating classNPE 崩溃隐患。 - 优化: 优化 VectorDrawable 安全 mutate 与 ColorFilter 状态残留清理逻辑,完善官方 Demo 示例(包含
Compose、WebView注入、动态调色盘与全量表单/选择控件演示)。
v2.2.0 (生产级安全兜底与 Compose 重构升级)
- 新增: 重构推出 SafeSkinResources 三级代理 (Layer 1 调色盘 -> Layer 2 插件包 -> Layer 3 宿主默认资源),捕获一切外置包解析异常、缺失 ID、OOM 等,严禁向 View 层抛出未捕获崩溃。
- 新增: 新增 SkinFallbackManager 事务与熔断器,实现
SkinTransaction原子换肤事务 Rollback 回滚与单会话连续失败熔断锁死,自动清空损坏缓存并锁定默认主题。 - 新增: 新增 ISkinErrorTracker APM 告警埋点契约,支持捕获上报
MISSING_RESOURCE、OOM_DECODE、CORRUPTED_PACKAGE、TIMEOUT、ROLLBACK_TRIGGERED与CIRCUIT_BREAKER_TRIPPED事件。 - 重构: 重构升华 Jetpack Compose 换肤体系,推出
@Stable/@Immutable限制的AppColors/AppTypography/AppShapes动态 Tokens、SkinTheme主题包装器及skinColorResource/skinPainterResource增量重组函数,支持无缝映射 Compose Material 3MaterialTheme。 - 新增: 新增
ComposeMixedActivity传统 View 与 ComposeView 混排全场景无缝换肤演示范例。 - 修复: 修复系统框架资源 (
0x01xxxxxx) 以及皮肤包正常未覆盖资源被误判定为熔断失败计数的 Bug。 - 修复: 修复
SkinUserThemeManager.apply()调色盘刷新时未能成功发射_skinStateFlow导致 Compose 界面未同步变色的 Bug。
v2.1.0
- 新增: 新增
SkinUserThemeManager用户动态调色盘功能,支持无需外部资源包即可实时修改 Theme 颜色与持久化。 - 新增: 新增
SkinnableThemeJetpack Compose 换肤集成,支持 Compose 组件响应式自动重绘。 - 新增: 新增
loadSkinWithTransition带淡出遮罩动画的无缝换肤方法,消除界面闪烁。 - 新增: 新增
bindSkinColor/bindSkinCustom等 View 动态 DSL 换肤扩展。 - 新增: 新增
loadSkinSuspend协程换肤挂起函数,支持非阻塞后台换肤。 - 新增: 新增
SkinLifecycleObserver自动绑定生命周期感知,在 Fragment/Dialog 重建或显示时自动恢复当前皮肤。 - 优化: 重构并升级官方 Demo 示例。
