键盘主题设置

This commit is contained in:
pengxiaolong
2025-11-28 16:44:12 +08:00
parent be276ae65d
commit 242d690e17
118 changed files with 1228 additions and 412 deletions

View File

@@ -29,6 +29,10 @@ import com.example.myapplication.data.WordDictionary
import com.example.myapplication.data.LanguageModelLoader
import com.example.myapplication.SuggestionStats
import android.widget.HorizontalScrollView
import com.example.myapplication.theme.ThemeManager
import android.util.TypedValue
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
class MyInputMethodService : InputMethodService() {
private var lastWordForLM: String? = null // 上一次输入的词
@@ -109,7 +113,7 @@ class MyInputMethodService : InputMethodService() {
private val keyStrokeWidthDp = 1 // 键的描边宽度
private val keyMarginDp = 2 // 键的外边距
private val keyMarginDp = 1 // 键的外边距
private val keyPaddingHorizontalDp = 6 // 键的水平内边距
@@ -134,6 +138,9 @@ class MyInputMethodService : InputMethodService() {
override fun onCreate() {
super.onCreate()
ThemeManager.ensureBuiltInThemesInstalled(this) // 如果你用了内置 assets 方案
ThemeManager.init(this)
Thread {
// 1) Trie 词典
try {
@@ -227,11 +234,11 @@ class MyInputMethodService : InputMethodService() {
updateKeyLabels(view)
// 默认应用:文字黑色 + 边框默认色 + 背景透明,并写入缓存
setKeyTextColorInt(Color.BLACK)
setKeyBorderColorInt(Color.parseColor("#1A000000"))
// // 默认应用:文字黑色 + 边框默认色 + 背景透明,并写入缓存
// setKeyTextColorInt(Color.BLACK)
// setKeyBorderColorInt(Color.parseColor("#1A000000"))
applyPerKeyBackgroundForMainKeyboard(view)
setKeyBackgroundColorInt(Color.TRANSPARENT) // 白底为 Color.WHITE
// 每次创建后立即把样式应用到所有键
@@ -242,6 +249,219 @@ class MyInputMethodService : InputMethodService() {
return view
}
/**
* 根据 viewIdName例如 "key_w")给某个键设置背景图片。
* 默认情况drawable 名 = viewIdName例如 "key_w" -> R.drawable.key_w
* 如果传了 drawableName比如 "key_w_up",就用那个。
*/
private fun applyKeyBackground(
root: View,
viewIdName: String,
drawableName: String? = null
) {
// 1. 找到这个 view
val viewId = resources.getIdentifier(viewIdName, "id", packageName)
if (viewId == 0) return
val v = root.findViewById<View?>(viewId) ?: return
// 2. 目标资源名key_xxx
val keyName = drawableName ?: viewIdName
val rawDrawable = ThemeManager.getDrawableForKey(this, keyName) ?: return
// ⚠️ 特殊:若是 background就要等比缩放到高度=243dp
if (viewIdName == "background") {
val scaled = scaleDrawableToHeight(rawDrawable, 243f)
v.background = scaled
return
}
// 普通按键直接用
v.background = rawDrawable
}
// 缩放 Drawable 到指定高度
private fun scaleDrawableToHeight(src: Drawable, targetDp: Float): Drawable {
val dm = resources.displayMetrics
val targetHeightPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
targetDp,
dm
).toInt()
// 尝试取 bitmap
val bitmap = (src as? BitmapDrawable)?.bitmap ?: return src
// 原图宽高
val w = bitmap.width
val h = bitmap.height
// 计算缩放比例(高度缩到 243dp
val ratio = targetHeightPx.toFloat() / h
val targetWidthPx = (w * ratio).toInt()
// 缩放 bitmap
val scaledBitmap = Bitmap.createScaledBitmap(
bitmap,
targetWidthPx,
targetHeightPx,
true
)
// 返回新的 drawable
return BitmapDrawable(resources, scaledBitmap).apply {
setBounds(0, 0, targetWidthPx, targetHeightPx)
}
}
// 主键盘所有按键的背景图片
private fun applyPerKeyBackgroundForMainKeyboard(root: View) {
// a..z 小写默认背景key_a, key_b, ...
for (c in 'a'..'z') {
val idName = "key_$c"
applyKeyBackground(root, idName) // 对应 drawable: key_a, key_b, ...
}
val idName = "background"
applyKeyBackground(root, idName)
// 键盘背景drawable = "keyboard_root"
// 其他功能键/特殊键,直接用同名 drawable
val others = listOf(
"key_space",
"key_send",
"key_del",
"key_up", // 如果布局里是 key_up就改成那个
"key_123", // 切到数字键盘
"key_ai" // AI 键盘
)
others.forEach { idName ->
applyKeyBackground(root, idName)
}
}
// 数字键盘所有按键的背景图片
private fun applyPerKeyBackgroundForNumberKeyboard(root: View) {
// 0..9
for (i in 0..9) {
val idName = "key_$i" // id: key_0 ... key_9
applyKeyBackground(root, idName) // drawable: key_0 ... key_9
}
val idName = "background"
applyKeyBackground(root, idName)
// 你在数字层定义的符号键
val symbolKeys = listOf(
"key_comma",
"key_dot",
"key_minus",
"key_slash",
"key_colon",
"key_semicolon",
"key_paren_l",
"key_paren_r",
"key_dollar",
"key_amp",
"key_at",
"key_question",
"key_exclam",
"key_quote",
"key_quote_d"
)
symbolKeys.forEach { idName ->
applyKeyBackground(root, idName)
}
// 功能键
val others = listOf(
"key_symbols_more", // 切符号层
"key_abc", // 回主键盘
"key_ai",
"key_space",
"key_send",
"key_del"
)
others.forEach { idName ->
applyKeyBackground(root, idName)
}
}
//符号键盘所有按键的背景图片
private fun applyPerKeyBackgroundForSymbolKeyboard(root: View) {
val symbolKeys = listOf(
// 第一行
"key_bracket_l",
"key_bracket_r",
"key_brace_l",
"key_brace_r",
"key_hash",
"key_percent",
"key_caret",
"key_asterisk",
"key_plus",
"key_equal",
// 第二行
"key_underscore",
"key_backslash",
"key_pipe",
"key_tilde",
"key_lt",
"key_gt",
"key_euro",
"key_pound",
"key_money",
"key_bullet",
// 第三行
"key_dot",
"key_comma",
"key_question",
"key_exclam",
"key_quote"
)
symbolKeys.forEach { idName ->
applyKeyBackground(root, idName)
}
val idName = "background"
applyKeyBackground(root, idName)
val others = listOf(
"key_symbols_123",
"key_backspace",
"key_abc",
"key_ai",
"key_space",
"key_send"
)
others.forEach { idName ->
applyKeyBackground(root, idName)
}
}
//大小写切换键的背景图片
private fun updateKeyBackgroundsForLetters(root: View) {
for (c in 'a'..'z') {
val idName = "key_$c"
// 小写drawable = "key_a"
// 大写drawable = "key_a_up"
val drawableName = if (isShiftOn) "${idName}_up" else idName
applyKeyBackground(root, idName, drawableName)
}
val upKeyIdName = "key_up"
// Shift 开:背景图 key_up_upper
// Shift 关:背景图 key_up
val upDrawableName = if (isShiftOn) "key_up_upper" else "key_up"
applyKeyBackground(root, upKeyIdName, upDrawableName)
}
// 确保了输入法在启动时能够正确显示主键盘
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
@@ -294,9 +514,9 @@ class MyInputMethodService : InputMethodService() {
?.setOnClickListener { sendKey(' ') }
// shift
var shiftId = resources.getIdentifier("key_shift","id",packageName)
var shiftId = resources.getIdentifier("key_up","id",packageName)
if (shiftId==0) shiftId = resources.getIdentifier("key_Shift","id",packageName)
if (shiftId==0) shiftId = resources.getIdentifier("key_up","id",packageName)
keyboardView.findViewById<View?>(shiftId)?.setOnClickListener { v ->
toggleShift()
@@ -307,18 +527,18 @@ class MyInputMethodService : InputMethodService() {
}
//点击一次删除,长按连删
keyboardView.findViewById<View?>(resources.getIdentifier("key_backspace","id",packageName))?.let { v ->
keyboardView.findViewById<View?>(resources.getIdentifier("key_del","id",packageName))?.let { v ->
v.setOnClickListener { handleBackspace() }
attachRepeatDelete(v)
}
//跳数字键盘
keyboardView.findViewById<View?>(resources.getIdentifier("key_number","id",packageName))
keyboardView.findViewById<View?>(resources.getIdentifier("key_123","id",packageName))
?.setOnClickListener { showNumberKeyboard() }
//跳AI 键盘
keyboardView.findViewById<View?>(resources.getIdentifier("key_Ai","id",packageName))
keyboardView.findViewById<View?>(resources.getIdentifier("key_ai","id",packageName))
?.setOnClickListener { showAiKeyboard() }
// 发送
@@ -332,21 +552,21 @@ class MyInputMethodService : InputMethodService() {
val resId = resources.getIdentifier("number_keyboard","layout",packageName)
if (resId != 0) {
numberKeyboardView = layoutInflater.inflate(resId,null)
// 首次创建:立即应用当前文字色、边框色与背景色
applyTextColorToAllTextViews(numberKeyboardView, currentTextColor)
applyBorderColorToAllKeys(numberKeyboardView, currentBorderColor)
numberKeyboardView?.let {
applyPerKeyBackgroundForNumberKeyboard(it)
applyTextColorToAllTextViews(it, currentTextColor)
applyBorderColorToAllKeys(it, currentBorderColor)
}
}
}
numberKeyboardView?.let {
currentKeyboardView = it
setupListenersForNumberView(it)
setInputView(it)
}
}
//数字键盘事件
private fun setupListenersForNumberView(numView: View) {
@@ -360,20 +580,20 @@ class MyInputMethodService : InputMethodService() {
// 符号键
val symbolMap: List<Pair<String, Char>> = listOf(
"key_comma" to ',',
"key_period" to '.',
"key_tilde" to '~',
"key_dot" to '.',
"key_minus" to '-',
"key_slash" to '/',
"key_colon" to ':',
"key_semicolon" to ';',
"key_left_paren" to '(',
"key_right_paren" to ')',
"key_paren_l" to '(',
"key_paren_r" to ')',
"key_dollar" to '$',
"key_amp" to '&',
"key_at" to '@',
"key_question" to '?',
"key_exclaim" to '!',
"key_quote_single" to '\'',
"key_quote" to '”'
"key_exclam" to '!',
"key_quote" to '\'',
"key_quote_d" to '”'
)
symbolMap.forEach { (name, ch) ->
val id = resources.getIdentifier(name, "id", packageName)
@@ -382,13 +602,17 @@ class MyInputMethodService : InputMethodService() {
}
// 切换:符号层
numView.findViewById<View?>(resources.getIdentifier("key_symbol_switch","id",packageName))
numView.findViewById<View?>(resources.getIdentifier("key_symbols_more","id",packageName))
?.setOnClickListener { showSymbolKeyboard() }
// 切回字母
numView.findViewById<View?>(resources.getIdentifier("key_abc_switch","id",packageName))
numView.findViewById<View?>(resources.getIdentifier("key_abc","id",packageName))
?.setOnClickListener { switchToMainKeyboard() }
//跳AI 键盘
numView.findViewById<View?>(resources.getIdentifier("key_ai","id",packageName))
?.setOnClickListener { showAiKeyboard() }
// 空格
numView.findViewById<View?>(resources.getIdentifier("key_space","id",packageName))
?.setOnClickListener { sendKey(' ') }
@@ -398,41 +622,41 @@ class MyInputMethodService : InputMethodService() {
?.setOnClickListener { performSendAction() }
//点击一次删除,长按连删
numView.findViewById<View?>(resources.getIdentifier("key_backspace","id",packageName))?.let { v ->
numView.findViewById<View?>(resources.getIdentifier("key_del","id",packageName))?.let { v ->
v.setOnClickListener { handleBackspace() }
attachRepeatDelete(v)
}
}
//显示符号键盘
private fun showSymbolKeyboard() {
private fun showSymbolKeyboard() {
if (symbolKeyboardView == null) {
val resId = resources.getIdentifier("symbol_keyboard","layout",packageName)
if (resId != 0) {
symbolKeyboardView = layoutInflater.inflate(resId,null)
// 首次创建:立即应用当前文字色、边框色与背景色
applyTextColorToAllTextViews(symbolKeyboardView, currentTextColor)
applyBorderColorToAllKeys(symbolKeyboardView, currentBorderColor)
symbolKeyboardView?.let {
applyPerKeyBackgroundForSymbolKeyboard(it)
applyTextColorToAllTextViews(it, currentTextColor)
applyBorderColorToAllKeys(it, currentBorderColor)
}
}
}
symbolKeyboardView?.let {
currentKeyboardView = it
setupListenersForSymbolView(it)
setInputView(it)
}
}
//符号键盘事件
private fun setupListenersForSymbolView(symView: View) {
val pairs = listOf(
// 第一行
"key_lbracket" to '[',
"key_rbracket" to ']',
"key_lbrace" to '{',
"key_rbrace" to '}',
"key_bracket_l" to '[',
"key_bracket_r" to ']',
"key_brace_l" to '{',
"key_brace_r" to '}',
"key_hash" to '#',
"key_percent" to '%',
"key_caret" to '^',
@@ -449,15 +673,15 @@ class MyInputMethodService : InputMethodService() {
"key_gt" to '>',
"key_euro" to '€',
"key_pound" to '£',
"key_yen" to '¥',
"key_middot" to '·',
"key_money" to '¥',
"key_bullet" to '',
// 第三行
"key_period" to '.',
"key_dot" to '.',
"key_comma" to ',',
"key_question" to '?',
"key_exclaim" to '!',
"key_quote_single" to '\''
"key_exclam" to '!',
"key_quote" to '\''
)
pairs.forEach { (name, ch) ->
val id = resources.getIdentifier(name, "id", packageName)
@@ -466,11 +690,11 @@ class MyInputMethodService : InputMethodService() {
}
// 切换回数字
symView.findViewById<View?>(resources.getIdentifier("key_123_switch","id",packageName))
symView.findViewById<View?>(resources.getIdentifier("key_symbols_123","id",packageName))
?.setOnClickListener { showNumberKeyboard() }
// 切回字母
symView.findViewById<View?>(resources.getIdentifier("key_abc_switch","id",packageName))
symView.findViewById<View?>(resources.getIdentifier("key_abc","id",packageName))
?.setOnClickListener { switchToMainKeyboard() }
// 空格
@@ -486,6 +710,9 @@ class MyInputMethodService : InputMethodService() {
v.setOnClickListener { handleBackspace() }
attachRepeatDelete(v)
}
//跳AI 键盘
symView.findViewById<View?>(resources.getIdentifier("key_ai","id",packageName))
?.setOnClickListener { showAiKeyboard() }
}
//显示AI 键盘
@@ -711,18 +938,19 @@ private fun insertCompletion(word: String) {
// 字母键大小写转换
private fun updateKeyLabels(view: View) {
var shiftId = resources.getIdentifier("key_shift","id",packageName)
if (shiftId==0) shiftId = resources.getIdentifier("key_Shift","id",packageName)
var shiftId = resources.getIdentifier("key_up","id",packageName)
if (shiftId==0) shiftId = resources.getIdentifier("key_up","id",packageName)
view.findViewById<View?>(shiftId)?.isActivated = isShiftOn
for (c in 'a'..'z') {
val id = resources.getIdentifier("key_$c","id",packageName)
findTextViewSafe(view,id)?.text = if (isShiftOn) c.uppercaseChar().toString() else c.toString()
findTextViewSafe(view,id)?.text = ""
}
//同步切换字母键背景
updateKeyBackgroundsForLetters(view)
}
//简单的音效播放器
private fun playKeyClick() {
@@ -772,13 +1000,13 @@ private fun insertCompletion(word: String) {
//键盘背景图更换
fun setKeyboardBackground(@DrawableRes resId: Int) {
mainHandler.post {
mainKeyboardView?.findViewById<LinearLayout>(R.id.keyboard_root)?.setBackgroundResource(resId)
mainKeyboardView?.findViewById<LinearLayout>(R.id.background)?.setBackgroundResource(resId)
numberKeyboardView?.findViewById<LinearLayout>(R.id.keyboard_root)?.setBackgroundResource(resId)
numberKeyboardView?.findViewById<LinearLayout>(R.id.background)?.setBackgroundResource(resId)
symbolKeyboardView?.findViewById<LinearLayout>(R.id.keyboard_root)?.setBackgroundResource(resId)
symbolKeyboardView?.findViewById<LinearLayout>(R.id.background)?.setBackgroundResource(resId)
aiKeyboardView?.findViewById<LinearLayout>(R.id.keyboard_root)?.setBackgroundResource(resId)
aiKeyboardView?.findViewById<LinearLayout>(R.id.background)?.setBackgroundResource(resId)
}
}
@@ -854,113 +1082,54 @@ private fun insertCompletion(word: String) {
}
}
//动态设置按键背景色
fun setKeyBackgroundColor(colorStateList: ColorStateList) {
currentBackgroundColor = colorStateList
}
fun setKeyBackgroundColorInt(@ColorInt colorInt: Int) {
setKeyBackgroundColor(ColorStateList.valueOf(colorInt))
}
//动态设置按键背景色
fun setKeyBackgroundColor(colorStateList: ColorStateList) {
currentBackgroundColor = colorStateList
mainHandler.post {
// 复用同一个应用函数(会读取 currentBackgroundColor 一起生成背景)
applyBorderColorToAllKeys(mainKeyboardView, currentBorderColor)
applyBorderColorToAllKeys(numberKeyboardView, currentBorderColor)
applyBorderColorToAllKeys(symbolKeyboardView, currentBorderColor)
applyBorderColorToAllKeys(aiKeyboardView, currentBorderColor)
}
}
//为根视图内所有 TextView 应用:圆角描边 + 背景色 + ripple + 统一外边距/内边距
// 为所有 TextView 只设置 margin/padding不再统一设置背景
private fun applyBorderColorToAllKeys(root: View?, borderColor: ColorStateList) {
if (root == null) return
val strokeWidthPx = keyStrokeWidthDp.dpToPx()
val cornerRadiusPx = keyCornerRadiusDp.dpToPx().toFloat()
val marginPx = keyMarginDp.dpToPx()
val paddingH = keyPaddingHorizontalDp.dpToPx()
// 忽略的按键 idsuggestion_1到suggestion_20
val ignoredIds = (0..20).map {
resources.getIdentifier("suggestion_$it", "id", packageName)
// 忽略的按键 idsuggestion_1...
val ignoredIds = (0..20).map {
resources.getIdentifier("suggestion_$it", "id", packageName)
}.toSet()
fun dfs(v: View?) {
when (v) {
is TextView -> {
if (v.id in ignoredIds) {
v.background = null // 或者 v.setBackgroundColor(Color.TRANSPARENT)
if (v.id in ignoredIds) {
v.background = null
return
}
v.background = buildKeyBackground(
borderColor = borderColor,
fillColor = currentBackgroundColor,
strokeWidthPx = strokeWidthPx,
cornerRadiusPx = cornerRadiusPx
)
// 只更新 margin
(v.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
if (lp.leftMargin != marginPx || lp.topMargin != marginPx ||
lp.rightMargin != marginPx || lp.bottomMargin != marginPx
) {
lp.setMargins(marginPx, marginPx, marginPx, marginPx)
v.layoutParams = lp
}
}
if (v.paddingLeft < paddingH || v.paddingRight < paddingH) {
v.setPadding(paddingH, v.paddingTop, paddingH, v.paddingBottom)
lp.setMargins(marginPx, marginPx, marginPx, marginPx)
v.layoutParams = lp
}
// 只更新 padding
v.setPadding(paddingH, v.paddingTop, paddingH, v.paddingBottom)
}
is ViewGroup -> for (i in 0 until v.childCount) dfs(v.getChildAt(i))
}
}
dfs(root)
}
// 构造“圆角 + 可变描边 + 可变背景色 + ripple”的背景
private fun buildKeyBackground(
borderColor: ColorStateList,
fillColor: ColorStateList,
strokeWidthPx: Int,
cornerRadiusPx: Float
): Drawable {
val content = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = cornerRadiusPx
setStroke(strokeWidthPx, borderColor)
color = fillColor // 动态背景色
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000")) // 涟漪色
val mask = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = cornerRadiusPx
setColor(Color.WHITE)
}
RippleDrawable(rippleColor, content, mask)
} else {
content
}
}
//工具dp -> px
private fun Int.dpToPx(): Int {

View File

@@ -0,0 +1,240 @@
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()
}
}

View File

@@ -0,0 +1,31 @@
package com.example.myapplication.utils
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
fun unzipToDir(zipInputStream: InputStream, targetDir: File) {
ZipInputStream(BufferedInputStream(zipInputStream)).use { zis ->
var entry: ZipEntry? = zis.nextEntry
val buffer = ByteArray(4096)
while (entry != null) {
val file = File(targetDir, entry.name)
if (entry.isDirectory) {
file.mkdirs()
} else {
file.parentFile?.mkdirs()
FileOutputStream(file).use { fos ->
var count: Int
while (zis.read(buffer).also { count = it } != -1) {
fos.write(buffer, 0, count)
}
}
}
zis.closeEntry()
entry = zis.nextEntry
}
}
}