键盘主题设置
This commit is contained in:
@@ -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()
|
||||
|
||||
// 忽略的按键 id(suggestion_1到suggestion_20)
|
||||
val ignoredIds = (0..20).map {
|
||||
resources.getIdentifier("suggestion_$it", "id", packageName)
|
||||
|
||||
// 忽略的按键 id(suggestion_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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user