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 = mutableMapOf() // ==================== 外部目录相关 ==================== //通知主题更新 private val listeners = mutableSetOf<() -> Unit>() fun addThemeChangeListener(listener: () -> Unit) { listeners.add(listener) } fun removeThemeChangeListener(listener: () -> Unit) { listeners.remove(listener) } /** 主题根目录:/Android/data//files/keyboard_themes */ private fun getThemeRootDir(context: Context): File = File(context.filesDir, "keyboard_themes") /** 某个具体主题目录:/Android/.../keyboard_themes/ */ 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 { val map = mutableMapOf() 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 { val root = getThemeRootDir(context) if (!root.exists() || !root.isDirectory) return emptyList() return root.listFiles() ?.filter { it.isDirectory } ?.map { it.name } ?.sorted() ?: emptyList() } }