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 @@ - + +