diff --git a/app/src/main/java/com/example/myapplication/GuideActivity.kt b/app/src/main/java/com/example/myapplication/GuideActivity.kt
index 650446a..f6477b7 100644
--- a/app/src/main/java/com/example/myapplication/GuideActivity.kt
+++ b/app/src/main/java/com/example/myapplication/GuideActivity.kt
@@ -209,7 +209,7 @@ class GuideActivity : AppCompatActivity() {
}, 1500)
scrollView.postDelayed({
- titleTextView.text = "The other party is typing..."
+ titleTextView.text = getString(R.string.currently_inputting)
}, 500)
inputMessage.isFocusable = true
inputMessage.isFocusableInTouchMode = true
diff --git a/app/src/main/java/com/example/myapplication/MyInputMethodService.kt b/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
index 4162190..60055e4 100644
--- a/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
+++ b/app/src/main/java/com/example/myapplication/MyInputMethodService.kt
@@ -69,6 +69,10 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
private val wordDictionary = WordDictionary(this) // 词库
private var currentInput = StringBuilder() // 当前输入前缀
private var completionSuggestions = emptyList() // 自动完成建议
+ private val suggestionViews = mutableListOf() // 缓存动态创建的候选视图
+ private var suggestionSlotCount: Int = 21 // 包含前缀位,调这里可修改渲染数量
+ private val completionCapacity: Int
+ get() = (suggestionSlotCount - 1).coerceAtLeast(0)
@Volatile private var isSpecialToken: BooleanArray = BooleanArray(0)
@@ -866,76 +870,116 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 统一处理补全/联想
override fun updateCompletionsAndRender(prefix: String) {
val ic = currentInputConnection
-
- // 先判断整个编辑框是不是“真的空”
+
val beforeAll = ic?.getTextBeforeCursor(256, 0)?.toString().orEmpty()
val afterAll = ic?.getTextAfterCursor(256, 0)?.toString().orEmpty()
val editorReallyEmpty = beforeAll.isEmpty() && afterAll.isEmpty()
-
- // 当前输入前缀
+
currentInput.clear()
currentInput.append(prefix)
-
- // 如果整个编辑框都是空的:直接清空联想 & 刷新 UI,什么都不算
+
if (editorReallyEmpty) {
clearEditorState()
return
}
-
- // 否则再去算 lastWord
+
val lastWord = getPrevWordBeforeCursor()
-
+ val maxCompletions = completionCapacity
+
Thread {
- val list = try {
- if (prefix.isEmpty()) {
- if (lastWord == null) {
- // 这里也保持 emptyList,防止空前缀 + 无上文时走全局高频随机词
- 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, 20)
- .filter { it != prefix }
- }
- }
- } catch (_: Throwable) {
- if (prefix.isNotEmpty()) {
- wordDictionary.wordTrie.startsWith(prefix, 20)
- .filterNot { it == prefix }
- } else {
+ val list =
+ if (maxCompletions <= 0) {
emptyList()
+ } else {
+ try {
+ if (prefix.isEmpty()) {
+ if (lastWord == null) {
+ emptyList()
+ } else {
+ suggestWithBigram("", lastWord, topK = maxCompletions)
+ }
+ } else {
+ val fromBi = suggestWithBigram(prefix, lastWord, topK = maxCompletions)
+ if (fromBi.isNotEmpty()) {
+ fromBi.filter { it != prefix }
+ } else {
+ wordDictionary.wordTrie.startsWith(prefix, maxCompletions)
+ .filter { it != prefix }
+ }
+ }
+ } catch (_: Throwable) {
+ if (prefix.isNotEmpty()) {
+ wordDictionary.wordTrie.startsWith(prefix, maxCompletions)
+ .filterNot { it == prefix }
+ } else {
+ emptyList()
+ }
+ }
}
- }
-
+
mainHandler.post {
- completionSuggestions = suggestionStats.sortByCount(list.distinct().take(20))
+ val limited = if (maxCompletions > 0) list.distinct().take(maxCompletions) else emptyList()
+ completionSuggestions = suggestionStats.sortByCount(limited)
showCompletionSuggestions()
}
}.start()
}
+ private fun ensureSuggestionViews(): List {
+ val container = mainKeyboardView?.findViewById(R.id.completion_suggestions)
+ ?: return emptyList()
+ val targetCount = maxOf(suggestionSlotCount, 1)
+
+ if (suggestionViews.size < targetCount) {
+ repeat(targetCount - suggestionViews.size) {
+ val view = buildSuggestionView(container)
+ suggestionViews.add(view)
+ container.addView(view)
+ }
+ } else if (suggestionViews.size > targetCount) {
+ val removeCount = suggestionViews.size - targetCount
+ repeat(removeCount) {
+ val view = suggestionViews.removeLast()
+ container.removeView(view)
+ }
+ }
+
+ suggestionViews.forEach { view ->
+ if (view.parent == null) container.addView(view)
+ }
+
+ return suggestionViews.take(targetCount)
+ }
+
+ private fun buildSuggestionView(parent: LinearLayout): TextView {
+ val dp = resources.displayMetrics.density
+ return TextView(parent.context).apply {
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ textSize = 16f
+ setPadding((12 * dp).toInt(), 0, (12 * dp).toInt(), 0)
+ gravity = Gravity.CENTER
+ isClickable = true
+ setBackgroundResource(R.drawable.btn_keyboard)
+ setTextColor(Color.parseColor("#000000"))
+ }
+ }
// 显示自动完成建议(布局不变)
private fun showCompletionSuggestions() {
mainHandler.post {
- val suggestionsView =
- mainKeyboardView?.findViewById(R.id.completion_suggestions)
-
- // 新增:联想滚动条 & 控制面板
val completionScroll =
mainKeyboardView?.findViewById(R.id.completion_scroll)
val controlLayout =
mainKeyboardView?.findViewById(R.id.control_layout)
- val suggestions = (0..20).map { i ->
- mainKeyboardView?.findViewById(
- resources.getIdentifier("suggestion_$i", "id", packageName)
- )
+ val suggestions = ensureSuggestionViews()
+ if (suggestions.isEmpty()) {
+ completionScroll?.visibility = View.GONE
+ controlLayout?.visibility = View.VISIBLE
+ return@post
}
// 当前前缀
@@ -962,25 +1006,46 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
controlLayout?.visibility = View.GONE
}
- // suggestion_0 显示 prefix
- suggestions[0]?.text = prefix
- suggestions[0]?.visibility = if (prefix.isEmpty()) View.GONE else View.VISIBLE
- suggestions[0]?.setOnClickListener {
- insertCompletion(prefix)
+ val prefixView = suggestions.first()
+ prefixView.text = prefix
+ prefixView.visibility = if (prefix.isEmpty()) View.GONE else View.VISIBLE
+ if (prefix.isEmpty()) {
+ prefixView.setOnClickListener(null)
+ } else {
+ prefixView.setOnClickListener { insertCompletion(prefix) }
}
- // suggestion_1.. 按 completionSuggestions 填充
+ // 按 completionSuggestions 填充
suggestions.drop(1).forEachIndexed { index, textView ->
- textView?.text = completionSuggestions.getOrNull(index) ?: ""
- if (index < completionSuggestions.size) {
- textView?.visibility = View.VISIBLE
- textView?.setOnClickListener {
- suggestionStats.incClick(completionSuggestions[index])
- insertCompletion(completionSuggestions[index])
+ val word = completionSuggestions.getOrNull(index)
+ if (word != null) {
+ textView.text = word
+ textView.visibility = View.VISIBLE
+ textView.setOnClickListener {
+ suggestionStats.incClick(word)
+ insertCompletion(word)
}
} else {
- textView?.visibility = View.GONE
- textView?.setOnClickListener(null)
+ textView.text = ""
+ textView.visibility = View.GONE
+ textView.setOnClickListener(null)
+ }
+ }
+
+ // 给最后一个可见候选留出右侧距离,避免被关闭按钮遮挡
+ val spacingEndPx = (44 * resources.displayMetrics.density).toInt()
+ suggestions.forEach { view ->
+ val lp = view.layoutParams
+ if (lp is LinearLayout.LayoutParams) {
+ lp.marginEnd = 0
+ view.layoutParams = lp
+ }
+ }
+ suggestions.lastOrNull { it.visibility == View.VISIBLE }?.let { lastView ->
+ val lp = lastView.layoutParams
+ if (lp is LinearLayout.LayoutParams) {
+ lp.marginEnd = spacingEndPx
+ lastView.layoutParams = lp
}
}
@@ -1251,6 +1316,8 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// ================== bigram & 联想实现 ==================
private fun suggestWithBigram(prefix: String, lastWord: String?, topK: Int = 20): List {
+ if (topK <= 0) return emptyList()
+
val m = bigramModel
if (m == null || !bigramReady) {
return if (prefix.isNotEmpty()) {
@@ -1308,6 +1375,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
private fun unigramTopKFiltered(topK: Int): List {
+ if (topK <= 0) return emptyList()
val m = bigramModel ?: return emptyList()
if (!bigramReady) return emptyList()
@@ -1332,6 +1400,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
private fun topKByScore(pairs: List>, k: Int): List {
+ if (k <= 0) return emptyList()
val heap = java.util.PriorityQueue>(k.coerceAtLeast(1)) { a, b ->
a.second.compareTo(b.second)
}
@@ -1399,15 +1468,11 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
controlLayout?.visibility = View.VISIBLE
// 再让联想区域里的文本都清空一下
- val suggestions = (0..20).map { i ->
- mainKeyboardView?.findViewById(
- resources.getIdentifier("suggestion_$i", "id", packageName)
- )
- }
+ val suggestions = ensureSuggestionViews()
suggestions.forEach { tv ->
- tv?.text = ""
- tv?.visibility = View.GONE
- tv?.setOnClickListener(null)
+ tv.text = ""
+ tv.visibility = View.GONE
+ tv.setOnClickListener(null)
}
}
}
diff --git a/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt b/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt
index 2ff5c5c..6d2d859 100644
--- a/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt
+++ b/app/src/main/java/com/example/myapplication/keyboard/BaseKeyboard.kt
@@ -77,11 +77,23 @@ abstract class BaseKeyboard(
if (id != 0) add(id)
}
}
+ val suggestionContainerId =
+ env.ctx.resources.getIdentifier("completion_suggestions", "id", env.ctx.packageName)
+
+ fun isInSuggestionContainer(view: View): Boolean {
+ if (suggestionContainerId == 0) return false
+ var parent = view.parent
+ while (parent is View) {
+ if (parent.id == suggestionContainerId) return true
+ parent = parent.parent
+ }
+ return false
+ }
fun dfs(v: View?) {
when (v) {
is TextView -> {
- if (ignoredIds.contains(v.id)) return
+ if (ignoredIds.contains(v.id) || isInSuggestionContainer(v)) return
val lp = v.layoutParams
if (lp is LinearLayout.LayoutParams) {
diff --git a/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt b/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt
index b3bf2a9..e0aaf8b 100644
--- a/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt
+++ b/app/src/main/java/com/example/myapplication/network/HttpInterceptors.kt
@@ -9,6 +9,7 @@ import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.content.Context
+import com.example.myapplication.AppContext
import com.example.myapplication.network.security.BodyParamsExtractor
import com.example.myapplication.network.security.NonceUtils
import com.example.myapplication.network.security.SignUtils
@@ -28,6 +29,9 @@ private val NO_LOGIN_REQUIRED_PATHS = setOf(
"/themes/listByStyle",
"/wallet/balance",
"/character/listByUser",
+ "/user/detail",
+ "/character/listByTag",
+ "/character/list",
)
private val NO_SIGN_REQUIRED_PATHS = setOf(
@@ -294,7 +298,7 @@ val responseInterceptor = Interceptor { chain ->
if (errorResponse.code == 40102|| errorResponse.code == 40103) {
val isNoLoginApi = noLoginRequired(request.url)
-
+ EncryptedSharedPreferencesUtil.remove(AppContext.context, "user")
Log.w(
"1314520-HTTP",
"40102 path=${request.url.encodedPath}, noLogin=$isNoLoginApi"
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 9fca452..ade8ab4 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
@@ -129,6 +129,12 @@ class HomeFragment : Fragment() {
}
}
+ is AuthEvent.TokenExpired,
+ is AuthEvent.Logout -> {
+ // token 被清理或主动退出后,刷新首页为未登录态数据
+ refreshHomeAfterNetwork()
+ }
+
is AuthEvent.CharacterAdded -> {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
diff --git a/app/src/main/java/com/example/myapplication/ui/shop/ShopFragment.kt b/app/src/main/java/com/example/myapplication/ui/shop/ShopFragment.kt
index c0537be..ac1333e 100644
--- a/app/src/main/java/com/example/myapplication/ui/shop/ShopFragment.kt
+++ b/app/src/main/java/com/example/myapplication/ui/shop/ShopFragment.kt
@@ -41,6 +41,7 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
private lateinit var tagContainer: LinearLayout
private lateinit var balance: TextView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
+ private lateinit var shopTitle: TextView
// ===== Data =====
private var tabTitles: List = emptyList()
@@ -335,19 +336,34 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
private fun setupSwipeRefreshConflictFix() {
val appBar = requireView().findViewById(R.id.appBar)
-
- // 1) 监听 AppBar 是否完全展开
- appBar.addOnOffsetChangedListener { _, verticalOffset ->
+
+ appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
+ // 你原来的逻辑:是否完全展开
appBarFullyExpanded = (verticalOffset == 0)
+
+ // ===== 1) 快吸顶才展开:最后 20% 才开始从 0 -> 50dp =====
+ val ratio = kotlin.math.abs(verticalOffset).toFloat() / appBarLayout.totalScrollRange
+ val start = 0.8f // 80% 之后开始出现
+ val range = 0.2f // 用最后 20% 展开到满
+ val progress = ((ratio - start) / range).coerceIn(0f, 1f)
+
+ val maxHeightPx = (50 * resources.displayMetrics.density).toInt()
+ val newHeight = (progress * maxHeightPx).toInt()
+
+ // ===== 2) 同步文字渐显 =====
+ shopTitle.alpha = progress
+
+ // ===== 3) 防止抖动:高度没变就不 setLayoutParams =====
+ val lp = shopTitle.layoutParams
+ if (lp.height != newHeight) {
+ lp.height = newHeight
+ shopTitle.layoutParams = lp
+ }
}
-
- // 2) 核心:自定义"子 View 是否能向上滚"的判断
+
swipeRefreshLayout.setOnChildScrollUpCallback { _, _ ->
- // AppBar 没完全展开:不要让刷新抢手势(优先展开/折叠头部)
if (!appBarFullyExpanded) return@setOnChildScrollUpCallback true
- // 找到 ViewPager2 当前页的 RecyclerView
val rv = findCurrentPageRecyclerView()
- // rv 能向上滚:说明列表不在顶部 -> 禁止触发刷新
rv?.canScrollVertically(-1) ?: false
}
}
@@ -405,6 +421,7 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
runCatching { RetrofitClient.apiService.themeList() }.getOrNull()
private fun bindViews(view: View) {
+ shopTitle = view.findViewById(R.id.shopTitle)
viewPager = view.findViewById(R.id.viewPager)
tagScroll = view.findViewById(R.id.tagScroll)
tagContainer = view.findViewById(R.id.tagContainer)
@@ -442,6 +459,17 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
"element_id" to "search_btn",
)
}
+
+ view.findViewById(R.id.recordButton).setOnClickListener {
+ //消费记录
+ AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.consumptionRecordFragment))
+ BehaviorReporter.report(
+ isNewUser = false,
+ "page_id" to "my",
+ "element_id" to "menu_item",
+ "item_title" to "消费记录"
+ )
+ }
}
}
diff --git a/app/src/main/res/drawable/bg_shop_gradient.xml b/app/src/main/res/drawable/bg_shop_gradient.xml
new file mode 100644
index 0000000..53b8284
--- /dev/null
+++ b/app/src/main/res/drawable/bg_shop_gradient.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shop_record_bg.xml b/app/src/main/res/drawable/shop_record_bg.xml
new file mode 100644
index 0000000..e95045f
--- /dev/null
+++ b/app/src/main/res/drawable/shop_record_bg.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_mine.xml b/app/src/main/res/layout/fragment_mine.xml
index 0e38dde..49e2cc5 100644
--- a/app/src/main/res/layout/fragment_mine.xml
+++ b/app/src/main/res/layout/fragment_mine.xml
@@ -9,12 +9,10 @@
tools:context=".ui.home.HomeFragment">
-
+ android:background="#F6F7FB"/>
-
+ android:adjustViewBounds="true" /> -->
+ android:background="@drawable/bg_shop_gradient"
+ android:elevation="0dp"
+ android:stateListAnimator="@null">
-
+
+
+
+
+
+
+
+
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_vertical">
+
+
+
+
diff --git a/app/src/main/res/layout/keyboard.xml b/app/src/main/res/layout/keyboard.xml
index 2260d70..fc45bdc 100644
--- a/app/src/main/res/layout/keyboard.xml
+++ b/app/src/main/res/layout/keyboard.xml
@@ -68,8 +68,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="3dp"
android:background="@drawable/complete_bg">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:paddingEnd="4dp" />
添加
+ 商城
积分商城
我的积分
充值
@@ -140,7 +141,7 @@
去切换
- 点击粘贴您的内容
+ 粘贴TA的话
粘贴
清空
发送
@@ -156,6 +157,7 @@
跳过
删除
下一步
+ 对方正在输入...
继续操作即表示您已经阅读并同意我们的
diff --git a/app/src/main/res/values/strings_i18n.xml b/app/src/main/res/values/strings_i18n.xml
index a76d5bc..fa36c48 100644
--- a/app/src/main/res/values/strings_i18n.xml
+++ b/app/src/main/res/values/strings_i18n.xml
@@ -103,6 +103,7 @@
+ Shop
Points Mall
My points
Recharge
@@ -162,6 +163,7 @@
Skip
Delete
Next step
+ The other party is currently inputting...
By Continuing, You Agree To Our