241 lines
8.1 KiB
Kotlin
241 lines
8.1 KiB
Kotlin
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()
|
||
|
||
// ==================== 外部目录相关 ====================
|
||
|
||
/** 主题根目录:/Android/data/<package>/files/keyboard_themes */
|
||
private fun getThemeRootDir(context: Context): File =
|
||
File(context.getExternalFilesDir(null), "keyboard_themes")
|
||
|
||
/** 某个具体主题目录:/Android/.../keyboard_themes/<themeName> */
|
||
private fun getThemeDir(context: Context, themeName: String): File =
|
||
File(getThemeRootDir(context), themeName)
|
||
|
||
// ==================== 内置主题拷贝(assets -> 外部目录) ====================
|
||
|
||
/**
|
||
* 确保 APK 自带的主题(assets/keyboard_themes/...) 已经复制到
|
||
* /Android/data/.../files/keyboard_themes 目录下。
|
||
*
|
||
* 行为:
|
||
* - 如果主题目录不存在:整套拷贝过去。
|
||
* - 如果主题目录已经存在:只复制“新增文件”,不会覆盖已有文件。
|
||
*
|
||
* 建议在 IME 的 onCreate() 里调用一次。
|
||
*/
|
||
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)
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|