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