diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4602773..9c54e85 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -67,4 +67,11 @@ dependencies {
implementation("androidx.navigation:navigation-compose:2.7.5")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
+
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+ implementation("com.squareup.retrofit2:retrofit:2.11.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.11.0")
+ // 协程(如果还没加)
+ implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8c6f6c8..166f92e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,8 +3,12 @@
+
+
@@ -21,6 +25,16 @@
+
+
+
+
+
+
(R.id.iv_close).setOnClickListener {
+ finish()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/myapplication/ImeGuideActivity.kt b/app/src/main/java/com/example/myapplication/ImeGuideActivity.kt
new file mode 100644
index 0000000..891791a
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/ImeGuideActivity.kt
@@ -0,0 +1,203 @@
+package com.example.myapplication
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.provider.Settings
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.FrameLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.example.myapplication.MyInputMethodService
+
+
+class ImeGuideActivity : AppCompatActivity() {
+
+ private lateinit var btnEnable: LinearLayout
+ private lateinit var btnSelect: LinearLayout
+ private lateinit var tvStep1Status: TextView//
+ private lateinit var tvStep2Status: TextView//
+ private lateinit var btnEnabledText: TextView//
+ private lateinit var btnSelectText: TextView//
+ private lateinit var btnEnabledimg: ImageView//
+ private lateinit var btnSelectimg: ImageView//
+
+ private var imeObserver: android.database.ContentObserver? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_ime_guide)
+
+ // // 设置关闭按钮点击事件
+ // findViewById(R.id.iv_close).setOnClickListener {
+ // finish()
+ // }
+
+ btnEnable = findViewById(R.id.enabled)//btn启用输入法
+ btnSelect = findViewById(R.id.select)//btn选择输入法
+ tvStep1Status = findViewById(R.id.Steps)//第一步的提示
+ tvStep2Status = findViewById(R.id.stepTips)//第二步的提示
+ btnEnabledText = findViewById(R.id.btnEnabledText)//启用输入法按钮文字
+ btnSelectText = findViewById(R.id.btnSelectText)//选择输入法按钮文字
+ btnEnabledimg = findViewById(R.id.btnEnabledimg)//启用输入法按钮图片
+ btnSelectimg = findViewById(R.id.btnSelectimg)//选择输入法按钮图片
+
+ // // 第一步:启用输入法
+ // btnEnable.setOnClickListener {
+ // startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS))
+ // }
+
+
+ // // 第二步:切换输入法
+ // btnSelect.setOnClickListener {
+ // val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ // imm.showInputMethodPicker()
+ // }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refreshStatus()
+ registerImeObserver()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterImeObserver()
+ }
+
+ private fun registerImeObserver() {
+ if (imeObserver != null) return
+
+ imeObserver = object : android.database.ContentObserver(
+ android.os.Handler(android.os.Looper.getMainLooper())
+ ) {
+ override fun onChange(selfChange: Boolean) {
+ super.onChange(selfChange)
+ refreshStatus()
+ }
+ }
+
+ contentResolver.registerContentObserver(
+ android.provider.Settings.Secure.getUriFor(
+ android.provider.Settings.Secure.DEFAULT_INPUT_METHOD
+ ),
+ false,
+ imeObserver!!
+ )
+ }
+
+ private fun unregisterImeObserver() {
+ imeObserver?.let {
+ contentResolver.unregisterContentObserver(it)
+ }
+ imeObserver = null
+ }
+
+ /** 刷新步骤状态(是否启用/选择) */
+ private fun refreshStatus() {
+ val enabled = isImeEnabled()
+ val selected = isImeSelected()
+
+ // 根据状态设置按钮的点击行为
+ if (enabled) {
+ // 输入法已启用时,禁用启用按钮的点击事件
+ btnEnable.setOnClickListener(null)
+ } else {
+ // 输入法未启用时,设置启用按钮的点击事件
+ btnEnable.setOnClickListener {
+ startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS))
+ }
+ }
+
+ if (selected) {
+ // 输入法已切换时,禁用选择按钮的点击事件
+ btnSelect.setOnClickListener(null)
+ } else {
+ // 输入法未切换时,设置选择按钮的点击事件
+ btnSelect.setOnClickListener {
+ val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.showInputMethodPicker()
+ }
+ }
+
+ if(!enabled &&!selected) {
+ btnEnable.background = getDrawable(R.drawable.ime_guide_activity_btn_unfinished)
+ btnSelect.background = getDrawable(R.drawable.ime_guide_activity_btn_unfinished)
+ btnEnabledText.setTextColor(Color.parseColor("#FFFFFF"))
+ btnSelectText.setTextColor(Color.parseColor("#FFFFFF"))
+ btnEnabledimg.setImageResource(R.drawable.ime_guide_activity_btn_unfinished_img)
+ btnSelectimg.setImageResource(R.drawable.ime_guide_activity_btn_unfinished_img)
+ tvStep1Status.text = "Step one"
+ tvStep1Status.text = "Step one"
+ tvStep2Status.text = "Check to enable key of love"
+ }else if(!enabled && selected){
+ btnEnable.background = getDrawable(R.drawable.ime_guide_activity_btn_unfinished)
+ btnSelect.background = getDrawable(R.drawable.ime_guide_activity_btn_unfinished)
+ btnEnabledimg.setImageResource(R.drawable.ime_guide_activity_btn_unfinished_img)
+ btnSelectimg.setImageResource(R.drawable.ime_guide_activity_btn_unfinished_img)
+ btnEnabledText.setTextColor(Color.parseColor("#FFFFFF"))
+ btnSelectText.setTextColor(Color.parseColor("#FFFFFF"))
+ tvStep1Status.text = "Step one"
+ tvStep2Status.text = "Check to enable key of love"
+ }else if(enabled && !selected){
+ btnEnable.background = getDrawable(R.drawable.ime_guide_activity_btn_completed)
+ btnSelect.background = getDrawable(R.drawable.ime_guide_activity_btn_unfinished)
+ btnEnabledimg.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
+ btnSelectimg.setImageResource(R.drawable.ime_guide_activity_btn_unfinished_img)
+ btnEnabledText.setTextColor(Color.parseColor("#A1A1A1"))
+ btnSelectText.setTextColor(Color.parseColor("#FFFFFF"))
+ tvStep1Status.text = "Step two"
+ tvStep2Status.text = "Select key of love as your default input method"
+ }else if(enabled && selected){
+ btnEnable.background = getDrawable(R.drawable.ime_guide_activity_btn_completed)
+ btnSelect.background = getDrawable(R.drawable.ime_guide_activity_btn_completed)
+ btnEnabledimg.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
+ btnSelectimg.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
+ btnEnabledText.setTextColor(Color.parseColor("#A1A1A1"))
+ btnSelectText.setTextColor(Color.parseColor("#A1A1A1"))
+ tvStep1Status.text = "Completed"
+ tvStep2Status.text = "You have completed the relevant Settings"
+ Toast.makeText(this, "The input method is all set!", Toast.LENGTH_SHORT).show()
+ startActivity(Intent(this, GuideActivity::class.java))
+ finish()
+ }
+ }
+
+ /** 是否启用了本输入法 */
+ private fun isImeEnabled(): Boolean {
+ val enabledImes = Settings.Secure.getString(
+ contentResolver,
+ Settings.Secure.ENABLED_INPUT_METHODS
+ ) ?: return false
+
+ // 用真正的类,而不是手写字符串
+ val myImeComponent = ComponentName(this, MyInputMethodService::class.java)
+ // 系统存的是 flattenToString() 的格式
+ val myImeId = myImeComponent.flattenToString()
+
+ // enabledImes 是一个用 ":" 分隔的列表
+ return enabledImes.split(':').contains(myImeId)
+ }
+
+ /** 是否已切换为当前输入法 */
+ private fun isImeSelected(): Boolean {
+ val currentIme = Settings.Secure.getString(
+ contentResolver,
+ Settings.Secure.DEFAULT_INPUT_METHOD
+ ) ?: return false
+
+ val myImeComponent = ComponentName(this, MyInputMethodService::class.java)
+ val myImeId = myImeComponent.flattenToString()
+
+ // 直接用同一种格式比对
+ return currentIme == myImeId
+ }
+
+}
diff --git a/app/src/main/java/com/example/myapplication/MainActivity.kt b/app/src/main/java/com/example/myapplication/MainActivity.kt
index 49fedec..670d273 100644
--- a/app/src/main/java/com/example/myapplication/MainActivity.kt
+++ b/app/src/main/java/com/example/myapplication/MainActivity.kt
@@ -43,5 +43,23 @@ class MainActivity : AppCompatActivity() {
}
}
}
+
+ // 6. 检查是否有导航参数,处理从键盘跳转过来的请求
+ handleNavigationFromIntent()
+ }
+
+ private fun handleNavigationFromIntent() {
+ val navigateTo = intent.getStringExtra("navigate_to")
+ if (navigateTo == "recharge_fragment") {
+ // 延迟执行导航,确保导航控制器已经准备好
+ bottomNav.post {
+ try {
+ navController.navigate(R.id.action_global_rechargeFragment)
+ } catch (e: Exception) {
+ // 如果导航失败,记录错误日志
+ android.util.Log.e("MainActivity", "Failed to navigate to recharge fragment", e)
+ }
+ }
+ }
}
}
diff --git a/app/src/main/java/com/example/myapplication/MyInputMethodService.kt b/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
index 1999beb..ade91fc 100644
--- a/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
+++ b/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
@@ -1,13 +1,10 @@
-// 键盘服务
package com.example.myapplication
+
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.RippleDrawable
-import android.graphics.drawable.Drawable
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.Build
@@ -15,132 +12,154 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Gravity
-import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import android.widget.HorizontalScrollView
import android.widget.LinearLayout
+import android.widget.PopupWindow
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
-import com.example.myapplication.R
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
+import com.example.myapplication.theme.ThemeManager
+import com.example.myapplication.keyboard.KeyboardEnvironment
+import com.example.myapplication.keyboard.MainKeyboard
+import com.example.myapplication.keyboard.NumberKeyboard
+import com.example.myapplication.keyboard.SymbolKeyboard
+import com.example.myapplication.keyboard.AiKeyboard
+
+class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
+
+ // ====== 键盘视图 & 键盘对象 ======
+ private var currentKeyboardView: View? = null
+
+ private var mainKeyboardView: View? = null
+ private var numberKeyboardView: View? = null
+ private var symbolKeyboardView: View? = null
+ private var aiKeyboardView: View? = null
+
+ private var mainKeyboard: MainKeyboard? = null
+ private var numberKeyboard: NumberKeyboard? = null
+ private var symbolKeyboard: SymbolKeyboard? = null
+ private var aiKeyboard: AiKeyboard? = null
+
+ // 单键预览(只给建议栏等用,按键的预览已经放到各 Keyboard 里了)
+ private var keyPreviewPopup: PopupWindow? = null
-class MyInputMethodService : InputMethodService() {
private var lastWordForLM: String? = null // 上一次输入的词
- private val wordDictionary = WordDictionary(this)// 词库
+ private val wordDictionary = WordDictionary(this) // 词库
+ private var currentInput = StringBuilder() // 当前输入前缀
+ private var completionSuggestions = emptyList() // 自动完成建议
- private var currentInput = StringBuilder()// 当前输入
+ @Volatile private var isSpecialToken: BooleanArray = BooleanArray(0)
- private var completionSuggestions = emptyList()// 自动完成建议
+ private val suggestionStats by lazy { SuggestionStats(applicationContext) }
+ private val specialTokens = setOf("", "", "")
- @Volatile private var isSpecialToken: BooleanArray = BooleanArray(0)// 是否是特殊词汇
+ @Volatile private var bigramModel: com.example.myapplication.data.BigramModel? = null
+ @Volatile private var word2id: Map = emptyMap()
+ @Volatile private var id2word: List = emptyList()
+ @Volatile private var bigramReady: Boolean = false
- private val suggestionStats by lazy { SuggestionStats(applicationContext) }// 建议统计
-
- private val specialTokens = setOf("", "", "") // 特殊词汇集合
-
- @Volatile private var bigramModel: com.example.myapplication.data.BigramModel? = null// 双字模型
-
- @Volatile private var word2id: Map = emptyMap()// 词到 id 映射
-
- @Volatile private var id2word: List = emptyList()// id 到词 映射
-
- @Volatile private var bigramReady: Boolean = false// 双字模型是否加载完成
-
- //
companion object {
private const val TAG = "MyIME"
-
private const val NOTIFICATION_CHANNEL_ID = "input_method_channel"
-
private const val NOTIFICATION_ID = 1
}
-
- private val audioManager by lazy { getSystemService(AUDIO_SERVICE) as AudioManager }// 音频管理器
- private val mainHandler = Handler(Looper.getMainLooper())// 主线程 Handler
+ // ===== KeyboardEnvironment 实现所需属性 =====
- private var currentKeyboardView: View? = null// 当前键盘视图
+ override val ctx: Context
+ get() = this
- private var mainKeyboardView: View? = null// 主键盘视图
+ // override val layoutInflater get() = super.getLayoutInflater()
- private var numberKeyboardView: View? = null// 数字键盘视图
-
- private var symbolKeyboardView: View? = null// 符号键盘视图
+ override val mainHandler: Handler = Handler(Looper.getMainLooper())
- private var aiKeyboardView: View? = null// AI 键盘视图
+ override val audioManager: AudioManager by lazy {
+ getSystemService(AUDIO_SERVICE) as AudioManager
+ }
- private var isShiftOn = false// 是否按下 Shift 键
- private var isInputViewShownFlag = false// 是否显示输入视图标志
+ // ===== 输入视图状态 =====
- private var isDeleting = false// 是否正在长按删除
+ private var isInputViewShownFlag = false
- private val repeatDelInitialDelay = 350L // 首次长按延迟
+ // Shift 状态
+ private var isShiftOn = false
- private val repeatDelInterval = 50L // 连续删除间隔
+ // 删除长按
+ private var isDeleting = false
+ private val repeatDelInitialDelay = 350L
+ private val repeatDelInterval = 50L
private val repeatDelRunnable = object : Runnable {
override fun run() {
if (!isDeleting) return
- handleBackspace()
-
+ deleteOne()
mainHandler.postDelayed(this, repeatDelInterval)
}
}
- // ===== 颜色缓存(关键) =====
- private var currentTextColor: ColorStateList = ColorStateList.valueOf(Color.BLACK) // 默认黑字
+ // ===== 颜色缓存 =====
+ override var currentTextColor: ColorStateList =
+ ColorStateList.valueOf(Color.parseColor("#000000"))//联想字体颜色
+ private set
- private var currentBorderColor: ColorStateList =
- ColorStateList.valueOf(Color.parseColor("#1A000000")) // 默认淡灰边框
+ override var currentBorderColor: ColorStateList =
+ ColorStateList.valueOf(Color.parseColor("#1A000000"))
+ private set
- private var currentBackgroundColor: ColorStateList =
- ColorStateList.valueOf(Color.TRANSPARENT) // 默认透明背景
+ override var currentBackgroundColor: ColorStateList =
+ ColorStateList.valueOf(Color.TRANSPARENT)
+ private set
- // ===== 样式参数(可按需调整) =====
- private val keyCornerRadiusDp = 5 // 键的圆角半径
+ // 副字符映射表(主键盘上滑)
+ private val swipeAltMap: Map = mapOf(
+ 'q' to '1',
+ 'w' to '2',
+ 'e' to '3',
+ 'r' to '4',
+ 't' to '5',
+ 'y' to '6',
+ 'u' to '7',
+ 'i' to '8',
+ 'o' to '9',
+ 'p' to '0',
- private val keyStrokeWidthDp = 1 // 键的描边宽度
+ 'a' to '!',
+ 's' to '@',
+ 'd' to '#',
+ 'f' to '$',
+ 'g' to '%',
+ 'h' to '^',
+ 'j' to '&',
+ 'k' to '*',
+ 'l' to '(',
- private val keyMarginDp = 1 // 键的外边距
+ 'z' to ')',
+ 'x' to '-',
+ 'c' to '+',
+ 'v' to '=',
+ 'b' to '?',
+ 'n' to '/',
+ 'm' to '.'
+ )
- private val keyPaddingHorizontalDp = 6 // 键的水平内边距
+ // ================= 生命周期 =================
- /** 获取上一个完整英文词(纯字母),统一小写 */
- private fun getPrevWordBeforeCursor(maxLen: Int = 128): String? {
- val before = currentInputConnection?.getTextBeforeCursor(maxLen, 0)?.toString() ?: return null
-
- // 如果最后一个字符是字母,说明词不完整,返回null
- if (before.isNotEmpty() && before.last().isLetter()) return null
-
- val toks = before.replace(Regex("[^A-Za-z]"), " ").trim().split(Regex("\\s+"))
-
- if (toks.isEmpty()) return null
-
- val word = toks.last().lowercase()
-
- if (word.isEmpty()) return null
- return word
- }
-
- //启动时初始化一些关键资源和设置。
override fun onCreate() {
super.onCreate()
-
- ThemeManager.ensureBuiltInThemesInstalled(this) // 如果你用了内置 assets 方案
+
+ ThemeManager.ensureBuiltInThemesInstalled(this)
ThemeManager.init(this)
+ // 异步加载词典与 bigram 模型
Thread {
// 1) Trie 词典
try {
@@ -148,30 +167,26 @@ class MyInputMethodService : InputMethodService() {
} catch (e: Throwable) {
Log.w(TAG, "Trie load failed: ${e.message}", e)
}
-
+
// 2) Bigram 模型
try {
val m = LanguageModelLoader.load(this)
- // 基本一致性校验:vocab 与 CSR 维度
+
require(m.biRowptr.size == m.vocab.size + 1) {
"biRowptr size ${m.biRowptr.size} != vocab.size+1 ${m.vocab.size + 1}"
}
require(m.biCols.size == m.biLogp.size) {
"biCols size ${m.biCols.size} != biLogp size ${m.biLogp.size}"
}
-
+
bigramModel = m
-
+
val map = HashMap(m.vocab.size * 2)
-
- m.vocab.forEachIndexed { idx, w -> map[w.lowercase()] = idx } // 统一小写映射
-
+ m.vocab.forEachIndexed { idx, w -> map[w.lowercase()] = idx }
word2id = map
-
id2word = m.vocab
isSpecialToken = BooleanArray(id2word.size)
-
for (i in id2word.indices) {
if (specialTokens.contains(id2word[i])) {
isSpecialToken[i] = true
@@ -181,14 +196,14 @@ class MyInputMethodService : InputMethodService() {
bigramReady = true
} catch (e: Throwable) {
bigramReady = false
+ Log.w(TAG, "Bigram load failed: ${e.message}", e)
}
}.start()
- createNotificationChannelIfNeeded()
+ createNotificationChannelIfNeeded()
tryStartForegroundSafe()
}
-
- //保服务在前台运行时能够被用户感知
+
private fun createNotificationChannelIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
@@ -198,12 +213,10 @@ class MyInputMethodService : InputMethodService() {
).apply { description = "保持输入法服务运行" }
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
manager.createNotificationChannel(channel)
}
}
- //将输入法服务以前台服务的形式启动
private fun tryStartForegroundSafe() {
try {
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
@@ -215,575 +228,166 @@ class MyInputMethodService : InputMethodService() {
.build()
startForeground(NOTIFICATION_ID, notification)
-
Log.i(TAG, "startForeground success")
} catch (e: Exception) {
Log.w(TAG, "startForeground failed: ${e.message}")
}
}
- //创建并初始化自定义输入法的主键盘视图。
+ // 创建输入视图:此处只负责选择主键盘
override fun onCreateInputView(): View {
- val view = layoutInflater.inflate(resources.getIdentifier("keyboard","layout",packageName), null)
-
- mainKeyboardView = view
-
- currentKeyboardView = view
-
- setupKeyboardListenersForMain(view)
-
- updateKeyLabels(view)
-
- // // 默认应用:文字黑色 + 边框默认色 + 背景透明,并写入缓存
- // setKeyTextColorInt(Color.BLACK)
-
- // setKeyBorderColorInt(Color.parseColor("#1A000000"))
- applyPerKeyBackgroundForMainKeyboard(view)
- setKeyBackgroundColorInt(Color.TRANSPARENT) // 白底为 Color.WHITE
-
- // 每次创建后立即把样式应用到所有键
- applyTextColorToAllTextViews(view, currentTextColor)
-
- applyBorderColorToAllKeys(view, currentBorderColor)
-
- return view
+ val keyboard = ensureMainKeyboard()
+ currentKeyboardView = keyboard.rootView
+ mainKeyboardView = keyboard.rootView
+ return keyboard.rootView
}
- /**
- * 根据 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(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) {
super.onStartInputView(info, restarting)
-
isInputViewShownFlag = true
-
- mainKeyboardView?.let { switchToMainKeyboard() }
+ showMainKeyboard()
}
- // 输入法界面结束时执行清理操作
override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput)
-
isInputViewShownFlag = false
-
stopRepeatDelete()
mainHandler.postDelayed({
if (!isInputViewShownFlag) {
- try { hideWindow() } catch (e: Exception) { Log.w(TAG, "hideWindow failed: ${e.message}") }
+ try {
+ hideWindow()
+ } catch (e: Exception) {
+ Log.w(TAG, "hideWindow failed: ${e.message}")
+ }
}
}, 50)
}
- // 输入法服务销毁时执行必要的清理操作
override fun onDestroy() {
super.onDestroy()
-
stopRepeatDelete()
}
- // 处理输入事件
- private fun setupKeyboardListenersForMain(keyboardView: View?) {
- if (keyboardView == null) return
+ // ================= KeyboardEnvironment:键盘切换 =================
- // letters a..z
- for (c in 'a'..'z') {
- val id = resources.getIdentifier("key_$c", "id", packageName)
-
- val tv = findTextViewSafe(keyboardView, id) ?: continue
-
- tv.isClickable = true
-
- tv.setOnClickListener { sendKey(c) }
- }
-
- // space
- keyboardView.findViewById(resources.getIdentifier("key_space","id",packageName))
- ?.setOnClickListener { sendKey(' ') }
-
- // shift
- var shiftId = resources.getIdentifier("key_up","id",packageName)
-
- if (shiftId==0) shiftId = resources.getIdentifier("key_up","id",packageName)
-
- keyboardView.findViewById(shiftId)?.setOnClickListener { v ->
- toggleShift()
-
- v.isActivated = isShiftOn
-
- updateKeyLabels(keyboardView)
- }
-
- //点击一次删除,长按连删
- keyboardView.findViewById(resources.getIdentifier("key_del","id",packageName))?.let { v ->
- v.setOnClickListener { handleBackspace() }
-
- attachRepeatDelete(v)
- }
-
- //跳数字键盘
- keyboardView.findViewById(resources.getIdentifier("key_123","id",packageName))
- ?.setOnClickListener { showNumberKeyboard() }
-
- //跳AI 键盘
- keyboardView.findViewById(resources.getIdentifier("key_ai","id",packageName))
- ?.setOnClickListener { showAiKeyboard() }
-
- // 发送
- keyboardView.findViewById(resources.getIdentifier("key_send","id",packageName))
- ?.setOnClickListener { performSendAction() }
- }
-
- //显示数字键盘
- private fun showNumberKeyboard() {
- if (numberKeyboardView == null) {
- val resId = resources.getIdentifier("number_keyboard","layout",packageName)
- if (resId != 0) {
- numberKeyboardView = layoutInflater.inflate(resId,null)
-
- numberKeyboardView?.let {
- applyPerKeyBackgroundForNumberKeyboard(it)
- applyTextColorToAllTextViews(it, currentTextColor)
- applyBorderColorToAllKeys(it, currentBorderColor)
+ private fun ensureMainKeyboard(): MainKeyboard {
+ if (mainKeyboard == null) {
+ mainKeyboard = MainKeyboard(
+ env = this,
+ swipeAltMap = swipeAltMap,
+ onToggleShift = {
+ isShiftOn = !isShiftOn
+ isShiftOn
}
- }
+ )
+ mainKeyboardView = mainKeyboard!!.rootView
+
+ // 这里再给删除键挂长按连删(用你自己的 attachRepeatDeleteInternal)
+ val delId = resources.getIdentifier("key_del", "id", packageName)
+ mainKeyboardView?.findViewById(delId)?.let { attachRepeatDeleteInternal(it) }
}
- numberKeyboardView?.let {
- currentKeyboardView = it
- setupListenersForNumberView(it)
- setInputView(it)
+ return mainKeyboard!!
+ }
+
+ private fun ensureNumberKeyboard(): NumberKeyboard {
+ if (numberKeyboard == null) {
+ numberKeyboard = NumberKeyboard(this)
+ numberKeyboardView = numberKeyboard!!.rootView
+
+ // 数字键盘删除键 key_del
+ val delId = resources.getIdentifier("key_del", "id", packageName)
+ numberKeyboardView?.findViewById(delId)?.let { attachRepeatDeleteInternal(it) }
}
+ return numberKeyboard!!
+ }
+
+ private fun ensureSymbolKeyboard(): SymbolKeyboard {
+ if (symbolKeyboard == null) {
+ symbolKeyboard = SymbolKeyboard(this)
+ symbolKeyboardView = symbolKeyboard!!.rootView
+
+ // 符号键盘删除键 key_backspace
+ val delId = resources.getIdentifier("key_backspace", "id", packageName)
+ symbolKeyboardView?.findViewById(delId)?.let { attachRepeatDeleteInternal(it) }
+ }
+ return symbolKeyboard!!
+ }
+
+ private fun ensureAiKeyboard(): AiKeyboard {
+ if (aiKeyboard == null) {
+ aiKeyboard = AiKeyboard(this)
+ aiKeyboardView = aiKeyboard!!.rootView
+ }
+ return aiKeyboard!!
+ }
+
+ override fun showMainKeyboard() {
+ val kb = ensureMainKeyboard()
+ currentKeyboardView = kb.rootView
+ setInputView(kb.rootView)
+ kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ }
+
+ override fun showNumberKeyboard() {
+ val kb = ensureNumberKeyboard()
+ currentKeyboardView = kb.rootView
+ setInputView(kb.rootView)
+ kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ }
+
+ override fun showSymbolKeyboard() {
+ val kb = ensureSymbolKeyboard()
+ currentKeyboardView = kb.rootView
+ setInputView(kb.rootView)
+ kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ }
+
+ override fun showAiKeyboard() {
+ val kb = ensureAiKeyboard()
+ currentKeyboardView = kb.rootView
+ setInputView(kb.rootView)
+ kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
- //数字键盘事件
- private fun setupListenersForNumberView(numView: View) {
- // 0~9
- for (i in 0..9) {
- val id = resources.getIdentifier("key_$i","id",packageName)
+ // ================== 文本输入核心逻辑 ==================
- numView.findViewById(id)?.setOnClickListener { sendKey(i.toString()[0]) }
- }
+ /** 获取上一个完整英文词(纯字母),统一小写 */
+ private fun getPrevWordBeforeCursor(maxLen: Int = 128): String? {
+ val before = currentInputConnection
+ ?.getTextBeforeCursor(maxLen, 0)
+ ?.toString()
+ ?: return null
- // 符号键
- val symbolMap: List> = listOf(
- "key_comma" to ',',
- "key_dot" to '.',
- "key_minus" to '-',
- "key_slash" to '/',
- "key_colon" to ':',
- "key_semicolon" to ';',
- "key_paren_l" to '(',
- "key_paren_r" to ')',
- "key_dollar" to '$',
- "key_amp" to '&',
- "key_at" to '@',
- "key_question" to '?',
- "key_exclam" to '!',
- "key_quote" to '\'',
- "key_quote_d" to '”'
- )
- symbolMap.forEach { (name, ch) ->
- val id = resources.getIdentifier(name, "id", packageName)
+ // 如果最后一个字符是字母,说明词不完整,返回 null
+ if (before.isNotEmpty() && before.last().isLetter()) return null
- numView.findViewById(id)?.setOnClickListener { sendKey(ch) }
- }
+ val toks = before
+ .replace(Regex("[^A-Za-z]"), " ")
+ .trim()
+ .split(Regex("\\s+"))
- // 切换:符号层
- numView.findViewById(resources.getIdentifier("key_symbols_more","id",packageName))
- ?.setOnClickListener { showSymbolKeyboard() }
+ if (toks.isEmpty()) return null
- // 切回字母
- numView.findViewById(resources.getIdentifier("key_abc","id",packageName))
- ?.setOnClickListener { switchToMainKeyboard() }
-
- //跳AI 键盘
- numView.findViewById(resources.getIdentifier("key_ai","id",packageName))
- ?.setOnClickListener { showAiKeyboard() }
-
- // 空格
- numView.findViewById(resources.getIdentifier("key_space","id",packageName))
- ?.setOnClickListener { sendKey(' ') }
-
- // 发送
- numView.findViewById(resources.getIdentifier("key_send","id",packageName))
- ?.setOnClickListener { performSendAction() }
-
- //点击一次删除,长按连删
- numView.findViewById(resources.getIdentifier("key_del","id",packageName))?.let { v ->
- v.setOnClickListener { handleBackspace() }
- attachRepeatDelete(v)
- }
+ val word = toks.last().lowercase()
+ if (word.isEmpty()) return null
+ return word
}
- //显示符号键盘
- private fun showSymbolKeyboard() {
- if (symbolKeyboardView == null) {
- val resId = resources.getIdentifier("symbol_keyboard","layout",packageName)
- if (resId != 0) {
- symbolKeyboardView = layoutInflater.inflate(resId,null)
- 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_bracket_l" to '[',
- "key_bracket_r" to ']',
- "key_brace_l" to '{',
- "key_brace_r" to '}',
- "key_hash" to '#',
- "key_percent" to '%',
- "key_caret" to '^',
- "key_asterisk" to '*',
- "key_plus" to '+',
- "key_equal" to '=',
-
- // 第二行
- "key_underscore" to '_',
- "key_backslash" to '\\',
- "key_pipe" to '|',
- "key_tilde" to '~',
- "key_lt" to '<',
- "key_gt" to '>',
- "key_euro" to '€',
- "key_pound" to '£',
- "key_money" to '¥',
- "key_bullet" to '•',
-
- // 第三行
- "key_dot" to '.',
- "key_comma" to ',',
- "key_question" to '?',
- "key_exclam" to '!',
- "key_quote" to '\''
- )
- pairs.forEach { (name, ch) ->
- val id = resources.getIdentifier(name, "id", packageName)
-
- symView.findViewById(id)?.setOnClickListener { sendKey(ch) }
- }
-
- // 切换回数字
- symView.findViewById(resources.getIdentifier("key_symbols_123","id",packageName))
- ?.setOnClickListener { showNumberKeyboard() }
-
- // 切回字母
- symView.findViewById(resources.getIdentifier("key_abc","id",packageName))
- ?.setOnClickListener { switchToMainKeyboard() }
-
- // 空格
- symView.findViewById(resources.getIdentifier("key_space","id",packageName))
- ?.setOnClickListener { sendKey(' ') }
-
- // 发送
- symView.findViewById(resources.getIdentifier("key_send","id",packageName))
- ?.setOnClickListener { performSendAction() }
-
- //点击一次删除,长按连删
- symView.findViewById(resources.getIdentifier("key_backspace","id",packageName))?.let { v ->
- v.setOnClickListener { handleBackspace() }
- attachRepeatDelete(v)
- }
- //跳AI 键盘
- symView.findViewById(resources.getIdentifier("key_ai","id",packageName))
- ?.setOnClickListener { showAiKeyboard() }
- }
-
- //显示AI 键盘
- private fun showAiKeyboard() {
- if (aiKeyboardView == null) {
- val res = resources.getIdentifier("ai_keyboard","layout",packageName)
-
- aiKeyboardView = if (res != 0) layoutInflater.inflate(res,null) else buildAiPlaceholder()
-
- // 首次创建:立即应用当前文字色、边框色与背景色
- applyTextColorToAllTextViews(aiKeyboardView, currentTextColor)
-
- applyBorderColorToAllKeys(aiKeyboardView, currentBorderColor)
- }
- aiKeyboardView?.let {
- currentKeyboardView = it
-
- setInputView(it)
- }
- }
- // AI 键盘占位
- private fun buildAiPlaceholder(): View {
- val layout = LinearLayout(this).apply {
- orientation = LinearLayout.VERTICAL
-
- layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
- }
- val tv = TextView(this).apply {
- text = "AI 键盘占位(点击返回主键盘)"
-
- gravity = Gravity.CENTER
-
- textSize = 16f
-
- setPadding(20,20,20,20)
-
- setOnClickListener { switchToMainKeyboard() }
- }
- layout.addView(tv)
-
- return layout
- }
-
- //切换到主键盘视图
- private fun switchToMainKeyboard() {
- mainKeyboardView?.let {
- currentKeyboardView = it
-
- setInputView(it)
-
- updateKeyLabels(it)
- }
- }
-
- //处理用户输入的单个字符,并根据输入字符的类型更新自动补全和联想建议
- private fun sendKey(c: Char) {
+ // 提交一个字符(原 sendKey)
+ override fun commitKey(c: Char) {
val ic = currentInputConnection ?: return
val toSend = if (isShiftOn && c in 'a'..'z') c.uppercaseChar() else c
-
ic.commitText(toSend.toString(), 1)
-
+
when {
c.isLetter() -> {
- // 直接从编辑器拿“真实前缀”,避免 currentInput 不同步
val prefix = getCurrentWordPrefix()
-
updateCompletionsAndRender(prefix)
}
c == ' ' || !c.isLetter() -> {
- // 遇到空格或标点:前缀结束,隐藏补全
updateCompletionsAndRender(prefix = "")
}
}
@@ -791,51 +395,117 @@ class MyInputMethodService : InputMethodService() {
playKeyClick()
}
- // 处理用户点击删除键
- private fun handleBackspace() {
+ // 删除一个字符(原 handleBackspace)
+ override fun deleteOne() {
val ic = currentInputConnection ?: return
ic.deleteSurroundingText(1, 0)
-
- // 回读光标前缀;若空则隐藏补全
- val prefix = getCurrentWordPrefix()
+ val prefix = getCurrentWordPrefix()
updateCompletionsAndRender(prefix)
-
+
playKeyClick()
}
-
- //根据用户当前输入的词的前缀,从 completionSuggestions 列表中获取建议单词,并将这些单词显示在输入法的界面上的特定位置
- private fun showCompletionSuggestions() {
- mainHandler.post {
- val suggestionsView = mainKeyboardView?.findViewById(R.id.completion_suggestions)
-
- // 创建suggestion视图数组(0..20)
- val suggestions = (0..20).map { i ->
- mainKeyboardView?.findViewById(resources.getIdentifier("suggestion_$i", "id", packageName))
+
+ // 发送(原 performSendAction)
+ override fun performSendAction() {
+ val ic = currentInputConnection ?: return
+
+ val handled = ic.performEditorAction(EditorInfo.IME_ACTION_SEND)
+ if (!handled) {
+ ic.commitText("\n", 1)
+ }
+ playKeyClick()
+ }
+
+ // 按键音效
+ override fun playKeyClick() {
+ try {
+ audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)
+ } catch (_: Throwable) {
+ }
+ }
+
+ // ================== 补全 & 联想 ==================
+
+ override fun getCurrentWordPrefix(maxLen: Int): String {
+ val before = currentInputConnection?.getTextBeforeCursor(maxLen, 0)?.toString() ?: ""
+ val match = Regex("[A-Za-z]+$").find(before)
+ return (match?.value ?: "").lowercase()
+ }
+
+ // 统一处理补全/联想
+ override fun updateCompletionsAndRender(prefix: String) {
+ currentInput.clear()
+ currentInput.append(prefix)
+
+ val lastWord = getPrevWordBeforeCursor()
+
+ Thread {
+ val list = try {
+ if (prefix.isEmpty()) {
+ if (lastWord == null) {
+ emptyList()
+ } else {
+ suggestWithBigram("", lastWord, topK = 20)
+ }
+ } else {
+ val fromBi = suggestWithBigram(prefix, lastWord, topK = 20)
+ if (fromBi.isNotEmpty()) {
+ fromBi.filter { it != prefix }
+ } else {
+ wordDictionary.wordTrie.startsWith(prefix)
+ .take(20)
+ .filter { it != prefix }
+ }
+ }
+ } catch (_: Throwable) {
+ if (prefix.isNotEmpty()) {
+ wordDictionary.wordTrie.startsWith(prefix)
+ .take(20)
+ .filterNot { it == prefix }
+ } else {
+ emptyList()
+ }
}
- // 特殊处理suggestion_0显示prefix
+ mainHandler.post {
+ completionSuggestions = suggestionStats.sortByCount(list.distinct().take(20))
+ showCompletionSuggestions()
+ }
+ }.start()
+ }
+
+ // 显示自动完成建议(布局不变)
+ private fun showCompletionSuggestions() {
+ mainHandler.post {
+ val suggestionsView =
+ mainKeyboardView?.findViewById(R.id.completion_suggestions)
+
+ val suggestions = (0..20).map { i ->
+ mainKeyboardView?.findViewById(
+ resources.getIdentifier("suggestion_$i", "id", packageName)
+ )
+ }
+
+ // suggestion_0 显示 prefix
val prefix = getCurrentWordPrefix()
-
suggestions[0]?.text = prefix
-
suggestions[0]?.visibility = if (prefix.isEmpty()) View.GONE else View.VISIBLE
-
suggestions[0]?.setOnClickListener {
insertCompletion(prefix)
}
- // 使用循环设置文本和点击监听器(从index 1开始)
+ // suggestion_1.. 按 completionSuggestions 填充
suggestions.drop(1).forEachIndexed { index, textView ->
textView?.text = completionSuggestions.getOrNull(index) ?: ""
-
if (index < completionSuggestions.size) {
- textView?.setOnClickListener {
+ textView?.setOnClickListener {
suggestionStats.incClick(completionSuggestions[index])
-
insertCompletion(completionSuggestions[index])
}
+ } else {
+ textView?.setOnClickListener(null)
}
}
@@ -843,153 +513,52 @@ class MyInputMethodService : InputMethodService() {
}
}
- //自动滚动到最左边
+ // 自动滚回到最左边
private fun scrollSuggestionsToStart() {
val sv = mainKeyboardView?.findViewById(R.id.completion_scroll)
-
sv?.post { sv.fullScroll(View.FOCUS_LEFT) }
}
- //获取当前输入的词的前缀
- private fun getCurrentWordPrefix(maxLen: Int = 128): String {
- val before = currentInputConnection?.getTextBeforeCursor(maxLen, 0)?.toString() ?: ""
-
- val match = Regex("[A-Za-z]+$").find(before)
-
- return (match?.value ?: "").lowercase()
- }
-
-// 统一处理补全(有前缀) 与 联想(前缀为空, 用 bigram 预测下一个词)
-private fun updateCompletionsAndRender(prefix: String) {
- // 先清掉你维护的前缀缓存
- currentInput.clear()
-
- currentInput.append(prefix)
-
- // 读取上文词:用于 bigram 联想
- val lastWord = getPrevWordBeforeCursor()
-
- Thread {
- val list = try {
- if (prefix.isEmpty()) {
- if (lastWord == null) {
- emptyList()}
- else {
- // 空前缀:用 bigram 预测下一个词
- suggestWithBigram("", lastWord, topK = 20)
- }
- } else {
- // 有前缀:优先 bigram 的匹配前缀,失败再回退 Trie
- val fromBi = suggestWithBigram(prefix, lastWord, topK = 20)
-
- if (fromBi.isNotEmpty()) fromBi.filter { it != prefix }
-
- else wordDictionary.wordTrie.startsWith(prefix).take(20).filter { it != prefix }
- }
- } catch (_: Throwable) {
- if (prefix.isNotEmpty()) wordDictionary.wordTrie.startsWith(prefix).take(20).filterNot { it == prefix } else emptyList()
- }
-
- mainHandler.post {
- completionSuggestions = suggestionStats.sortByCount(list.distinct().take(20))
-
- val suggestionsView = mainKeyboardView?.findViewById(R.id.completion_suggestions)
-
- showCompletionSuggestions()
- }
- }.start()
-}
-
-//插入一个自动补全的单词
-private fun insertCompletion(word: String) {
- val ic = currentInputConnection ?: return
-
- ic.beginBatchEdit()
-
- try {
- val len = currentInput.length
-
- if (len > 0) ic.deleteSurroundingText(len, 0) // 补全:删掉前缀
-
- ic.commitText("$word ", 1) // 自动补空格
- } finally {
- ic.endBatchEdit()
- }
-
- currentInput.clear()
- //补完一个词后,直接触发“下一个词联想”
- updateCompletionsAndRender(prefix = "")
-}
-
- // 发送
- private fun performSendAction() {
+ // 插入一个补全词
+ private fun insertCompletion(word: String) {
val ic = currentInputConnection ?: return
- val handled = ic.performEditorAction(EditorInfo.IME_ACTION_SEND)
-
- if (!handled) {
- ic.commitText("\n", 1)
+ ic.beginBatchEdit()
+ try {
+ val len = currentInput.length
+ if (len > 0) ic.deleteSurroundingText(len, 0)
+ ic.commitText("$word ", 1)
+ } finally {
+ ic.endBatchEdit()
}
- playKeyClick()
+
+ currentInput.clear()
+ // 补完一个词后,直接联想下一个词
+ updateCompletionsAndRender(prefix = "")
}
- // 切换大小写
- private fun toggleShift() { isShiftOn = !isShiftOn }
+ // ================== 长按删除 ==================
- // 字母键大小写转换
- private fun updateKeyLabels(view: View) {
- var shiftId = resources.getIdentifier("key_up","id",packageName)
- if (shiftId==0) shiftId = resources.getIdentifier("key_up","id",packageName)
- view.findViewById(shiftId)?.isActivated = isShiftOn
-
- for (c in 'a'..'z') {
- val id = resources.getIdentifier("key_$c","id",packageName)
- findTextViewSafe(view,id)?.text = ""
- }
-
- //同步切换字母键背景
- updateKeyBackgroundsForLetters(view)
- }
-
-
- //简单的音效播放器
- private fun playKeyClick() {
- try { audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) } catch (_: Throwable) {}
- }
-
- //安全地查找一个视图中的 TextView 对象
- private fun findTextViewSafe(root: View, id: Int): TextView? {
- val v = root.findViewById(id) ?: return null
-
- return v as? TextView
- }
-
-// 长按触发连续删除
- private fun attachRepeatDelete(view: View) {
+ // 真正实现逻辑(基本照搬你原来的 attachRepeatDelete)
+ private fun attachRepeatDeleteInternal(view: View) {
view.setOnLongClickListener {
if (!isDeleting) {
isDeleting = true
-
- // 先执行一次删除,提升手感,稍作延迟后进入高频连删
mainHandler.postDelayed(repeatDelRunnable, repeatDelInitialDelay)
-
- handleBackspace()
+ deleteOne() // 首次立刻删一次
}
- true // 消费长按事件
+ true
}
- // 松手/取消时停止
view.setOnTouchListener { _, event ->
when (event.actionMasked) {
- MotionEvent.ACTION_UP,
- MotionEvent.ACTION_CANCEL,
- MotionEvent.ACTION_OUTSIDE -> stopRepeatDelete()
+ android.view.MotionEvent.ACTION_UP,
+ android.view.MotionEvent.ACTION_CANCEL,
+ android.view.MotionEvent.ACTION_OUTSIDE -> stopRepeatDelete()
}
- // 返回 false 以便点击事件还能触发单次删除
false
}
}
- //停止长按删除操作
private fun stopRepeatDelete() {
if (isDeleting) {
isDeleting = false
@@ -997,40 +566,40 @@ private fun insertCompletion(word: String) {
}
}
- //键盘背景图更换
+ // ================== 主题 / 样式接口(保留原 API) ==================
+
+ // 键盘背景大图(LinearLayout R.id.background 用的)
fun setKeyboardBackground(@DrawableRes resId: Int) {
mainHandler.post {
- mainKeyboardView?.findViewById(R.id.background)?.setBackgroundResource(resId)
+ mainKeyboardView
+ ?.findViewById(R.id.background)
+ ?.setBackgroundResource(resId)
- numberKeyboardView?.findViewById(R.id.background)?.setBackgroundResource(resId)
+ numberKeyboardView
+ ?.findViewById(R.id.background)
+ ?.setBackgroundResource(resId)
- symbolKeyboardView?.findViewById(R.id.background)?.setBackgroundResource(resId)
+ symbolKeyboardView
+ ?.findViewById(R.id.background)
+ ?.setBackgroundResource(resId)
- aiKeyboardView?.findViewById(R.id.background)?.setBackgroundResource(resId)
+ aiKeyboardView
+ ?.findViewById(R.id.background)
+ ?.setBackgroundResource(resId)
}
}
- //文字颜色
+ // ===== 文字颜色 =====
+
fun setKeyTextColorInt(@ColorInt colorInt: Int) {
setKeyTextColor(ColorStateList.valueOf(colorInt))
}
- //定义视图在不同状态下(如正常、按下、激活等)的颜色
fun setKeyTextColor(colorStateList: ColorStateList) {
- currentTextColor = colorStateList // 更新缓存
-
- mainHandler.post {
- applyTextColorToAllTextViews(mainKeyboardView, currentTextColor)
-
- applyTextColorToAllTextViews(numberKeyboardView, currentTextColor)
-
- applyTextColorToAllTextViews(symbolKeyboardView, currentTextColor)
-
- applyTextColorToAllTextViews(aiKeyboardView, currentTextColor)
- }
+ currentTextColor = colorStateList
+ mainHandler.post { applyThemeToAllKeyboards() }
}
- //设置键盘上按键的文字颜色
fun setKeyTextColorWithStates() {
val states = arrayOf(
intArrayOf(android.R.attr.state_pressed),
@@ -1047,142 +616,75 @@ private fun insertCompletion(word: String) {
setKeyTextColor(ColorStateList(states, colors))
}
- //指定的根视图开始,递归遍历整个视图树
- private fun applyTextColorToAllTextViews(root: View?, color: ColorStateList) {
- if (root == null) return
+ // ===== 边框颜色 =====
- fun dfs(v: View?) {
- when (v) {
- is TextView -> v.setTextColor(color)
-
- is ViewGroup -> for (i in 0 until v.childCount) dfs(v.getChildAt(i))
- }
- }
-
- dfs(root)
- }
-
- //设置键盘上所有按键的边框颜色
fun setKeyBorderColorInt(@ColorInt colorInt: Int) {
setKeyBorderColor(ColorStateList.valueOf(colorInt))
}
- //设置并应用输入法键盘上所有按键的边框颜色
fun setKeyBorderColor(colorStateList: ColorStateList) {
currentBorderColor = colorStateList
-
- mainHandler.post {
- applyBorderColorToAllKeys(mainKeyboardView, currentBorderColor)
-
- applyBorderColorToAllKeys(numberKeyboardView, currentBorderColor)
-
- applyBorderColorToAllKeys(symbolKeyboardView, currentBorderColor)
-
- applyBorderColorToAllKeys(aiKeyboardView, currentBorderColor)
- }
+ mainHandler.post { applyThemeToAllKeyboards() }
}
+ // ===== 背景颜色(如果你有用纯色背景的话) =====
+
fun setKeyBackgroundColor(colorStateList: ColorStateList) {
currentBackgroundColor = colorStateList
+ mainHandler.post { applyThemeToAllKeyboards() }
}
-
+
fun setKeyBackgroundColorInt(@ColorInt colorInt: Int) {
setKeyBackgroundColor(ColorStateList.valueOf(colorInt))
}
+ private fun applyThemeToAllKeyboards() {
+ mainKeyboard?.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ numberKeyboard?.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ symbolKeyboard?.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ aiKeyboard?.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
+ }
- // 为所有 TextView 只设置 margin/padding,不再统一设置背景
- private fun applyBorderColorToAllKeys(root: View?, borderColor: ColorStateList) {
- if (root == null) return
+ // ================== bigram & 联想实现 ==================
- val marginPx = keyMarginDp.dpToPx()
- val paddingH = keyPaddingHorizontalDp.dpToPx()
-
- // 忽略的按键 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
- return
- }
-
- // 只更新 margin
- (v.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
- 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))
+ private fun suggestWithBigram(prefix: String, lastWord: String?, topK: Int = 20): List {
+ val m = bigramModel
+ if (m == null || !bigramReady) {
+ return if (prefix.isNotEmpty()) {
+ wordDictionary.wordTrie.startsWith(prefix).take(topK)
+ } else {
+ emptyList()
}
}
- dfs(root)
- }
-
-
-
- //工具:dp -> px
- private fun Int.dpToPx(): Int {
- return (this * resources.displayMetrics.density + 0.5f).toInt()
- }
-
- //联想:bigram + Trie
- private fun suggestWithBigram(prefix: String, lastWord: String?, topK: Int = 20): List {
- val m = bigramModel
-
- if (m == null || !bigramReady) {
- // 模型未就绪 -> 用 Trie 兜底
- return if (prefix.isNotEmpty()) wordDictionary.wordTrie.startsWith(prefix).take(topK) else emptyList()
- }
-
val pf = prefix.lowercase()
-
val last = lastWord?.lowercase()
-
val lastId = last?.let { word2id[it] }
-
- // —— 有上文:从 bigram 出边取 TopK ——
+
if (lastId != null && lastId >= 0 && lastId + 1 < m.biRowptr.size) {
val start = m.biRowptr[lastId]
-
val end = m.biRowptr[lastId + 1]
if (start in 0..end && end <= m.biCols.size) {
-
- // ① 有前缀过滤版本(过滤特殊词 + 前缀匹配)
+
+ // 带前缀过滤
val buf = ArrayList>(maxOf(0, end - start))
-
var i = start
-
while (i < end) {
val nextId = m.biCols[i]
if (nextId in id2word.indices && !isSpecialToken[nextId]) {
val w = id2word[nextId]
-
if (pf.isEmpty() || w.startsWith(pf)) {
buf.add(w to m.biLogp[i])
}
}
i++
}
-
if (buf.isNotEmpty()) return topKByScore(buf, topK)
-
- //无前缀兜底版本(不看前缀,但也要跳过特殊词)
+
+ // 无前缀兜底
val allBuf = ArrayList>(maxOf(0, end - start))
-
i = start
-
while (i < end) {
val nextId = m.biCols[i]
if (nextId in id2word.indices && !isSpecialToken[nextId]) {
@@ -1190,72 +692,53 @@ private fun insertCompletion(word: String) {
}
i++
}
-
if (allBuf.isNotEmpty()) return topKByScore(allBuf, topK)
}
}
-
+
// —— 无上文 或 无出边 ——
return if (pf.isNotEmpty()) {
- // 有前缀:Trie 兜底
wordDictionary.wordTrie.startsWith(pf).take(topK)
} else {
- // 无前缀:unigram TopK 兜底(过滤特殊词)
unigramTopKFiltered(topK)
}
}
-
- //从大语言模型中获取前缀为空时的前 topK 个最可能的单词,并过滤掉特殊标记的词汇。它通过遍历所有单词并利用优先级队列来维护概率最大的 topK 个词汇,最终返回这些词汇的列表,按概率从大到小排序。
+
private fun unigramTopKFiltered(topK: Int): List {
val m = bigramModel ?: return emptyList()
-
if (!bigramReady) return emptyList()
-
+
val heap = java.util.PriorityQueue>(topK.coerceAtLeast(1)) { a, b ->
a.second.compareTo(b.second)
}
var i = 0
-
val n = id2word.size
-
while (i < n) {
- if (!isSpecialToken[i]) { // ★ 跳过 //
+ if (!isSpecialToken[i]) {
heap.offer(id2word[i] to m.uniLogp[i])
-
if (heap.size > topK) heap.poll()
}
-
i++
}
val out = ArrayList(heap.size)
-
while (heap.isNotEmpty()) out.add(heap.poll().first)
-
out.reverse()
-
return out
}
-
- //从输入的词对列表中获取得分最高的 k 个词,并按得分从高到低的顺序返回。
+
private fun topKByScore(pairs: List>, k: Int): List {
val heap = java.util.PriorityQueue>(k.coerceAtLeast(1)) { a, b ->
a.second.compareTo(b.second)
}
-
for (p in pairs) {
heap.offer(p)
-
if (heap.size > k) heap.poll()
}
val out = ArrayList(heap.size)
-
- while (heap.isNotEmpty()) out.add(heap.poll()?.first ?: "")
-
+ while (heap.isNotEmpty()) out.add(heap.poll().first)
out.reverse()
-
return out
}
-
}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/AiKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/AiKeyboard.kt
new file mode 100644
index 0000000..b25efb9
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/AiKeyboard.kt
@@ -0,0 +1,133 @@
+package com.example.myapplication.keyboard
+
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.example.myapplication.MainActivity
+import com.example.myapplication.theme.ThemeManager
+
+class AiKeyboard(
+ env: KeyboardEnvironment
+) : BaseKeyboard(env) {
+
+ override val rootView: View = run {
+ val res = env.ctx.resources
+ val layoutId = res.getIdentifier("ai_keyboard", "layout", env.ctx.packageName)
+ if (layoutId != 0) {
+ env.layoutInflater.inflate(layoutId, null) as View
+ } else {
+ // 如果找不到布局,创建一个默认的View
+ LinearLayout(env.ctx).apply {
+ orientation = LinearLayout.VERTICAL
+ gravity = Gravity.CENTER
+ addView(TextView(env.ctx).apply {
+ text = "AI Keyboard"
+ })
+ }
+ }
+ }
+
+ init {
+ applyKeyBackground(rootView, "background")
+ applyTheme(
+ env.currentTextColor,
+ env.currentBorderColor,
+ env.currentBackgroundColor
+ )
+ setupListeners()
+ }
+
+ private fun applyKeyBackground(
+ root: View,
+ viewIdName: String,
+ drawableName: String? = null
+ ) {
+ val res = env.ctx.resources
+ val viewId = res.getIdentifier(viewIdName, "id", env.ctx.packageName)
+ if (viewId == 0) return
+ val v = root.findViewById(viewId) ?: return
+
+ val keyName = drawableName ?: viewIdName
+ val rawDrawable = ThemeManager.getDrawableForKey(env.ctx, keyName) ?: return
+
+ if (viewIdName == "background") {
+ val scaled = scaleDrawableToHeight(rawDrawable, 243f)
+ v.background = scaled
+ return
+ }
+ v.background = rawDrawable
+ }
+
+ private fun scaleDrawableToHeight(src: Drawable, targetDp: Float): Drawable {
+ val res = env.ctx.resources
+ val dm = res.displayMetrics
+ val targetHeightPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ targetDp,
+ dm
+ ).toInt()
+
+ val bitmap = (src as? BitmapDrawable)?.bitmap ?: return src
+ val w = bitmap.width
+ val h = bitmap.height
+
+ val ratio = targetHeightPx.toFloat() / h
+ val targetWidthPx = (w * ratio).toInt()
+
+ val scaled = Bitmap.createScaledBitmap(bitmap, targetWidthPx, targetHeightPx, true)
+ return BitmapDrawable(res, scaled).apply {
+ setBounds(0, 0, targetWidthPx, targetHeightPx)
+ }
+ }
+
+
+ private fun setupListeners() {
+ val res = env.ctx.resources
+ val pkg = env.ctx.packageName
+
+ // 如果 ai_keyboard.xml 里有 “返回主键盘” 的按钮,比如 key_abc,就绑定一下
+ val backId = res.getIdentifier("key_abc", "id", pkg)
+ if (backId != 0) {
+ rootView.findViewById(backId)?.setOnClickListener {
+ env.showMainKeyboard()
+ }
+ }
+
+ // 绑定 VIP 按钮点击事件,跳转到充值页面
+ val vipButtonId = res.getIdentifier("key_vip", "id", pkg)
+ if (vipButtonId != 0) {
+ rootView.findViewById(vipButtonId)?.setOnClickListener {
+ navigateToRechargeFragment()
+ }
+ }
+ }
+
+ private fun navigateToRechargeFragment() {
+ try {
+ val intent = Intent(env.ctx, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ putExtra("navigate_to", "recharge_fragment")
+ }
+ env.ctx.startActivity(intent)
+ } catch (e: Exception) {
+ // 如果启动失败,记录错误日志
+ android.util.Log.e("AiKeyboard", "Failed to navigate to recharge fragment", e)
+ }
+ }
+
+ override fun applyTheme(
+ textColor: ColorStateList,
+ borderColor: ColorStateList,
+ backgroundColor: ColorStateList
+ ) {
+
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt
new file mode 100644
index 0000000..3a0ba1a
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt
@@ -0,0 +1,112 @@
+package com.example.myapplication.keyboard
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * 所有键盘的基础类:只处理文本颜色、边距 / 内边距 这些通用样式。
+ * 不再直接访问 resources,统一走 env.ctx.resources。
+ */
+abstract class BaseKeyboard(
+ protected val env: KeyboardEnvironment
+) {
+ abstract val rootView: View
+
+ /**
+ * 应用主题:文字颜色 + 边框(只调 margin/padding,不动你每个键的图片背景)
+ */
+ open fun applyTheme(
+ textColor: ColorStateList,
+ borderColor: ColorStateList,
+ backgroundColor: ColorStateList
+ ) {
+ applyTextColorToAllTextViews(rootView, textColor)
+ applyBorderToAllKeyViews(rootView)
+ }
+
+ // 文字颜色递归设置
+ protected fun applyTextColorToAllTextViews(root: View?, color: ColorStateList) {
+ if (root == null) return
+
+ fun dfs(v: View?) {
+ when (v) {
+ is TextView -> v.setTextColor(color)
+ is ViewGroup -> {
+ val childCount = v.childCount
+ var i = 0
+ while (i < childCount) {
+ dfs(v.getChildAt(i))
+ i++
+ }
+ }
+ }
+ }
+
+ dfs(root)
+ }
+
+ /**
+ * 只设置 margin / padding,不统一改背景,避免覆盖你用 ThemeManager 设置的按键图。
+ * 跟你原来 MyInputMethodService.applyBorderColorToAllKeys 的逻辑保持一致。
+ */
+ protected fun applyBorderToAllKeyViews(root: View?) {
+ if (root == null) return
+
+ val res = env.ctx.resources
+ val pkg = env.ctx.packageName
+
+ val keyMarginPx = 1.dpToPx()
+ val keyPaddingH = 6.dpToPx()
+
+ // 忽略 suggestion_0..20(联想栏),不改它们背景
+ val ignoredIds = HashSet().apply {
+ var i = 0
+ while (i <= 20) {
+ val id = res.getIdentifier("suggestion_$i", "id", pkg)
+ if (id != 0) add(id)
+ i++
+ }
+ }
+
+ fun dfs(v: View?) {
+ when (v) {
+ is TextView -> {
+ if (ignoredIds.contains(v.id)) {
+ // 联想词不加边距
+ return
+ }
+ val lp = v.layoutParams
+ if (lp is LinearLayout.LayoutParams) {
+ lp.setMargins(keyMarginPx, keyMarginPx, keyMarginPx, keyMarginPx)
+ v.layoutParams = lp
+ }
+ v.setPadding(
+ keyPaddingH,
+ v.paddingTop,
+ keyPaddingH,
+ v.paddingBottom
+ )
+ }
+ is ViewGroup -> {
+ val childCount = v.childCount
+ var i = 0
+ while (i < childCount) {
+ dfs(v.getChildAt(i))
+ i++
+ }
+ }
+ }
+ }
+
+ dfs(root)
+ }
+
+ // dp -> px 工具
+ protected fun Int.dpToPx(): Int {
+ val density = env.ctx.resources.displayMetrics.density
+ return (this * density + 0.5f).toInt()
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/KeyboardEnvironment.kt b/app/src/main/java/com/example/myapplication/keyboard/KeyboardEnvironment.kt
new file mode 100644
index 0000000..0227ee5
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/KeyboardEnvironment.kt
@@ -0,0 +1,38 @@
+package com.example.myapplication.keyboard
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.media.AudioManager
+import android.os.Handler
+import android.view.LayoutInflater
+
+interface KeyboardEnvironment {
+ val ctx: Context
+ val mainHandler: Handler
+ val audioManager: AudioManager
+
+ // 只保留 layoutInflater 的默认实现,避免跟 MyInputMethodService 冲突
+ val layoutInflater: LayoutInflater
+ get() = LayoutInflater.from(ctx)
+
+ // 主题色
+ val currentTextColor: ColorStateList
+ val currentBorderColor: ColorStateList
+ val currentBackgroundColor: ColorStateList
+
+ // 文本 / 联想
+ fun getCurrentWordPrefix(maxLen: Int = 128): String
+ fun updateCompletionsAndRender(prefix: String)
+ fun commitKey(c: Char)
+ fun deleteOne()
+ fun performSendAction()
+
+ // 键盘切换
+ fun showMainKeyboard()
+ fun showNumberKeyboard()
+ fun showSymbolKeyboard()
+ fun showAiKeyboard()
+
+ // 音效
+ fun playKeyClick()
+}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/MainKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/MainKeyboard.kt
new file mode 100644
index 0000000..192990b
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/MainKeyboard.kt
@@ -0,0 +1,314 @@
+package com.example.myapplication.keyboard
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.widget.PopupWindow
+import android.widget.TextView
+import com.example.myapplication.theme.ThemeManager
+
+class MainKeyboard(
+ env: KeyboardEnvironment,
+ private val swipeAltMap: Map,
+ /**
+ * 交给 MyInputMethodService 切换 Shift 状态,并返回最新状态
+ */
+ private val onToggleShift: () -> Boolean
+) : BaseKeyboard(env) {
+
+ override val rootView: View = env.layoutInflater.inflate(
+ env.ctx.resources.getIdentifier("keyboard", "layout", env.ctx.packageName),
+ null
+ )
+
+ private var isShiftOn: Boolean = false
+ private var keyPreviewPopup: PopupWindow? = null
+
+ // ======================== 震动相关 ========================
+
+ private val vibrator: Vibrator? =
+ env.ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
+
+ private fun vibrateKey(
+ duration: Long = 20L, // 时间:10~40 推荐
+ amplitude: Int = 150 // 1~255,100~150 最舒服
+ ) {
+ val v = vibrator ?: return
+ if (!v.hasVibrator()) return
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(
+ VibrationEffect.createOneShot(duration, amplitude)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ v.vibrate(duration)
+ }
+ } catch (_: SecurityException) {
+ // 没权限就自动静音,不崩溃
+ }
+ }
+
+ init {
+ applyPerKeyBackgroundForMainKeyboard(rootView)
+
+ applyTheme(
+ env.currentTextColor,
+ env.currentBorderColor,
+ env.currentBackgroundColor
+ )
+
+ setupListenersForMain(rootView)
+ }
+
+ // ======================== 背景图 ========================
+
+ private fun applyPerKeyBackgroundForMainKeyboard(root: View) {
+ // a..z 小写
+ var c = 'a'
+ while (c <= 'z') {
+ val idName = "key_$c"
+ applyKeyBackground(root, idName)
+ c++
+ }
+
+ // 键盘背景
+ applyKeyBackground(root, "background")
+
+ // 其他功能键
+ val others = listOf(
+ "key_space",
+ "key_send",
+ "key_del",
+ "key_up",
+ "key_123",
+ "key_ai"
+ )
+ for (idName in others) {
+ applyKeyBackground(root, idName)
+ }
+ }
+
+ private fun applyKeyBackground(
+ root: View,
+ viewIdName: String,
+ drawableName: String? = null
+ ) {
+ val res = env.ctx.resources
+ val viewId = res.getIdentifier(viewIdName, "id", env.ctx.packageName)
+ if (viewId == 0) return
+ val v = root.findViewById(viewId) ?: return
+
+ val keyName = drawableName ?: viewIdName
+ val rawDrawable = ThemeManager.getDrawableForKey(env.ctx, keyName) ?: return
+
+ if (viewIdName == "background") {
+ val scaled = scaleDrawableToHeight(rawDrawable, 243f)
+ v.background = scaled
+ return
+ }
+ v.background = rawDrawable
+ }
+
+ private fun scaleDrawableToHeight(src: Drawable, targetDp: Float): Drawable {
+ val res = env.ctx.resources
+ val dm = res.displayMetrics
+ val targetHeightPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ targetDp,
+ dm
+ ).toInt()
+
+ val bitmap = (src as? BitmapDrawable)?.bitmap ?: return src
+ val w = bitmap.width
+ val h = bitmap.height
+
+ val ratio = targetHeightPx.toFloat() / h
+ val targetWidthPx = (w * ratio).toInt()
+
+ val scaled = Bitmap.createScaledBitmap(bitmap, targetWidthPx, targetHeightPx, true)
+ return BitmapDrawable(res, scaled).apply {
+ setBounds(0, 0, targetWidthPx, targetHeightPx)
+ }
+ }
+
+ // ======================== 事件绑定 ========================
+
+ private fun setupListenersForMain(view: View) {
+ val res = env.ctx.resources
+ val pkg = env.ctx.packageName
+
+ // a..z:支持上滑副字符
+ var c = 'a'
+ while (c <= 'z') {
+ val id = res.getIdentifier("key_$c", "id", pkg)
+ val tv = view.findViewById(id)
+ if (tv != null) {
+ val baseChar = c
+ val altChar = swipeAltMap[baseChar]
+
+ attachKeyTouchWithSwipe(
+ tv,
+ normalCharProvider = { baseChar },
+ altCharProvider = altChar?.let { ac ->
+ { ac }
+ }
+ )
+ }
+ c++
+ }
+
+ // space
+ view.findViewById(res.getIdentifier("key_space", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.commitKey(' ')
+ }
+
+ // Shift
+ val shiftId = res.getIdentifier("key_up", "id", pkg)
+ view.findViewById(shiftId)?.setOnClickListener {
+ vibrateKey()
+ isShiftOn = onToggleShift()
+ it.isActivated = isShiftOn
+ updateKeyBackgroundsForLetters(view)
+ }
+
+ // 删除(单击;长按由 MyInputMethodService 挂)
+ view.findViewById(res.getIdentifier("key_del", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.deleteOne()
+ }
+
+ // 切换数字键盘
+ view.findViewById(res.getIdentifier("key_123", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showNumberKeyboard()
+ }
+
+ // 跳 AI
+ view.findViewById(res.getIdentifier("key_ai", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showAiKeyboard()
+ }
+
+ // 发送
+ view.findViewById(res.getIdentifier("key_send", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.performSendAction()
+ }
+ }
+
+ // Shift 后更新字母按键背景(key_a vs key_a_up)
+ private fun updateKeyBackgroundsForLetters(root: View) {
+ var c = 'a'
+ while (c <= 'z') {
+ val idName = "key_$c"
+ val drawableName = if (isShiftOn) "${idName}_up" else idName
+ applyKeyBackground(root, idName, drawableName)
+ c++
+ }
+
+ val upKeyIdName = "key_up"
+ val upDrawableName = if (isShiftOn) "key_up_upper" else "key_up"
+ applyKeyBackground(root, upKeyIdName, upDrawableName)
+ }
+
+ // ======================== 触摸 + 预览 ========================
+
+ private fun attachKeyTouchWithSwipe(
+ view: View,
+ normalCharProvider: () -> Char,
+ altCharProvider: (() -> Char)?
+ ) {
+ val swipeThreshold = 20.dpToPx().toFloat()
+ var downY = 0f
+ var isAlt = false
+ var currentChar: Char = '\u0000'
+
+ view.setOnTouchListener { v, event ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ downY = event.rawY
+ isAlt = false
+ currentChar = normalCharProvider()
+ vibrateKey() // 按下就震
+ showKeyPreview(v, currentChar.toString())
+ v.isPressed = true
+ true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (altCharProvider != null) {
+ val dy = event.rawY - downY
+ val shouldAlt = dy < -swipeThreshold
+ if (shouldAlt != isAlt) {
+ isAlt = shouldAlt
+ currentChar = if (isAlt) altCharProvider() else normalCharProvider()
+ showKeyPreview(v, currentChar.toString())
+ }
+ }
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ env.commitKey(currentChar)
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_OUTSIDE -> {
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ else -> false
+ }
+ }
+ }
+
+ private fun showKeyPreview(anchor: View, text: String) {
+ keyPreviewPopup?.dismiss()
+
+ val tv = TextView(env.ctx).apply {
+ this.text = text
+ textSize = 26f
+ setTextColor(Color.BLACK)
+ setPadding(20, 10, 20, 10)
+ background = GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = 16f
+ setColor(Color.WHITE)
+ setStroke(1, Color.parseColor("#33000000"))
+ }
+ gravity = Gravity.CENTER
+ }
+
+ val w = (anchor.width * 1.2f).toInt()
+ val h = (anchor.height * 1.2f).toInt()
+
+ keyPreviewPopup = PopupWindow(tv, w, h, false).apply {
+ isClippingEnabled = false
+ }
+
+ keyPreviewPopup?.showAsDropDown(
+ anchor,
+ -(w - anchor.width) / 2,
+ -(h + anchor.height * 1.1f).toInt()
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/NumberKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/NumberKeyboard.kt
new file mode 100644
index 0000000..dd84b52
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/NumberKeyboard.kt
@@ -0,0 +1,311 @@
+package com.example.myapplication.keyboard
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.widget.PopupWindow
+import android.widget.TextView
+import com.example.myapplication.R
+import com.example.myapplication.theme.ThemeManager
+
+class NumberKeyboard(
+ env: KeyboardEnvironment
+) : BaseKeyboard(env) {
+
+ override val rootView: View = env.layoutInflater.inflate(
+ env.ctx.resources.getIdentifier("number_keyboard", "layout", env.ctx.packageName),
+ null
+ )
+
+ private var keyPreviewPopup: PopupWindow? = null
+
+ // ================= 震动相关 =================
+
+ private val vibrator: Vibrator? =
+ env.ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
+
+ private fun vibrateKey(
+ duration: Long = 20L, // 时间:10~40 推荐
+ amplitude: Int = 150 // 1~255,100~150 最舒服
+ ) {
+ val v = vibrator ?: return
+ if (!v.hasVibrator()) return
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(
+ VibrationEffect.createOneShot(duration, amplitude)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ v.vibrate(duration)
+ }
+ } catch (_: SecurityException) {
+ // 没权限就自动静音,不崩溃
+ }
+ }
+
+ init {
+ applyPerKeyBackgroundForNumberKeyboard(rootView)
+
+ // 初次创建立刻应用当前主题
+ applyTheme(
+ env.currentTextColor,
+ env.currentBorderColor,
+ env.currentBackgroundColor
+ )
+
+ setupListenersForNumberView(rootView)
+ }
+
+ // ================= 背景(完全拷贝原逻辑,只是换成 env.ctx.resources) =================
+
+ private fun applyPerKeyBackgroundForNumberKeyboard(root: View) {
+ val res = env.ctx.resources
+
+ // 0..9
+ for (i in 0..9) {
+ val idName = "key_$i"
+ applyKeyBackground(root, idName)
+ }
+
+ // 背景
+ applyKeyBackground(root, "background")
+
+ // 符号键
+ 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 applyKeyBackground(
+ root: View,
+ viewIdName: String,
+ drawableName: String? = null
+ ) {
+ val res = env.ctx.resources
+ val viewId = res.getIdentifier(viewIdName, "id", env.ctx.packageName)
+ if (viewId == 0) return
+ val v = root.findViewById(viewId) ?: return
+
+ val keyName = drawableName ?: viewIdName
+ val rawDrawable = ThemeManager.getDrawableForKey(env.ctx, keyName) ?: return
+
+ if (viewIdName == "background") {
+ val scaled = scaleDrawableToHeight(rawDrawable, 243f)
+ v.background = scaled
+ return
+ }
+ v.background = rawDrawable
+ }
+
+ private fun scaleDrawableToHeight(src: Drawable, targetDp: Float): Drawable {
+ val res = env.ctx.resources
+ val dm = res.displayMetrics
+ val targetHeightPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ targetDp,
+ dm
+ ).toInt()
+
+ val bitmap = (src as? BitmapDrawable)?.bitmap ?: return src
+ val w = bitmap.width
+ val h = bitmap.height
+
+ val ratio = targetHeightPx.toFloat() / h
+ val targetWidthPx = (w * ratio).toInt()
+
+ val scaledBitmap = Bitmap.createScaledBitmap(
+ bitmap,
+ targetWidthPx,
+ targetHeightPx,
+ true
+ )
+ return BitmapDrawable(res, scaledBitmap).apply {
+ setBounds(0, 0, targetWidthPx, targetHeightPx)
+ }
+ }
+
+ // ================= 按键事件 =================
+
+ private fun setupListenersForNumberView(numView: View) {
+ val res = env.ctx.resources
+ val pkg = env.ctx.packageName
+
+ // 0~9
+ for (i in 0..9) {
+ val id = res.getIdentifier("key_$i", "id", pkg)
+ numView.findViewById(id)?.let { v ->
+ attachKeyTouch(v) { i.toString()[0] }
+ }
+ }
+
+ // 符号键
+ val symbolMap: List> = listOf(
+ "key_comma" to ',',
+ "key_dot" to '.',
+ "key_minus" to '-',
+ "key_slash" to '/',
+ "key_colon" to ':',
+ "key_semicolon" to ';',
+ "key_paren_l" to '(',
+ "key_paren_r" to ')',
+ "key_dollar" to '$',
+ "key_amp" to '&',
+ "key_at" to '@',
+ "key_question" to '?',
+ "key_exclam" to '!',
+ "key_quote" to '\'',
+ "key_quote_d" to '”'
+ )
+ symbolMap.forEach { (name, ch) ->
+ val id = res.getIdentifier(name, "id", pkg)
+ numView.findViewById(id)?.let { v ->
+ attachKeyTouch(v) { ch }
+ }
+ }
+
+ // 切换:符号层
+ numView.findViewById(res.getIdentifier("key_symbols_more", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showSymbolKeyboard()
+ }
+
+ // 切回字母
+ numView.findViewById(res.getIdentifier("key_abc", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showMainKeyboard()
+ }
+
+ // 跳 AI
+ numView.findViewById(res.getIdentifier("key_ai", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showAiKeyboard()
+ }
+
+ // 空格
+ numView.findViewById(res.getIdentifier("key_space", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.commitKey(' ')
+ }
+
+ // 发送
+ numView.findViewById(res.getIdentifier("key_send", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.performSendAction()
+ }
+
+ // 删除(单击;长按连删在 MyInputMethodService 里挂)
+ numView.findViewById(res.getIdentifier("key_del", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.deleteOne()
+ }
+ }
+
+ // ================= 按键触摸 & 预览 =================
+
+ private fun attachKeyTouch(view: View, charProvider: () -> Char) {
+ view.setOnTouchListener { v, event ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ val ch = charProvider()
+ vibrateKey() // 按下就震一下
+ showKeyPreview(v, ch.toString())
+ v.isPressed = true
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ val ch = charProvider()
+ env.commitKey(ch)
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_OUTSIDE -> {
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ else -> false
+ }
+ }
+ }
+
+ private fun showKeyPreview(anchor: View, text: String) {
+ keyPreviewPopup?.dismiss()
+
+ val tv = TextView(env.ctx).apply {
+ this.text = text
+ textSize = 26f
+ setTextColor(Color.BLACK)
+ setPadding(20, 10, 20, 10)
+ background = GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = 16f
+ setColor(Color.WHITE)
+ setStroke(1, Color.parseColor("#33000000"))
+ }
+ gravity = Gravity.CENTER
+ }
+
+ val w = (anchor.width * 1.2f).toInt()
+ val h = (anchor.height * 1.2f).toInt()
+
+ keyPreviewPopup = PopupWindow(tv, w, h, false).apply {
+ isClippingEnabled = false
+ }
+
+ keyPreviewPopup?.showAsDropDown(
+ anchor,
+ -(w - anchor.width) / 2,
+ -(h + anchor.height * 1.1f).toInt()
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/SymbolKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/SymbolKeyboard.kt
new file mode 100644
index 0000000..65d2cba
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/keyboard/SymbolKeyboard.kt
@@ -0,0 +1,329 @@
+package com.example.myapplication.keyboard
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.widget.PopupWindow
+import android.widget.TextView
+import com.example.myapplication.R
+import com.example.myapplication.theme.ThemeManager
+
+class SymbolKeyboard(
+ env: KeyboardEnvironment
+) : BaseKeyboard(env) {
+
+ override val rootView: View = env.layoutInflater.inflate(
+ env.ctx.resources.getIdentifier("symbol_keyboard", "layout", env.ctx.packageName),
+ null
+ )
+
+ private var keyPreviewPopup: PopupWindow? = null
+
+ // ================== 震动相关 ==================
+
+ private val vibrator: Vibrator? =
+ env.ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
+
+ private fun vibrateKey(
+ duration: Long = 20L, // 时间:10~40 推荐
+ amplitude: Int = 150 // 1~255,100~150 最舒服
+ ) {
+ val v = vibrator ?: return
+ if (!v.hasVibrator()) return
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(
+ VibrationEffect.createOneShot(duration, amplitude)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ v.vibrate(duration)
+ }
+ } catch (_: SecurityException) {
+ // 没权限就自动静音,不崩溃
+ }
+ }
+
+ init {
+ // 按键背景图片(跟你原来 applyPerKeyBackgroundForSymbolKeyboard 一样)
+ applyPerKeyBackgroundForSymbolKeyboard(rootView)
+
+ // 初次创建立刻应用当前主题色
+ applyTheme(
+ env.currentTextColor,
+ env.currentBorderColor,
+ env.currentBackgroundColor
+ )
+
+ setupListenersForSymbolView(rootView)
+ }
+
+ // ================== 背景(完全按你原来的 key 列表) ==================
+
+ private fun applyPerKeyBackgroundForSymbolKeyboard(root: View) {
+ val res = env.ctx.resources
+
+ 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)
+ }
+
+ // 背景整体
+ applyKeyBackground(root, "background")
+
+ // 功能键
+ val others = listOf(
+ "key_symbols_123",
+ "key_backspace",
+ "key_abc",
+ "key_ai",
+ "key_space",
+ "key_send"
+ )
+
+ others.forEach { idName ->
+ applyKeyBackground(root, idName)
+ }
+ }
+
+ private fun applyKeyBackground(
+ root: View,
+ viewIdName: String,
+ drawableName: String? = null
+ ) {
+ val res = env.ctx.resources
+ val viewId = res.getIdentifier(viewIdName, "id", env.ctx.packageName)
+ if (viewId == 0) return
+ val v = root.findViewById(viewId) ?: return
+
+ val keyName = drawableName ?: viewIdName
+ val rawDrawable = ThemeManager.getDrawableForKey(env.ctx, keyName) ?: return
+
+ if (viewIdName == "background") {
+ val scaled = scaleDrawableToHeight(rawDrawable, 243f)
+ v.background = scaled
+ return
+ }
+ v.background = rawDrawable
+ }
+
+ private fun scaleDrawableToHeight(src: Drawable, targetDp: Float): Drawable {
+ val res = env.ctx.resources
+ val dm = res.displayMetrics
+ val targetHeightPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ targetDp,
+ dm
+ ).toInt()
+
+ val bitmap = (src as? BitmapDrawable)?.bitmap ?: return src
+ val w = bitmap.width
+ val h = bitmap.height
+
+ val ratio = targetHeightPx.toFloat() / h
+ val targetWidthPx = (w * ratio).toInt()
+
+ val scaledBitmap = Bitmap.createScaledBitmap(
+ bitmap,
+ targetWidthPx,
+ targetHeightPx,
+ true
+ )
+ return BitmapDrawable(res, scaledBitmap).apply {
+ setBounds(0, 0, targetWidthPx, targetHeightPx)
+ }
+ }
+
+ // ================== 符号键盘事件 ==================
+
+ private fun setupListenersForSymbolView(symView: View) {
+ val res = env.ctx.resources
+ val pkg = env.ctx.packageName
+
+ val pairs = listOf(
+ // 第一行
+ "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 '^',
+ "key_asterisk" to '*',
+ "key_plus" to '+',
+ "key_equal" to '=',
+
+ // 第二行
+ "key_underscore" to '_',
+ "key_backslash" to '\\',
+ "key_pipe" to '|',
+ "key_tilde" to '~',
+ "key_lt" to '<',
+ "key_gt" to '>',
+ "key_euro" to '€',
+ "key_pound" to '£',
+ "key_money" to '¥',
+ "key_bullet" to '•',
+
+ // 第三行
+ "key_dot" to '.',
+ "key_comma" to ',',
+ "key_question" to '?',
+ "key_exclam" to '!',
+ "key_quote" to '\''
+ )
+
+ pairs.forEach { (name, ch) ->
+ val id = res.getIdentifier(name, "id", pkg)
+ symView.findViewById(id)?.let { v ->
+ attachKeyTouch(v) { ch }
+ }
+ }
+
+ // 切换回数字
+ symView.findViewById(res.getIdentifier("key_symbols_123", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showNumberKeyboard()
+ }
+
+ // 切回字母
+ symView.findViewById(res.getIdentifier("key_abc", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showMainKeyboard()
+ }
+
+ // 空格
+ symView.findViewById(res.getIdentifier("key_space", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.commitKey(' ')
+ }
+
+ // 发送
+ symView.findViewById(res.getIdentifier("key_send", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.performSendAction()
+ }
+
+ // 删除(单击;长按连删在 MyInputMethodService 里统一挂)
+ symView.findViewById(res.getIdentifier("key_backspace", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.deleteOne()
+ }
+
+ // 跳 AI 键盘
+ symView.findViewById(res.getIdentifier("key_ai", "id", pkg))
+ ?.setOnClickListener {
+ vibrateKey()
+ env.showAiKeyboard()
+ }
+ }
+
+ // ================== 触摸 + 预览 ==================
+
+ private fun attachKeyTouch(view: View, charProvider: () -> Char) {
+ view.setOnTouchListener { v, event ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ val ch = charProvider()
+ vibrateKey() // 按下震动
+ showKeyPreview(v, ch.toString())
+ v.isPressed = true
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ val ch = charProvider()
+ env.commitKey(ch)
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_OUTSIDE -> {
+ keyPreviewPopup?.dismiss()
+ v.isPressed = false
+ true
+ }
+ else -> false
+ }
+ }
+ }
+
+ private fun showKeyPreview(anchor: View, text: String) {
+ keyPreviewPopup?.dismiss()
+
+ val tv = TextView(env.ctx).apply {
+ this.text = text
+ textSize = 26f
+ setTextColor(Color.BLACK)
+ setPadding(20, 10, 20, 10)
+ background = GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = 16f
+ setColor(Color.WHITE)
+ setStroke(1, Color.parseColor("#33000000"))
+ }
+ gravity = Gravity.CENTER
+ }
+
+ val w = (anchor.width * 1.2f).toInt()
+ val h = (anchor.height * 1.2f).toInt()
+
+ keyPreviewPopup = PopupWindow(tv, w, h, false).apply {
+ isClippingEnabled = false
+ }
+
+ keyPreviewPopup?.showAsDropDown(
+ anchor,
+ -(w - anchor.width) / 2,
+ -(h + anchor.height * 1.1f).toInt()
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/network/ApiResponse.kt b/app/src/main/java/com/example/myapplication/network/ApiResponse.kt
new file mode 100644
index 0000000..3530708
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/ApiResponse.kt
@@ -0,0 +1,8 @@
+// ApiResponse.kt
+package com.example.network
+
+data class ApiResponse(
+ val code: Int,
+ val msg: String,
+ val data: T?
+)
diff --git a/app/src/main/java/com/example/myapplication/network/ApiService.kt b/app/src/main/java/com/example/myapplication/network/ApiService.kt
new file mode 100644
index 0000000..bd8e792
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/ApiService.kt
@@ -0,0 +1,43 @@
+// 请求方法
+package com.example.network
+
+import okhttp3.ResponseBody
+import retrofit2.Response
+import retrofit2.http.*
+
+interface ApiService {
+
+ // GET 示例:/users/{id}
+ @GET("users/{id}")
+ suspend fun getUser(
+ @Path("id") id: String
+ ): ApiResponse
+
+ // GET 示例:带查询参数 /users?page=1&pageSize=20
+ @GET("users")
+ suspend fun getUsers(
+ @Query("page") page: Int,
+ @Query("pageSize") pageSize: Int
+ ): ApiResponse>
+
+ // POST JSON 示例:Body 为 JSON:{"username": "...", "password": "..."}
+ @POST("auth/login")
+ suspend fun login(
+ @Body body: LoginRequest
+ ): ApiResponse
+
+ // POST 表单示例:x-www-form-urlencoded
+ @FormUrlEncoded
+ @POST("auth/loginForm")
+ suspend fun loginForm(
+ @Field("username") username: String,
+ @Field("password") password: String
+ ): ApiResponse
+
+ // zip 文件下载(或其它大文件)——必须 @Streaming
+ @Streaming
+ @GET("files/{fileName}")
+ suspend fun downloadZip(
+ @Path("fileName") fileName: String // 比如 "xxx.zip"
+ ): Response
+}
diff --git a/app/src/main/java/com/example/myapplication/network/FileDownloader.kt b/app/src/main/java/com/example/myapplication/network/FileDownloader.kt
new file mode 100644
index 0000000..a6400ed
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/FileDownloader.kt
@@ -0,0 +1,86 @@
+// zip 文件下载器
+package com.example.network
+
+import android.content.Context
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.ResponseBody
+import java.io.File
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+
+object FileDownloader {
+
+ /**
+ * 下载 zip 文件并保存到 app 专属目录
+ * @param context 用来获取文件目录
+ * @param remoteFileName 服务器上的文件名,比如 "test.zip"
+ * @param localFileName 本地保存名字,比如 "test_local.zip"
+ * @return 保存成功后返回 File,失败返回 null
+ */
+ suspend fun downloadZipFile(
+ context: Context,
+ remoteFileName: String,
+ localFileName: String = remoteFileName
+ ): File? = withContext(Dispatchers.IO) {
+ val api = RetrofitClient.apiService
+ try {
+ val response = api.downloadZip(remoteFileName)
+ if (!response.isSuccessful) {
+ Log.e("Downloader", "download failed: code=${response.code()}")
+ return@withContext null
+ }
+
+ val body = response.body() ?: run {
+ Log.e("Downloader", "response body is null")
+ return@withContext null
+ }
+
+ val dir = File(context.getExternalFilesDir(null), "Downloads")
+ if (!dir.exists()) dir.mkdirs()
+
+ val outFile = File(dir, localFileName)
+
+ saveResponseBodyToFile(body, outFile)
+
+ Log.d("Downloader", "file saved to: ${outFile.absolutePath}")
+ return@withContext outFile
+
+ } catch (e: Exception) {
+ Log.e("Downloader", "download exception: ${e.message}", e)
+ return@withContext null
+ }
+ }
+
+ /**
+ * 把 ResponseBody 写入文件
+ */
+ private fun saveResponseBodyToFile(body: ResponseBody, outFile: File) {
+ var inputStream: InputStream? = null
+ var outputStream: OutputStream? = null
+ try {
+ val buffer = ByteArray(8 * 1024)
+ var downloadedBytes: Long = 0
+ val totalBytes = body.contentLength()
+
+ inputStream = body.byteStream()
+ outputStream = FileOutputStream(outFile)
+
+ while (true) {
+ val read = inputStream.read(buffer)
+ if (read == -1) break
+ outputStream.write(buffer, 0, read)
+ downloadedBytes += read
+
+ // 需要的话可以在这里回调进度
+ val progress = downloadedBytes * 100 / totalBytes
+ }
+ outputStream.flush()
+ } finally {
+ inputStream?.close()
+ outputStream?.close()
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt b/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt
new file mode 100644
index 0000000..fdb2a49
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt
@@ -0,0 +1,53 @@
+// 定义请求 & 响应拦截器
+package com.example.network
+
+import android.util.Log
+import okhttp3.Interceptor
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+
+/**
+ * 请求拦截器:统一加 Header、token 等
+ */
+val requestInterceptor = Interceptor { chain ->
+ val original = chain.request()
+ val token = "your_token" // 这里换成你自己的 token
+ val newRequest = original.newBuilder()
+ .addHeader("Accept", "application/json")
+ .addHeader("Content-Type", "application/json")
+ // 这里加你自己的 token,如果没有就注释掉
+ .addHeader("Authorization", "Bearer $token")
+ .build()
+
+ chain.proceed(newRequest)
+}
+
+/**
+ * 响应拦截器:统一打印日志、做一些简单的错误处理
+ */
+val responseInterceptor = Interceptor { chain ->
+ val request = chain.request()
+ val startNs = System.nanoTime()
+ val response: Response = chain.proceed(request)
+ val tookMs = (System.nanoTime() - startNs) / 1_000_000
+
+ val rawBody = response.body
+ val mediaType = rawBody?.contentType()
+ val bodyString = rawBody?.string() ?: ""
+
+ Log.d(
+ "HTTP",
+ "⬇⬇⬇\n" +
+ "URL : ${request.url}\n" +
+ "Method: ${request.method}\n" +
+ "Code : ${response.code}\n" +
+ "Time : ${tookMs}ms\n" +
+ "Body : $bodyString\n" +
+ "⬆⬆⬆"
+ )
+
+ // body 只能读一次,这里读完后再重新构建一个
+ response.newBuilder()
+ .body(bodyString.toResponseBody(mediaType))
+ .build()
+}
diff --git a/app/src/main/java/com/example/myapplication/network/Models.kt b/app/src/main/java/com/example/myapplication/network/Models.kt
new file mode 100644
index 0000000..1701394
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/Models.kt
@@ -0,0 +1,18 @@
+// Models.kt
+package com.example.network
+
+data class User(
+ val id: String,
+ val name: String,
+ val age: Int
+)
+
+data class LoginRequest(
+ val username: String,
+ val password: String
+)
+
+data class LoginResponse(
+ val token: String,
+ val user: User
+)
diff --git a/app/src/main/java/com/example/myapplication/network/RetrofitClient.kt b/app/src/main/java/com/example/myapplication/network/RetrofitClient.kt
new file mode 100644
index 0000000..af5d3a3
--- /dev/null
+++ b/app/src/main/java/com/example/myapplication/network/RetrofitClient.kt
@@ -0,0 +1,44 @@
+// RetrofitClient.kt
+package com.example.network
+
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+object RetrofitClient {
+
+ private const val BASE_URL = "https://api.example.com/" // 换成你的地址
+
+ // 日志拦截器(可选)
+ private val loggingInterceptor = HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+ private val okHttpClient: OkHttpClient by lazy {
+ OkHttpClient.Builder()
+ // 超时时间自己看需求改
+ .connectTimeout(15, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+
+ // 顺序:请求拦截 -> logging -> 响应拦截
+ .addInterceptor(requestInterceptor)
+ .addInterceptor(loggingInterceptor)
+ .addInterceptor(responseInterceptor)
+ .build()
+ }
+
+ private val retrofit: Retrofit by lazy {
+ Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ val apiService: ApiService by lazy {
+ retrofit.create(ApiService::class.java)
+ }
+}
diff --git a/app/src/main/java/com/example/myapplication/ui/home/HomeFragment.kt b/app/src/main/java/com/example/myapplication/ui/home/HomeFragment.kt
index 01131b8..f83f237 100644
--- a/app/src/main/java/com/example/myapplication/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/example/myapplication/ui/home/HomeFragment.kt
@@ -24,6 +24,7 @@ import android.widget.HorizontalScrollView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import kotlin.math.abs
import android.content.Intent
+import com.example.myapplication.ImeGuideActivity
class HomeFragment : Fragment() {
@@ -63,6 +64,10 @@ class HomeFragment : Fragment() {
view.findViewById(R.id.rechargeButton).setOnClickListener {
findNavController().navigate(R.id.action_global_rechargeFragment)
}
+ //输入法激活跳转
+ view.findViewById(R.id.floatingImage).setOnClickListener {
+ startActivity(Intent(requireContext(), ImeGuideActivity::class.java))
+ }
scrim = view.findViewById(R.id.view_scrim)
bottomSheet = view.findViewById(R.id.bottomSheet)
diff --git a/app/src/main/res/drawable/btn_keyboard.xml b/app/src/main/res/drawable/btn_keyboard.xml
index 41b8ceb..aa2962b 100644
--- a/app/src/main/res/drawable/btn_keyboard.xml
+++ b/app/src/main/res/drawable/btn_keyboard.xml
@@ -3,18 +3,14 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/copy_the_message.png b/app/src/main/res/drawable/copy_the_message.png
new file mode 100644
index 0000000..e7a052e
Binary files /dev/null and b/app/src/main/res/drawable/copy_the_message.png differ
diff --git a/app/src/main/res/drawable/default_avatar.png b/app/src/main/res/drawable/default_avatar.png
new file mode 100644
index 0000000..9e1955c
Binary files /dev/null and b/app/src/main/res/drawable/default_avatar.png differ
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9..ca3826a 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -1,170 +1,74 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ime_guide_activity_btn_completed.xml b/app/src/main/res/drawable/ime_guide_activity_btn_completed.xml
new file mode 100644
index 0000000..1c04006
--- /dev/null
+++ b/app/src/main/res/drawable/ime_guide_activity_btn_completed.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ime_guide_activity_btn_completed_img.png b/app/src/main/res/drawable/ime_guide_activity_btn_completed_img.png
new file mode 100644
index 0000000..2466287
Binary files /dev/null and b/app/src/main/res/drawable/ime_guide_activity_btn_completed_img.png differ
diff --git a/app/src/main/res/drawable/ime_guide_activity_btn_unfinished.xml b/app/src/main/res/drawable/ime_guide_activity_btn_unfinished.xml
new file mode 100644
index 0000000..aba31ae
--- /dev/null
+++ b/app/src/main/res/drawable/ime_guide_activity_btn_unfinished.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ime_guide_activity_btn_unfinished_img.png b/app/src/main/res/drawable/ime_guide_activity_btn_unfinished_img.png
new file mode 100644
index 0000000..146f58b
Binary files /dev/null and b/app/src/main/res/drawable/ime_guide_activity_btn_unfinished_img.png differ
diff --git a/app/src/main/res/drawable/jump_to_the_keyboard.png b/app/src/main/res/drawable/jump_to_the_keyboard.png
new file mode 100644
index 0000000..7bfee1a
Binary files /dev/null and b/app/src/main/res/drawable/jump_to_the_keyboard.png differ
diff --git a/app/src/main/res/drawable/jump_to_vip.png b/app/src/main/res/drawable/jump_to_vip.png
new file mode 100644
index 0000000..861a449
Binary files /dev/null and b/app/src/main/res/drawable/jump_to_vip.png differ
diff --git a/app/src/main/res/drawable/keyboard_button_bg1.xml b/app/src/main/res/drawable/keyboard_button_bg1.xml
new file mode 100644
index 0000000..9a4547a
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_button_bg1.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_button_bg2.xml b/app/src/main/res/drawable/keyboard_button_bg2.xml
new file mode 100644
index 0000000..829a810
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_button_bg2.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_button_bg3.xml b/app/src/main/res/drawable/keyboard_button_bg3.xml
new file mode 100644
index 0000000..76a2433
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_button_bg3.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_icon_voice.png b/app/src/main/res/drawable/keyboard_icon_voice.png
new file mode 100644
index 0000000..7bce8e1
Binary files /dev/null and b/app/src/main/res/drawable/keyboard_icon_voice.png differ
diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png
new file mode 100644
index 0000000..056c316
Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ
diff --git a/app/src/main/res/layout/activity_guide.xml b/app/src/main/res/layout/activity_guide.xml
new file mode 100644
index 0000000..2034a6b
--- /dev/null
+++ b/app/src/main/res/layout/activity_guide.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_ime_guide.xml b/app/src/main/res/layout/activity_ime_guide.xml
new file mode 100644
index 0000000..333f8e6
--- /dev/null
+++ b/app/src/main/res/layout/activity_ime_guide.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_recharge.xml b/app/src/main/res/layout/activity_recharge.xml
index 8b8cbc0..2372f67 100644
--- a/app/src/main/res/layout/activity_recharge.xml
+++ b/app/src/main/res/layout/activity_recharge.xml
@@ -221,7 +221,7 @@
+ android:src="@drawable/default_avatar" />
+ android:orientation="vertical">
+
+
-
-
+
+
+ android:id="@+id/keyboard_row_1"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_page_list1.xml b/app/src/main/res/layout/bottom_page_list1.xml
index 8b4cd3b..f3fc7a5 100644
--- a/app/src/main/res/layout/bottom_page_list1.xml
+++ b/app/src/main/res/layout/bottom_page_list1.xml
@@ -32,7 +32,7 @@
android:layout_width="67dp"
android:layout_height="67dp"
android:elevation="4dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
app:civ_border_color="#DFF346"
app:civ_border_width="2dp" />
@@ -69,7 +69,7 @@
android:layout_width="67dp"
android:layout_height="67dp"
android:elevation="4dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
app:civ_border_width="2dp"
app:civ_border_color="#DFF346" />
@@ -106,7 +106,7 @@
android:layout_width="67dp"
android:layout_height="67dp"
android:elevation="4dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
app:civ_border_width="2dp"
app:civ_border_color="#DFF346" />
@@ -165,7 +165,7 @@
android:layout_weight="1"
android:layout_marginEnd="10dp"
android:elevation="4dp"
- android:src="@drawable/bg"/>
+ android:src="@drawable/default_avatar"/>
diff --git a/app/src/main/res/layout/fragment_mine.xml b/app/src/main/res/layout/fragment_mine.xml
index 3d068bc..fb2da8b 100644
--- a/app/src/main/res/layout/fragment_mine.xml
+++ b/app/src/main/res/layout/fragment_mine.xml
@@ -81,7 +81,7 @@
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginStart="5dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
android:clickable="true"
android:focusable="true"/>
diff --git a/app/src/main/res/layout/keyboard.xml b/app/src/main/res/layout/keyboard.xml
index 1d40b29..c3c753f 100644
--- a/app/src/main/res/layout/keyboard.xml
+++ b/app/src/main/res/layout/keyboard.xml
@@ -25,232 +25,232 @@
android:id="@+id/suggestion_0"
android:layout_width="wrap_content"
android:layout_height="match_parent"
- android:textSize="12sp"
+ android:textSize="16sp"
android:paddingHorizontal="12dp"
android:gravity="center"
android:clickable="true"
android:background="@drawable/btn_keyboard"
- android:textColor="?android:attr/textColorPrimary" />
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
diff --git a/app/src/main/res/layout/my_keyboard.xml b/app/src/main/res/layout/my_keyboard.xml
index 23e7381..b506bd0 100644
--- a/app/src/main/res/layout/my_keyboard.xml
+++ b/app/src/main/res/layout/my_keyboard.xml
@@ -82,7 +82,7 @@
android:id="@+id/avatar"
android:layout_width="20dp"
android:layout_height="20dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
android:clickable="true"
android:focusable="true"/>
diff --git a/app/src/main/res/layout/my_skin.xml b/app/src/main/res/layout/my_skin.xml
index b646890..9b4d2d6 100644
--- a/app/src/main/res/layout/my_skin.xml
+++ b/app/src/main/res/layout/my_skin.xml
@@ -81,7 +81,7 @@
android:layout_width="match_parent"
android:layout_height="127dp"
android:scaleType="centerCrop"
- android:src="@drawable/bg" />
+ android:src="@drawable/default_avatar" />
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
diff --git a/app/src/main/res/layout/personal_settings.xml b/app/src/main/res/layout/personal_settings.xml
index 7520ff3..26be5c1 100644
--- a/app/src/main/res/layout/personal_settings.xml
+++ b/app/src/main/res/layout/personal_settings.xml
@@ -62,7 +62,7 @@
android:id="@+id/avatar"
android:layout_width="88dp"
android:layout_height="88dp"
- android:src="@drawable/bg"
+ android:src="@drawable/default_avatar"
android:elevation="1dp"
android:clickable="true"
android:focusable="true"/>
diff --git a/app/src/main/res/layout/symbol_keyboard.xml b/app/src/main/res/layout/symbol_keyboard.xml
index c18b3ab..0ca16da 100644
--- a/app/src/main/res/layout/symbol_keyboard.xml
+++ b/app/src/main/res/layout/symbol_keyboard.xml
@@ -30,7 +30,7 @@
android:gravity="center"
android:clickable="true"
android:background="@drawable/btn_keyboard"
- android:textColor="?android:attr/textColorPrimary" />
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
+ android:textColor="#3C3C3C"/>
diff --git a/app/src/main/res/menu/menu_bottom_nav.xml b/app/src/main/res/menu/menu_bottom_nav.xml
index 9a02f17..e6e632c 100644
--- a/app/src/main/res/menu/menu_bottom_nav.xml
+++ b/app/src/main/res/menu/menu_bottom_nav.xml
@@ -11,10 +11,10 @@
android:icon="@drawable/shop_selector"
android:title="Shop" />
-
+ android:title="Circle" /> -->
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755..c4a603d 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index c209e78..7157b5e 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..746edb9
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index b2dfe3d..3c9347e 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4f0f1d6..6f6be7f 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..d5f110b
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 62b611d..de84ba0 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 948a307..24c9837 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..0fdad13
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 1b9a695..850ae67 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 28d4b77..142577c 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..ada29be
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 9287f50..b427138 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index aa7d642..9f89a51 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..8e7394c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 9126ae3..458f705 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/xml/method.xml b/app/src/main/res/xml/method.xml
index 19b00fe..9e2c140 100644
--- a/app/src/main/res/xml/method.xml
+++ b/app/src/main/res/xml/method.xml
@@ -1,5 +1,7 @@
-
+
+