Files
Android-key-of-love/app/src/main/java/com/example/myapplication/theme/ThemeManager.kt
pengxiaolong 7814a10815 完善
2025-12-26 22:01:04 +08:00

242 lines
7.9 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.example.myapplication.theme
import android.content.Context
import android.content.res.AssetManager
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import java.io.File
object ThemeManager {
// SharedPreferences 保存当前主题名
private const val PREF_NAME = "ime_theme_prefs"
private const val KEY_CURRENT_THEME = "current_theme"
// 当前主题名(如 "default"
@Volatile
private var currentThemeName: String? = null
// 缓存:规范化后的 keyName(lowercase) -> Drawable
@Volatile
private var drawableCache: MutableMap<String, Drawable> = mutableMapOf()
// ==================== 外部目录相关 ====================
//通知主题更新
private val listeners = mutableSetOf<() -> Unit>()
fun addThemeChangeListener(listener: () -> Unit) {
listeners.add(listener)
}
fun removeThemeChangeListener(listener: () -> Unit) {
listeners.remove(listener)
}
/** 主题根目录:/Android/data/<package>/files/keyboard_themes */
private fun getThemeRootDir(context: Context): File =
File(context.filesDir, "keyboard_themes")
/** 某个具体主题目录:/Android/.../keyboard_themes/<themeName> */
private fun getThemeDir(context: Context, themeName: String): File =
File(getThemeRootDir(context), themeName)
fun ensureBuiltInThemesInstalled(context: Context) {
val am = context.assets
val rootName = "keyboard_themes"
val themeRootDir = getThemeRootDir(context)
if (!themeRootDir.exists()) themeRootDir.mkdirs()
// 列出 assets/keyboard_themes 下的所有子目录,比如 default、dark...
val themeNames = am.list(rootName) ?: return
for (themeName in themeNames) {
val assetThemePath = "$rootName/$themeName" // 如 keyboard_themes/default
val targetThemeDir = getThemeDir(context, themeName)
copyAssetsDirToDir(am, assetThemePath, targetThemeDir)
}
}
/**
* 递归地将 assets 中的某个目录合并复制到目标 File 目录下。
*
* 规则:
* - 如果目标目录不存在,则创建。
* - 如果目标文件已存在,则跳过(不覆盖用户改过的图)。
* - 如果是新文件,则复制过去(补充新图)。
*/
private fun copyAssetsDirToDir(
assetManager: AssetManager,
assetDir: String,
targetDir: File
) {
if (!targetDir.exists()) targetDir.mkdirs()
val children = assetManager.list(assetDir) ?: return
for (child in children) {
val childAssetPath = "$assetDir/$child"
val outFile = File(targetDir, child)
val grandChildren = assetManager.list(childAssetPath)
if (grandChildren?.isNotEmpty() == true) {
// 子目录,递归
copyAssetsDirToDir(assetManager, childAssetPath, outFile)
} else {
// 文件:如果已经存在就不覆盖;不存在才复制(补充新图)
if (outFile.exists()) continue
assetManager.open(childAssetPath).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
}
// ==================== 主题初始化 / 切换 ====================
/**
* 初始化主题系统:
* - 读取 SharedPreferences 中上次使用的主题名
* - 默认使用 "default"
* - 加载该主题的所有图片到缓存
*/
fun init(context: Context) {
if (currentThemeName != null) return
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val name = prefs.getString(KEY_CURRENT_THEME, "default") ?: "default"
setCurrentTheme(context, name)
}
/**
* 切换当前主题:
* - 更新 currentThemeName
* - 写 SharedPreferences
* - 重新加载该主题的全部图片到缓存
*/
fun setCurrentTheme(context: Context, themeName: String) {
currentThemeName = themeName
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_CURRENT_THEME, themeName)
.apply()
drawableCache = loadThemeDrawables(context, themeName)
listeners.forEach { it.invoke() }
}
fun getCurrentThemeName(): String? = currentThemeName
/**
* 扫描某个主题目录下的所有 png/jpg/webp 文件,
* 用“文件名(去掉扩展名,小写)”作为 keyName构造缓存。
*
* 例如:
* /.../keyboard_themes/default/key_a.png -> "key_a"
* /.../keyboard_themes/default/key_a_up.PNG -> "key_a_up"
*/
private fun loadThemeDrawables(
context: Context,
themeName: String
): MutableMap<String, Drawable> {
val map = mutableMapOf<String, Drawable>()
val dir = getThemeDir(context, themeName)
if (!dir.exists() || !dir.isDirectory) return map
dir.listFiles()?.forEach { file ->
if (!file.isFile) return@forEach
val lowerName = file.name.lowercase()
if (
!(lowerName.endsWith(".png") ||
lowerName.endsWith(".jpg") ||
lowerName.endsWith(".jpeg") ||
lowerName.endsWith(".webp"))
) return@forEach
// 统一小写作为 key比如 key_a_up.png -> "key_a_up"
val key = lowerName.substringBeforeLast(".")
val bmp = BitmapFactory.decodeFile(file.absolutePath) ?: return@forEach
val d = BitmapDrawable(context.resources, bmp)
map[key] = d
}
return map
}
// ==================== 对外:按 keyName 取 Drawable ====================
/**
* 根据 keyName 获取对应的 Drawable。
*
* keyName 一般就是 view 的 idName
* - "key_a"
* - "key_a_up"
* - "key_1"
* - "key_space"
* - "key_send"
* ...
*
* 对应文件:
* /Android/data/.../files/keyboard_themes/<当前主题>/key_a.png
*
* 内部统一用 keyName.lowercase() 做匹配,不区分大小写。
*/
fun getDrawableForKey(context: Context, keyName: String): Drawable? {
if (currentThemeName == null) {
init(context)
}
// 统一小写,避免大小写差异
val norm = keyName.lowercase()
// 1) 缓存里有就直接返回
drawableCache[norm]?.let { return it }
// 2) 缓存里没有:尝试从当前主题目录里单独加载一遍(兼容运行时新增图片)
val theme = currentThemeName ?: return null
val dir = getThemeDir(context, theme)
val candidates = listOf(
File(dir, "$norm.png"),
File(dir, "$norm.webp"),
File(dir, "$norm.jpg"),
File(dir, "$norm.jpeg")
)
for (f in candidates) {
if (f.exists() && f.isFile) {
val bmp = BitmapFactory.decodeFile(f.absolutePath) ?: continue
val d = BitmapDrawable(context.resources, bmp)
drawableCache[norm] = d
return d
}
}
// 3) 找不到就返回 null调用方自己决定是否兜底
return null
}
// ==================== 可选:列出所有已安装主题 ====================
/**
* 返回当前外部目录下所有已经存在的主题名列表,
* 例如 ["default", "dark", "cute_pink"]。
*/
fun listAvailableThemes(context: Context): List<String> {
val root = getThemeRootDir(context)
if (!root.exists() || !root.isDirectory) return emptyList()
return root.listFiles()
?.filter { it.isDirectory }
?.map { it.name }
?.sorted()
?: emptyList()
}
}