键盘ai角色,无数据显示2

This commit is contained in:
pengxiaolong
2026-02-12 19:40:32 +08:00
parent ea2ec19dc8
commit acf4d39892
47 changed files with 1923 additions and 472 deletions

View File

@@ -3,7 +3,8 @@
"allow": [ "allow": [
"Bash(dir:*)", "Bash(dir:*)",
"Bash(powershell:*)", "Bash(powershell:*)",
"Bash(find /d/Test/MyApplication/app/src/main/assets -name \"vocab.txt\" -exec wc -l {} ;)" "Bash(find /d/Test/MyApplication/app/src/main/assets -name \"vocab.txt\" -exec wc -l {} ;)",
"WebSearch"
] ]
} }
} }

View File

@@ -13,6 +13,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.network.BehaviorReporter
import com.example.myapplication.utils.ImeUtils
class ImeGuideActivity : AppCompatActivity() { class ImeGuideActivity : AppCompatActivity() {
@@ -228,56 +229,7 @@ class ImeGuideActivity : AppCompatActivity() {
} }
} }
/** 是否启用了本输入法 */ private fun isImeEnabled(): Boolean = ImeUtils.isImeEnabled(this)
private fun isImeEnabled(): Boolean {
return try {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val myComponent = ComponentName(this, MyInputMethodService::class.java)
val result = imm.enabledInputMethodList.any { imeInfo ->
imeInfo.packageName == myComponent.packageName &&
imeInfo.serviceName == myComponent.className
}
Log.d(TAG, "isImeEnabled = $result")
result
} catch (e: Exception) {
Log.e(TAG, "isImeEnabled 出错", e)
false
}
}
/** 是否已切换为当前输入法 */ private fun isImeSelected(): Boolean = ImeUtils.isImeSelected(this)
private fun isImeSelected(): Boolean {
return try {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val myComponent = ComponentName(this, MyInputMethodService::class.java)
val currentImeId = Settings.Secure.getString(
contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
) ?: return false
Log.d(TAG, "DEFAULT_INPUT_METHOD = $currentImeId")
// 找到“当前默认 IME”对应的 InputMethodInfo
val currentImeInfo = imm.enabledInputMethodList.firstOrNull { imeInfo ->
imeInfo.id == currentImeId
}
if (currentImeInfo == null) {
Log.d(TAG, "currentImeInfo == null")
return false
}
val isMine = currentImeInfo.packageName == myComponent.packageName &&
currentImeInfo.serviceName == myComponent.className
Log.d(TAG, "isImeSelected = $isMine")
isMine
} catch (e: Exception) {
Log.e(TAG, "isImeSelected 出错", e)
false
}
}
} }

View File

@@ -27,6 +27,7 @@ import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.network.BehaviorReporter
import com.example.myapplication.network.NetworkEvent import com.example.myapplication.network.NetworkEvent
import com.example.myapplication.network.NetworkEventBus import com.example.myapplication.network.NetworkEventBus
import com.example.myapplication.keyboard.AiRolePreferences
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@@ -50,7 +51,8 @@ class MainActivity : AppCompatActivity() {
private val protectedTabs = setOf( private val protectedTabs = setOf(
R.id.shop_graph, R.id.shop_graph,
R.id.mine_graph R.id.mine_graph,
R.id.circle_graph
) )
private val tabMap by lazy { private val tabMap by lazy {
@@ -73,6 +75,7 @@ class MainActivity : AppCompatActivity() {
private var connectivityManager: ConnectivityManager? = null private var connectivityManager: ConnectivityManager? = null
private var networkCallback: ConnectivityManager.NetworkCallback? = null private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var hasNetworkConnection: Boolean = true private var hasNetworkConnection: Boolean = true
private var wasNetworkValidated: Boolean = true
private var noNetworkDialog: AlertDialog? = null private var noNetworkDialog: AlertDialog? = null
private val currentTabHost: NavHostFragment private val currentTabHost: NavHostFragment
@@ -192,34 +195,37 @@ class MainActivity : AppCompatActivity() {
// 登录成功事件处理 // 登录成功事件处理
is AuthEvent.LoginSuccess -> { is AuthEvent.LoginSuccess -> {
// 关闭 global overlay回到 empty // 捕获待切换的 tab立即清除
globalNavController.popBackStack(R.id.globalEmptyFragment, false) val targetTab = pendingTabAfterLogin
pendingTabAfterLogin = null
// 如果之前想去商城/我的,登录成功后自动切过去 // 先切 tab在 global overlay 关闭之前),确保 currentTabTag 已更新
pendingTabAfterLogin?.let { tag -> if (targetTab != null) {
switchTab(tag) switchTab(targetTab)
bottomNav.selectedItemId = when (tag) { bottomNav.selectedItemId = when (targetTab) {
TAB_SHOP -> R.id.shop_graph TAB_SHOP -> R.id.shop_graph
TAB_CIRCLE -> R.id.circle_graph TAB_CIRCLE -> R.id.circle_graph
TAB_MINE -> R.id.mine_graph TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph else -> R.id.home_graph
} }
supportFragmentManager.executePendingTransactions()
} }
pendingTabAfterLogin = null
// 再关闭 global overlaypopBackStack 会触发 bindGlobalVisibility
// 此时 tab 已切换完成updateBottomNavVisibility 能正确判断
globalNavController.popBackStack(R.id.globalEmptyFragment, false)
// 处理intent跳转目标页 // 处理intent跳转目标页
if (pendingNavigationAfterLogin == "recharge_fragment") { if (pendingNavigationAfterLogin == "recharge_fragment") {
openGlobal(R.id.rechargeFragment) openGlobal(R.id.rechargeFragment)
pendingNavigationAfterLogin = null pendingNavigationAfterLogin = null
} }
// ✅ 登录成功后也刷新一次
bottomNav.post { updateBottomNavVisibility() }
} }
// 登出事件处理 // 登出事件处理
is AuthEvent.Logout -> { is AuthEvent.Logout -> {
pendingTabAfterLogin = event.returnTabTag pendingTabAfterLogin = event.returnTabTag
AiRolePreferences.clear(this@MainActivity)
// ✅ 用户没登录按返回,应回首页,所以先切到首页 // ✅ 用户没登录按返回,应回首页,所以先切到首页
switchTab(TAB_HOME, force = true) switchTab(TAB_HOME, force = true)
@@ -393,9 +399,6 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.global_container).visibility = findViewById<View>(R.id.global_container).visibility =
if (isEmpty) View.GONE else View.VISIBLE if (isEmpty) View.GONE else View.VISIBLE
// ✅ 底栏统一走 update
updateBottomNavVisibility()
val justClosedOverlay = val justClosedOverlay =
(dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment) (dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
lastGlobalDestId = dest.id lastGlobalDestId = dest.id
@@ -404,6 +407,7 @@ class MainActivity : AppCompatActivity() {
val currentTabGraphId = when (currentTabTag) { val currentTabGraphId = when (currentTabTag) {
TAB_SHOP -> R.id.shop_graph TAB_SHOP -> R.id.shop_graph
TAB_MINE -> R.id.mine_graph TAB_MINE -> R.id.mine_graph
TAB_CIRCLE -> R.id.circle_graph
else -> R.id.home_graph else -> R.id.home_graph
} }
@@ -415,9 +419,10 @@ class MainActivity : AppCompatActivity() {
if (!isLoggedIn()) { if (!isLoggedIn()) {
pendingTabAfterLogin = null pendingTabAfterLogin = null
} }
bottomNav.post { updateBottomNavVisibility() }
} }
// 无论何种情况global 目的地变化后都刷新底栏
bottomNav.post { updateBottomNavVisibility() }
} }
} }
@@ -705,6 +710,7 @@ class MainActivity : AppCompatActivity() {
if (cm == null) return if (cm == null) return
hasNetworkConnection = isNetworkConnected(cm) hasNetworkConnection = isNetworkConnected(cm)
wasNetworkValidated = hasNetworkConnection
if (!hasNetworkConnection) { if (!hasNetworkConnection) {
showNoNetworkDialog() showNoNetworkDialog()
NetworkEventBus.emit(NetworkEvent.NetworkLost) NetworkEventBus.emit(NetworkEvent.NetworkLost)
@@ -718,6 +724,24 @@ class MainActivity : AppCompatActivity() {
override fun onLost(network: Network) { override fun onLost(network: Network) {
handleNetworkStateChange(isNetworkConnected(cm)) handleNetworkStateChange(isNetworkConnected(cm))
} }
override fun onCapabilitiesChanged(
network: Network,
capabilities: NetworkCapabilities
) {
val validated = capabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_VALIDATED
)
if (validated && !wasNetworkValidated) {
// 网络从未验证(慢/超时)变为已验证(可用),触发刷新
wasNetworkValidated = true
runOnUiThread {
NetworkEventBus.emit(NetworkEvent.NetworkAvailable)
}
} else if (!validated && wasNetworkValidated) {
wasNetworkValidated = false
}
}
} }
networkCallback = callback networkCallback = callback
@@ -735,6 +759,7 @@ class MainActivity : AppCompatActivity() {
private fun handleNetworkStateChange(connected: Boolean) { private fun handleNetworkStateChange(connected: Boolean) {
if (connected == hasNetworkConnection) return if (connected == hasNetworkConnection) return
hasNetworkConnection = connected hasNetworkConnection = connected
wasNetworkValidated = connected
runOnUiThread { runOnUiThread {
if (connected) { if (connected) {

View File

@@ -43,7 +43,15 @@ import kotlin.math.abs
import java.text.BreakIterator import java.text.BreakIterator
import android.widget.EditText import android.widget.EditText
import android.content.res.Configuration import android.content.res.Configuration
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.Glide
import com.example.myapplication.keyboard.AiRolePreferences
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.aiCompanionPageRequest
import com.example.myapplication.keyboard.AiRolePanelController
import com.example.myapplication.keyboard.FixedHeightFrameLayout
import de.hdodenhof.circleimageview.CircleImageView
class MyInputMethodService : InputMethodService(), KeyboardEnvironment { class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
@@ -88,6 +96,11 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// ================= 表情 ================= // ================= 表情 =================
private var emojiKeyboardView: View? = null private var emojiKeyboardView: View? = null
private var emojiKeyboard: com.example.myapplication.keyboard.EmojiKeyboard? = null private var emojiKeyboard: com.example.myapplication.keyboard.EmojiKeyboard? = null
// ================= AI 角色面板(通过 CandidatesView 实现) =================
private var aiRolePanelView: View? = null
private var isAiRolePanelShown = false
private var aiRolePanelController: AiRolePanelController? = null
// =================上滑清空================== // =================上滑清空==================
private var swipeHintPopup: PopupWindow? = null private var swipeHintPopup: PopupWindow? = null
private var swipeClearPopup: PopupWindow? = null private var swipeClearPopup: PopupWindow? = null
@@ -242,8 +255,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
} }
// 输入法状态变化 // 创建输入视图:直接返回键盘 rootView
// 创建输入视图:此处只负责选择主键盘
override fun onCreateInputView(): View { override fun onCreateInputView(): View {
val keyboard = ensureMainKeyboard() val keyboard = ensureMainKeyboard()
currentKeyboardView = keyboard.rootView currentKeyboardView = keyboard.rootView
@@ -252,6 +264,22 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
return keyboard.rootView return keyboard.rootView
} }
// 创建候选视图:用作 AI 角色面板的容器Android 官方 API
override fun onCreateCandidatesView(): View {
val panel = ensureAiRolePanelView()
(panel.parent as? ViewGroup)?.removeView(panel)
return panel
}
// 关键:让系统把 CandidatesView 区域也算作输入法内容区域,正确推上输入框
// 这是解决 Android 13+ setCandidatesViewShown 不生效的官方 workaround
override fun onComputeInsets(outInsets: Insets) {
super.onComputeInsets(outInsets)
if (!isFullscreenMode()) {
outInsets.contentTopInsets = outInsets.visibleTopInsets
}
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
super.onStartInputView(info, restarting) super.onStartInputView(info, restarting)
isInputViewShownFlag = true isInputViewShownFlag = true
@@ -303,6 +331,10 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 清理本次输入状态 // 清理本次输入状态
clearEditorState() clearEditorState()
// 键盘收起时清理聊天数据
dismissAiRolePanel()
aiRolePanelController?.clearMessages()
mainHandler.postDelayed({ mainHandler.postDelayed({
if (!isInputViewShownFlag) { if (!isInputViewShownFlag) {
try { try {
@@ -318,6 +350,8 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
ThemeManager.removeThemeChangeListener(themeListener) ThemeManager.removeThemeChangeListener(themeListener)
stopRepeatDelete() stopRepeatDelete()
aiKeyboard?.cancelAiStream() aiKeyboard?.cancelAiStream()
aiRolePanelController?.destroy()
aiRolePanelController = null
super.onDestroy() super.onDestroy()
} }
@@ -432,6 +466,82 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
return lastClearedText != null return lastClearedText != null
} }
// ================= AI 角色面板展开/关闭 =================
override fun toggleAiRolePanel() {
if (isAiRolePanelShown) {
dismissAiRolePanel()
} else {
showAiRolePanel()
}
}
private fun ensureAiRolePanelView(): View {
if (aiRolePanelView == null) {
aiRolePanelView = layoutInflater.inflate(R.layout.airole_panel, null)
// CandidatesView 父容器mCandidatesFrame用 MeasureSpec.UNSPECIFIED 测量子 View
// 任何 XML/LayoutParams 声明的固定高度都会被忽略RecyclerView 会展开所有 item。
// 通过自定义 FixedHeightFrameLayout.maxHeightPx 在 onMeasure 中强制钳制高度。
val panelHeightPx = resources.getDimensionPixelSize(R.dimen.sw_200dp)
(aiRolePanelView as? FixedHeightFrameLayout)?.maxHeightPx = panelHeightPx
aiRolePanelView?.findViewById<View>(R.id.airole_panel_close)?.setOnClickListener {
dismissAiRolePanel()
}
// 创建 Controller 并绑定视图
aiRolePanelController = AiRolePanelController(this)
aiRolePanelController?.bindView(aiRolePanelView!!)
}
return aiRolePanelView!!
}
private fun showAiRolePanel() {
if (isAiRolePanelShown) return
// 复制当前键盘根视图的背景到面板
val keyboardView = currentKeyboardView
val panel = ensureAiRolePanelView()
if (keyboardView != null) {
val bgDrawable = keyboardView.background?.constantState?.newDrawable()?.mutate()
if (bgDrawable != null) {
panel.background = bgDrawable
}
}
isAiRolePanelShown = true
aiRolePanelController?.onPanelShow()
setCandidatesViewShown(true)
// Android 13+ bug workaround系统内部可能把候选容器设为 INVISIBLE
// 需要主动遍历找到面板的父容器链,强制设为 VISIBLE
mainHandler.post {
forceCandidatesFrameVisible()
}
}
private fun dismissAiRolePanel() {
if (!isAiRolePanelShown) return
isAiRolePanelShown = false
setCandidatesViewShown(false)
}
/**
* Android 13+ workaroundsetCandidatesViewShown(true) 后,
* 系统内部的 mCandidatesFrame 可能被设为 INVISIBLE。
* 从面板 View 向上遍历父容器,强制所有不可见的父容器设为 VISIBLE。
*/
private fun forceCandidatesFrameVisible() {
var v: View? = aiRolePanelView ?: return
while (v != null) {
if (v.visibility != View.VISIBLE) {
v.visibility = View.VISIBLE
}
val parent = v.parent
v = if (parent is View) parent else null
}
}
private fun showSwipeClearHint(anchor: View, text: String = "Clear") { private fun showSwipeClearHint(anchor: View, text: String = "Clear") {
mainHandler.post { mainHandler.post {
if (swipeClearPopupShown) return@post if (swipeClearPopupShown) return@post
@@ -548,40 +658,84 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
} }
override fun showMainKeyboard() { override fun showMainKeyboard() {
clearEditorState() clearEditorState()
val kb = ensureMainKeyboard() val kb = ensureMainKeyboard()
currentKeyboardView = kb.rootView currentKeyboardView = kb.rootView
setInputViewSafely(kb.rootView) setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor) kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
updateAiRoleAvatar()
} }
private fun updateAiRoleAvatar() {
val view = currentKeyboardView ?: return
val avatarView = view.findViewById<CircleImageView>(R.id.key_airole) ?: return
val info = AiRolePreferences.getLastCompanion(this)
if (info != null && !info.avatarUrl.isNullOrBlank()) {
Glide.with(this)
.load(info.avatarUrl)
.circleCrop()
.placeholder(android.R.drawable.ic_menu_myplaces)
.into(avatarView)
} else {
avatarView.setImageResource(android.R.drawable.ic_menu_myplaces)
// 没有角色数据时,异步获取第一个角色作为默认值
fetchDefaultCompanion()
}
}
private fun fetchDefaultCompanion() {
CoroutineScope(Dispatchers.IO).launch {
try {
val response = RetrofitClient.apiService.aiCompanionPage(
aiCompanionPageRequest(pageNum = 1, pageSize = 1)
)
val companion = response.data?.records?.firstOrNull() ?: return@launch
AiRolePreferences.saveLastCompanion(
this@MyInputMethodService,
companion.id,
companion.name,
companion.avatarUrl
)
// 保存后刷新头像
launch(Dispatchers.Main) {
updateAiRoleAvatar()
}
} catch (e: Exception) {
// 网络异常忽略,下次切换键盘时会重试
}
}
}
override fun showNumberKeyboard() { override fun showNumberKeyboard() {
clearEditorState() clearEditorState()
val kb = ensureNumberKeyboard() val kb = ensureNumberKeyboard()
currentKeyboardView = kb.rootView currentKeyboardView = kb.rootView
setInputViewSafely(kb.rootView) setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor) kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
updateAiRoleAvatar()
} }
override fun showSymbolKeyboard() { override fun showSymbolKeyboard() {
clearEditorState() clearEditorState()
val kb = ensureSymbolKeyboard() val kb = ensureSymbolKeyboard()
currentKeyboardView = kb.rootView currentKeyboardView = kb.rootView
setInputViewSafely(kb.rootView) setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor) kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
updateAiRoleAvatar()
} }
override fun showAiKeyboard() { override fun showAiKeyboard() {
clearEditorState() clearEditorState()
val kb = ensureAiKeyboard() val kb = ensureAiKeyboard()
currentKeyboardView = kb.rootView currentKeyboardView = kb.rootView
setInputViewSafely(kb.rootView) setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor) kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
kb.refreshPersonas() kb.refreshPersonas()
updateAiRoleAvatar()
} }
override fun showEmojiKeyboard() { override fun showEmojiKeyboard() {
clearEditorState() clearEditorState()
val kb = ensureEmojiKeyboard() val kb = ensureEmojiKeyboard()
currentKeyboardView = kb.rootView currentKeyboardView = kb.rootView
setInputViewSafely(kb.rootView) setInputViewSafely(kb.rootView)
@@ -589,27 +743,31 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
} }
override fun associateClose() { override fun associateClose() {
clearEditorState() clearEditorState()
val kb = ensureEmojiKeyboard() val kb = ensureEmojiKeyboard()
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
// 先清理缓存,避免复用旧 View dismissAiRolePanel()
aiRolePanelController?.destroy()
aiRolePanelController = null
aiRolePanelView = null
currentKeyboardView = null currentKeyboardView = null
aiKeyboard?.cancelAiStream() aiKeyboard?.cancelAiStream()
mainKeyboardView = null mainKeyboardView = null
numberKeyboardView = null numberKeyboardView = null
symbolKeyboardView = null symbolKeyboardView = null
aiKeyboardView = null aiKeyboardView = null
emojiKeyboardView = null emojiKeyboardView = null
mainKeyboard = null mainKeyboard = null
numberKeyboard = null numberKeyboard = null
symbolKeyboard = null symbolKeyboard = null
aiKeyboard = null aiKeyboard = null
emojiKeyboard = null emojiKeyboard = null
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
@@ -822,6 +980,26 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 发送(标准 SEND + 回车 fallback // 发送(标准 SEND + 回车 fallback
override fun performSendAction() { override fun performSendAction() {
// AI 角色面板打开时,拦截发送操作,将文本发给 AI 角色
if (isAiRolePanelShown) {
val ic = currentInputConnection ?: return
val et = try {
ic.getExtractedText(ExtractedTextRequest(), 0)
} catch (_: Throwable) { null }
val text = et?.text?.toString()?.trim().orEmpty()
if (text.isNotEmpty() && aiRolePanelController?.sendMessage(text) == true) {
// 清空输入框
ic.beginBatchEdit()
try {
ic.performContextMenuAction(android.R.id.selectAll)
ic.commitText("", 1)
} finally {
ic.endBatchEdit()
}
}
return
}
val ic = currentInputConnection ?: return val ic = currentInputConnection ?: return
val info = currentInputEditorInfo val info = currentInputEditorInfo

View File

@@ -484,6 +484,14 @@ class AiKeyboard(
} }
} }
// AI 角色面板
val airoleId = res.getIdentifier("key_airole", "id", pkg)
if (airoleId != 0) {
rootView.findViewById<View?>(airoleId)?.setOnClickListener {
env.toggleAiRolePanel()
}
}
// VIP // VIP
val vipButtonId = res.getIdentifier("key_vip", "id", pkg) val vipButtonId = res.getIdentifier("key_vip", "id", pkg)
if (vipButtonId != 0) { if (vipButtonId != 0) {

View File

@@ -0,0 +1,195 @@
package com.example.myapplication.keyboard
import android.content.Context
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.chatMessageRequest
import com.example.myapplication.ui.circle.ChatMessage
import com.example.myapplication.ui.circle.ChatMessageAdapter
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* AI 角色面板控制器,封装面板内聊天的全部业务逻辑
*/
class AiRolePanelController(private val context: Context) {
companion object {
private const val TAG = "AiRolePanel"
}
private var recyclerView: RecyclerView? = null
private var loginHint: TextView? = null
private var adapter: ChatMessageAdapter? = null
private val messages = mutableListOf<ChatMessage>()
private var currentCompanionId: Int = 0
private var messageIdCounter = System.currentTimeMillis()
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun bindView(panelView: View) {
recyclerView = panelView.findViewById(R.id.airole_panel_messages)
loginHint = panelView.findViewById(R.id.airole_panel_login_hint)
adapter = ChatMessageAdapter()
recyclerView?.apply {
layoutManager = LinearLayoutManager(context).apply {
stackFromEnd = true
}
adapter = this@AiRolePanelController.adapter
}
adapter?.bindMessages(messages)
}
private fun isLoggedIn(): Boolean {
return EncryptedSharedPreferencesUtil.contains(context, "user")
}
private fun updateLoginState() {
val loggedIn = isLoggedIn()
recyclerView?.visibility = if (loggedIn) View.VISIBLE else View.GONE
loginHint?.visibility = if (loggedIn) View.GONE else View.VISIBLE
}
fun onPanelShow() {
updateLoginState()
if (!isLoggedIn()) return
val info = AiRolePreferences.getLastCompanion(context)
val newId = info?.companionId ?: 0
if (newId != currentCompanionId) {
currentCompanionId = newId
messages.clear()
adapter?.bindMessages(messages)
}
}
/**
* 发送用户消息到 AI 角色
* @return true 表示消息已处理面板打开且有角色false 表示不处理
*/
fun sendMessage(text: String): Boolean {
if (!isLoggedIn()) return false
if (text.isBlank() || currentCompanionId <= 0) return false
// 添加用户消息
val userMsg = ChatMessage(
id = nextId(),
text = text,
isMine = true,
timestamp = System.currentTimeMillis()
)
messages.add(userMsg)
adapter?.notifyLastInserted()
scrollToBottom()
// 添加占位消息
val placeholder = ChatMessage(
id = nextId(),
text = "...",
isMine = false,
timestamp = System.currentTimeMillis(),
isLoading = true,
hasAnimated = false
)
messages.add(placeholder)
adapter?.notifyLastInserted()
scrollToBottom()
// 请求 AI 回复
scope.launch {
val response = try {
withContext(Dispatchers.IO) {
RetrofitClient.apiService.chatMessage(
chatMessageRequest(content = text, companionId = currentCompanionId)
)
}
} catch (e: Exception) {
Log.e(TAG, "chatMessage failed: ${e.message}", e)
placeholder.text = context.getString(R.string.refresh_failed)
placeholder.isLoading = false
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
return@launch
}
val data = response.data
if (data == null) {
Log.e(TAG, "chatMessage no data, code=${response.code}")
placeholder.text = context.getString(R.string.refresh_failed)
placeholder.isLoading = false
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
return@launch
}
placeholder.text = data.aiResponse
placeholder.audioId = data.audioId
placeholder.isLoading = false
placeholder.hasAnimated = false
adapter?.notifyMessageUpdated(placeholder.id)
scrollToBottom()
// 轮询音频 URL
val audioUrl = fetchAudioUrl(data.audioId)
if (!audioUrl.isNullOrBlank()) {
placeholder.audioUrl = audioUrl
adapter?.notifyMessageUpdated(placeholder.id)
}
}
return true
}
private suspend fun fetchAudioUrl(audioId: String): String? {
if (audioId.isBlank()) return null
return withContext(Dispatchers.IO) {
try {
RetrofitClient.apiService.chatAudioStatus(audioId).data?.audioUrl
} catch (e: Exception) {
Log.e(TAG, "chatAudioStatus failed: ${e.message}", e)
null
}
}
}
private fun scrollToBottom() {
val rv = recyclerView ?: return
val count = adapter?.itemCount ?: return
if (count > 0) {
rv.post { rv.smoothScrollToPosition(count - 1) }
}
}
private fun nextId(): Long = messageIdCounter++
fun clearMessages() {
scope.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
adapter?.release()
messages.clear()
adapter = ChatMessageAdapter()
recyclerView?.adapter = adapter
adapter?.bindMessages(messages)
}
fun destroy() {
scope.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
adapter?.release()
adapter = null
recyclerView?.adapter = null
recyclerView = null
loginHint = null
messages.clear()
currentCompanionId = 0
}
}

View File

@@ -0,0 +1,52 @@
package com.example.myapplication.keyboard
import android.content.Context
/**
* 持久化最后停留的 AI 角色信息,供输入法服务跨进程读取
*/
object AiRolePreferences {
private const val PREFS_NAME = "ai_role_prefs"
private const val KEY_COMPANION_ID = "companion_id"
private const val KEY_PERSONA_NAME = "persona_name"
private const val KEY_AVATAR_URL = "avatar_url"
data class CompanionInfo(
val companionId: Int,
val personaName: String,
val avatarUrl: String?
)
fun saveLastCompanion(
context: Context,
companionId: Int,
personaName: String,
avatarUrl: String?
) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
.edit()
.putInt(KEY_COMPANION_ID, companionId)
.putString(KEY_PERSONA_NAME, personaName)
.putString(KEY_AVATAR_URL, avatarUrl)
.apply()
}
fun getLastCompanion(context: Context): CompanionInfo? {
val sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
val id = sp.getInt(KEY_COMPANION_ID, 0)
if (id <= 0) return null
return CompanionInfo(
companionId = id,
personaName = sp.getString(KEY_PERSONA_NAME, "") ?: "",
avatarUrl = sp.getString(KEY_AVATAR_URL, null)
)
}
fun clear(context: Context) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
.edit()
.clear()
.apply()
}
}

View File

@@ -5,6 +5,7 @@ import android.content.res.ColorStateList
import android.os.Build import android.os.Build
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -118,6 +119,125 @@ abstract class BaseKeyboard(
dfs(root) dfs(root)
} }
/**
* 为指定的行容器启用间隙触摸转发:
* 当触摸点落在子 View 之间的间隙时,找到最近的可点击子 View 并转发事件
*/
protected fun enableGapTouchForwarding(row: ViewGroup) {
var forwardTarget: View? = null
row.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// 查找触摸点命中的子 View如果已经命中了则不拦截
val touchX = event.x
val touchY = event.y
val hitChild = findChildAt(row, touchX, touchY)
if (hitChild != null) {
// 触摸直接落在按键上,走正常分发流程
forwardTarget = null
false
} else {
// 触摸落在间隙,找到最近的可点击子 View
val nearest = findNearestChild(row, touchX, touchY)
if (nearest != null) {
forwardTarget = nearest
// 将坐标转换到目标 View 的局部坐标系并转发
val forwarded = forwardEventToChild(row, nearest, event)
forwarded
} else {
false
}
}
}
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_OUTSIDE -> {
val target = forwardTarget
if (target != null) {
val result = forwardEventToChild(row, target, event)
if (event.actionMasked == MotionEvent.ACTION_UP ||
event.actionMasked == MotionEvent.ACTION_CANCEL) {
forwardTarget = null
}
result
} else {
false
}
}
else -> false
}
}
}
/** 判断触摸坐标是否直接落在某个子 View 上 */
private fun findChildAt(parent: ViewGroup, x: Float, y: Float): View? {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child.visibility != View.VISIBLE) continue
if (x >= child.left && x <= child.right && y >= child.top && y <= child.bottom) {
return child
}
}
return null
}
/** 找到距离触摸点最近的可点击子 View */
private fun findNearestChild(parent: ViewGroup, x: Float, y: Float): View? {
var nearest: View? = null
var minDist = Float.MAX_VALUE
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child.visibility != View.VISIBLE) continue
if (!child.isClickable && !child.hasOnClickListeners()) continue
// 计算触摸点到子 View 边界的最短距离
val cx = x.coerceIn(child.left.toFloat(), child.right.toFloat())
val cy = y.coerceIn(child.top.toFloat(), child.bottom.toFloat())
val dx = x - cx
val dy = y - cy
val dist = dx * dx + dy * dy
if (dist < minDist) {
minDist = dist
nearest = child
}
}
return nearest
}
/** 将触摸事件的坐标从父容器转换到目标子 View 并分发 */
private fun forwardEventToChild(parent: ViewGroup, child: View, event: MotionEvent): Boolean {
val offsetX = parent.scrollX - child.left
val offsetY = parent.scrollY - child.top
val childEvent = MotionEvent.obtain(event)
childEvent.offsetLocation(offsetX.toFloat(), offsetY.toFloat())
val handled = child.dispatchTouchEvent(childEvent)
childEvent.recycle()
return handled
}
/**
* 为根布局中所有包含按键的水平行容器启用间隙触摸转发。
* 排除控制栏和补全建议区(它们有独立的交互逻辑)。
*/
protected fun setupGapTouchForwardingForRows(root: View) {
if (root !is ViewGroup) return
val res = env.ctx.resources
val pkg = env.ctx.packageName
val excludedIds = setOf(
res.getIdentifier("control_layout", "id", pkg),
res.getIdentifier("completion_scroll", "id", pkg)
)
for (i in 0 until root.childCount) {
val child = root.getChildAt(i)
if (child is LinearLayout
&& child.orientation == LinearLayout.HORIZONTAL
&& child.id !in excludedIds
) {
enableGapTouchForwarding(child)
}
}
}
/** dp -> px */ /** dp -> px */
protected fun Int.dpToPx(): Int { protected fun Int.dpToPx(): Int {
val density = env.ctx.resources.displayMetrics.density val density = env.ctx.resources.displayMetrics.density

View File

@@ -0,0 +1,37 @@
package com.example.myapplication.keyboard
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
/**
* 固定最大高度的 FrameLayout。
*
* Android InputMethodService 的 CandidatesView 父容器mCandidatesFrame
* 使用 MeasureSpec.UNSPECIFIED 测量子 View导致子 View 声明的任何固定高度
* 都会被忽略RecyclerView 会展开为所有 item 的总高度。
*
* 本类在 onMeasure 中将 UNSPECIFIED/过大的高度钳制为 [maxHeightPx]
* 确保内部 RecyclerView 在固定区域内滚动。
*/
class FixedHeightFrameLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
/** 最大高度(像素),由外部设置 */
var maxHeightPx: Int = 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val constrainedHeight = if (maxHeightPx > 0) {
MeasureSpec.makeMeasureSpec(maxHeightPx, MeasureSpec.AT_MOST)
} else {
heightMeasureSpec
}
super.onMeasure(widthMeasureSpec, constrainedHeight)
if (maxHeightPx > 0 && measuredHeight > maxHeightPx) {
setMeasuredDimension(measuredWidth, maxHeightPx)
}
}
}

View File

@@ -50,4 +50,7 @@ interface KeyboardEnvironment {
// 检查是否有可回填的文本 // 检查是否有可回填的文本
fun hasClearedText(): Boolean fun hasClearedText(): Boolean
// AI 角色面板
fun toggleAiRolePanel()
} }

View File

@@ -170,6 +170,13 @@ class MainKeyboard(
view.findViewById<View?>(res.getIdentifier("key_emoji", "id", pkg))?.setOnClickListener { view.findViewById<View?>(res.getIdentifier("key_emoji", "id", pkg))?.setOnClickListener {
vibrateKey(); env.showEmojiKeyboard() vibrateKey(); env.showEmojiKeyboard()
} }
view.findViewById<View?>(res.getIdentifier("key_airole", "id", pkg))?.setOnClickListener {
vibrateKey(); env.toggleAiRolePanel()
}
// 为键盘行容器启用间隙触摸转发,点击按键间隙时触发最近按键
setupGapTouchForwardingForRows(view)
} }
// 更新Revoke按钮的可见性 // 更新Revoke按钮的可见性

View File

@@ -120,6 +120,9 @@ class NumberKeyboard(
numView.findViewById<View?>(res.getIdentifier("key_ai", "id", pkg)) numView.findViewById<View?>(res.getIdentifier("key_ai", "id", pkg))
?.setOnClickListener { vibrateKey(); env.showAiKeyboard() } ?.setOnClickListener { vibrateKey(); env.showAiKeyboard() }
numView.findViewById<View?>(res.getIdentifier("key_airole", "id", pkg))
?.setOnClickListener { vibrateKey(); env.toggleAiRolePanel() }
numView.findViewById<View?>(res.getIdentifier("key_space", "id", pkg)) numView.findViewById<View?>(res.getIdentifier("key_space", "id", pkg))
?.setOnClickListener { ?.setOnClickListener {
vibrateKey(); env.commitKey(' ') vibrateKey(); env.commitKey(' ')
@@ -148,11 +151,14 @@ class NumberKeyboard(
?.setOnClickListener { vibrateKey(); env.showEmojiKeyboard() } ?.setOnClickListener { vibrateKey(); env.showEmojiKeyboard() }
numView.findViewById<View?>(res.getIdentifier("key_revoke", "id", pkg)) numView.findViewById<View?>(res.getIdentifier("key_revoke", "id", pkg))
?.setOnClickListener { ?.setOnClickListener {
vibrateKey(); env.revokeLastClearedText() vibrateKey(); env.revokeLastClearedText()
// 回填后更新按钮可见性 // 回填后更新按钮可见性
updateRevokeButtonVisibility(numView, res, pkg) updateRevokeButtonVisibility(numView, res, pkg)
} }
// 为键盘行容器启用间隙触摸转发,点击按键间隙时触发最近按键
setupGapTouchForwardingForRows(numView)
} }
// 更新Revoke按钮的可见性 // 更新Revoke按钮的可见性

View File

@@ -140,6 +140,9 @@ class SymbolKeyboard(
symView.findViewById<View?>(res.getIdentifier("key_ai", "id", pkg)) symView.findViewById<View?>(res.getIdentifier("key_ai", "id", pkg))
?.setOnClickListener { vibrateKey(); env.showAiKeyboard() } ?.setOnClickListener { vibrateKey(); env.showAiKeyboard() }
symView.findViewById<View?>(res.getIdentifier("key_airole", "id", pkg))
?.setOnClickListener { vibrateKey(); env.toggleAiRolePanel() }
symView.findViewById<View?>(res.getIdentifier("collapse_button", "id", pkg)) symView.findViewById<View?>(res.getIdentifier("collapse_button", "id", pkg))
?.setOnClickListener { vibrateKey(); env.hideKeyboard() } ?.setOnClickListener { vibrateKey(); env.hideKeyboard() }
@@ -147,11 +150,14 @@ class SymbolKeyboard(
?.setOnClickListener { vibrateKey(); env.showEmojiKeyboard() } ?.setOnClickListener { vibrateKey(); env.showEmojiKeyboard() }
symView.findViewById<View?>(res.getIdentifier("key_revoke", "id", pkg)) symView.findViewById<View?>(res.getIdentifier("key_revoke", "id", pkg))
?.setOnClickListener { ?.setOnClickListener {
vibrateKey(); env.revokeLastClearedText() vibrateKey(); env.revokeLastClearedText()
// 回填后更新按钮可见性 // 回填后更新按钮可见性
updateRevokeButtonVisibility(symView, res, pkg) updateRevokeButtonVisibility(symView, res, pkg)
} }
// 为键盘行容器启用间隙触摸转发,点击按键间隙时触发最近按键
setupGapTouchForwardingForRows(symView)
} }
// 更新Revoke按钮的可见性 // 更新Revoke按钮的可见性

View File

@@ -32,6 +32,8 @@ private val NO_LOGIN_REQUIRED_PATHS = setOf(
"/user/detail", "/user/detail",
"/character/listByTag", "/character/listByTag",
"/character/list", "/character/list",
"/themes/listAllStyles",
"/ai-companion/page"
) )
private val NO_SIGN_REQUIRED_PATHS = setOf( private val NO_SIGN_REQUIRED_PATHS = setOf(

View File

@@ -30,6 +30,7 @@ data class LoginResponse(
val emailVerified: Boolean, val emailVerified: Boolean,
val isVip: Boolean, val isVip: Boolean,
val vipExpiry: String, val vipExpiry: String,
val vipLevel: Int?,
val token: String val token: String
) )
@@ -72,6 +73,7 @@ data class User(
val emailVerified: Boolean, val emailVerified: Boolean,
val isVip: Boolean, val isVip: Boolean,
val vipExpiry: String?, val vipExpiry: String?,
val vipLevel: Int?,
val token: String?, val token: String?,
) )

View File

@@ -85,6 +85,28 @@ class CircleChatRepository(
preloadRange(start, end, pageFetchSize, DEFAULT_CHAT_PAGE_SIZE) preloadRange(start, end, pageFetchSize, DEFAULT_CHAT_PAGE_SIZE)
} }
// 清除加载失败的缓存页面companionId <= 0使后续 preloadAround 能重新加载
fun invalidateFailedPages() {
synchronized(lock) {
val snapshot = cache.snapshot()
for ((position, page) in snapshot) {
if (page.companionId <= 0) {
cache.remove(position)
}
}
}
}
// 检查缓存中是否存在加载失败的页面
fun hasFailedPages(): Boolean {
synchronized(lock) {
for ((_, page) in cache.snapshot()) {
if (page.companionId <= 0) return true
}
}
return false
}
fun preloadInitialPages() { fun preloadInitialPages() {
val maxPages = availablePages val maxPages = availablePages
if (maxPages <= 0) return if (maxPages <= 0) return

View File

@@ -27,13 +27,17 @@ import androidx.core.widget.doAfterTextChanged
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.NetworkEvent
import com.example.myapplication.network.NetworkEventBus
import com.example.myapplication.network.chatMessageRequest import com.example.myapplication.network.chatMessageRequest
import com.example.myapplication.network.aiCompanionLikeRequest import com.example.myapplication.network.aiCompanionLikeRequest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -53,12 +57,15 @@ import androidx.core.content.ContextCompat
import android.widget.ImageView import android.widget.ImageView
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import com.example.myapplication.network.AiCompanion import com.example.myapplication.network.AiCompanion
import com.example.myapplication.keyboard.AiRolePreferences
import java.io.File import java.io.File
class CircleFragment : Fragment() { class CircleFragment : Fragment() {
private lateinit var pageRv: RecyclerView private lateinit var pageRv: RecyclerView
private lateinit var inputOverlay: View private lateinit var inputOverlay: View
private lateinit var noResultOverlay: View
private lateinit var imeDismissOverlay: View
private lateinit var inputContainerText: View private lateinit var inputContainerText: View
private lateinit var inputContainerVoice: View private lateinit var inputContainerVoice: View
private lateinit var inputEdit: EditText private lateinit var inputEdit: EditText
@@ -187,6 +194,8 @@ class CircleFragment : Fragment() {
pageRv = view.findViewById(R.id.pageRv) pageRv = view.findViewById(R.id.pageRv)
inputOverlay = view.findViewById(R.id.inputOverlay) inputOverlay = view.findViewById(R.id.inputOverlay)
noResultOverlay = view.findViewById(R.id.noResultOverlay)
imeDismissOverlay = view.findViewById(R.id.imeDismissOverlay)
inputContainerText = view.findViewById(R.id.inputContainerText) inputContainerText = view.findViewById(R.id.inputContainerText)
inputContainerVoice = view.findViewById(R.id.inputContainerVoice) inputContainerVoice = view.findViewById(R.id.inputContainerVoice)
inputEdit = view.findViewById(R.id.inputEdit) inputEdit = view.findViewById(R.id.inputEdit)
@@ -213,6 +222,7 @@ class CircleFragment : Fragment() {
drawerMenuRv = view.findViewById(R.id.rvDrawerMenu) drawerMenuRv = view.findViewById(R.id.rvDrawerMenu)
searchEdit = view.findViewById(R.id.etCircleSearch) searchEdit = view.findViewById(R.id.etCircleSearch)
searchIcon = view.findViewById(R.id.ivSearchIcon) searchIcon = view.findViewById(R.id.ivSearchIcon)
imeDismissOverlay.setOnClickListener { hideImeFromOverlay() }
setupDrawerMenu() setupDrawerMenu()
setupDrawerBlur() setupDrawerBlur()
if (drawerListener == null) { if (drawerListener == null) {
@@ -224,6 +234,14 @@ class CircleFragment : Fragment() {
} }
} }
override fun onDrawerStateChanged(newState: Int) {
if (newState == DrawerLayout.STATE_DRAGGING || newState == DrawerLayout.STATE_SETTLING) {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
hideImeFromDrawer()
}
}
}
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {
if (drawerView.id == R.id.circleSideDrawer) { if (drawerView.id == R.id.circleSideDrawer) {
lockOverlayForSheet(false) lockOverlayForSheet(false)
@@ -310,6 +328,7 @@ class CircleFragment : Fragment() {
repository.onTotalPagesChanged = { newTotal -> repository.onTotalPagesChanged = { newTotal ->
pageRv.post { pageRv.post {
pageAdapter.notifyDataSetChanged() pageAdapter.notifyDataSetChanged()
updateNoResultOverlay(newTotal)
if (newTotal <= 0) { if (newTotal <= 0) {
currentPage = RecyclerView.NO_POSITION currentPage = RecyclerView.NO_POSITION
} else if (currentPage >= newTotal) { } else if (currentPage >= newTotal) {
@@ -325,6 +344,7 @@ class CircleFragment : Fragment() {
pageRv.setHasFixedSize(true) pageRv.setHasFixedSize(true)
pageRv.itemAnimator = null pageRv.itemAnimator = null
pageRv.setItemViewCacheSize(computeViewCache(preloadCount)) pageRv.setItemViewCacheSize(computeViewCache(preloadCount))
updateNoResultOverlayFromFirstPage()
//设置滑动辅助器 //设置滑动辅助器
snapHelper = PagerSnapHelper() snapHelper = PagerSnapHelper()
@@ -386,11 +406,37 @@ class CircleFragment : Fragment() {
false false
} }
} }
// 监听网络恢复事件,重新加载数据
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
NetworkEventBus.events.collect { event ->
if (event is NetworkEvent.NetworkAvailable) {
loadDrawerMenuData()
retryFailedPages()
}
}
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
view?.post { requestOverlayUpdate() } view?.post { requestOverlayUpdate() }
// 检查是否有加载失败的页面,如果有则重新加载
retryFailedPages()
}
// 清除失败缓存并重新加载
private fun retryFailedPages() {
if (!::repository.isInitialized) return
if (!repository.hasFailedPages()) return
repository.invalidateFailedPages()
if (currentPage != RecyclerView.NO_POSITION) {
repository.preloadAround(currentPage)
}
pageAdapter.notifyDataSetChanged()
updateNoResultOverlayFromFirstPage()
} }
//清理和恢复输入框的高CircleFragment 在生命周期结束时的状态 //清理和恢复输入框的高CircleFragment 在生命周期结束时的状态
@@ -439,6 +485,44 @@ class CircleFragment : Fragment() {
sendButton.visibility = if (visible) View.VISIBLE else View.GONE sendButton.visibility = if (visible) View.VISIBLE else View.GONE
} }
private fun updateNoResultOverlay(pageCount: Int = repository.getAvailablePages()) {
if (!::noResultOverlay.isInitialized) return
setNoResultVisible(pageCount <= 0)
}
private fun updateNoResultOverlayFromFirstPage() {
if (!::repository.isInitialized) return
viewLifecycleOwner.lifecycleScope.launch {
val pageCount = repository.getAvailablePages()
if (pageCount <= 0) {
setNoResultVisible(true)
return@launch
}
val page = withContext(Dispatchers.IO) { repository.getPage(0) }
if (!isAdded) return@launch
setNoResultVisible(page.companionId <= 0)
}
}
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun hideImeFromOverlay() {
if (!isImeVisible) return
val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(inputEdit.windowToken, 0)
inputEdit.clearFocus()
setImeDismissOverlayVisible(false)
}
private fun hideImeFromDrawer() {
val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(searchEdit.windowToken, 0)
searchEdit.clearFocus()
}
private fun handleVoiceTouch(event: MotionEvent): Boolean { private fun handleVoiceTouch(event: MotionEvent): Boolean {
return when (event.actionMasked) { return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
@@ -906,6 +990,22 @@ class CircleFragment : Fragment() {
currentPage = position currentPage = position
CircleCommentSheet.clearCachedComments() CircleCommentSheet.clearCachedComments()
// 持久化当前角色信息到 SharedPreferences供输入法读取
val pageData = repository.getPage(position)
if (pageData.companionId > 0) {
AiRolePreferences.saveLastCompanion(
requireContext(), pageData.companionId,
pageData.personaName, pageData.avatarUrl
)
} else if (position < allCompanions.size) {
val comp = allCompanions[position]
if (comp.id > 0) {
AiRolePreferences.saveLastCompanion(
requireContext(), comp.id, comp.name, comp.avatarUrl
)
}
}
repository.preloadAround(position) repository.preloadAround(position)
val oldHolder = pageRv.findViewHolderForAdapterPosition(oldPos) as? PageViewHolder val oldHolder = pageRv.findViewHolderForAdapterPosition(oldPos) as? PageViewHolder
@@ -1014,6 +1114,7 @@ class CircleFragment : Fragment() {
val imeVisible = imeBottom > 0 val imeVisible = imeBottom > 0
isImeVisible = imeVisible isImeVisible = imeVisible
setImeDismissOverlayVisible(imeVisible)
bottomInset = if (imeVisible) 0 else systemBottom bottomInset = if (imeVisible) 0 else systemBottom
inputOverlay.translationY = 0f inputOverlay.translationY = 0f
@@ -1039,7 +1140,9 @@ class CircleFragment : Fragment() {
blur.visibility = View.GONE blur.visibility = View.GONE
} }
} else { } else {
restoreBottomNavVisibility() // 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE
nav.visibility = View.VISIBLE
prevBottomNavVisibility = null
if (forceHideBottomNavBlur) { if (forceHideBottomNavBlur) {
blur?.visibility = View.GONE blur?.visibility = View.GONE
} }
@@ -1130,11 +1233,19 @@ class CircleFragment : Fragment() {
private fun setImeHandlingEnabled(enabled: Boolean) { private fun setImeHandlingEnabled(enabled: Boolean) {
if (imeHandlingEnabled == enabled) return if (imeHandlingEnabled == enabled) return
imeHandlingEnabled = enabled imeHandlingEnabled = enabled
if (!enabled) {
setImeDismissOverlayVisible(false)
}
if (enabled) { if (enabled) {
view?.let { ViewCompat.requestApplyInsets(it) } view?.let { ViewCompat.requestApplyInsets(it) }
} }
} }
private fun setImeDismissOverlayVisible(visible: Boolean) {
if (!::imeDismissOverlay.isInitialized) return
imeDismissOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun lockOverlayForSheet(locked: Boolean) { private fun lockOverlayForSheet(locked: Boolean) {
if (isOverlayLocked == locked) return if (isOverlayLocked == locked) return
isOverlayLocked = locked isOverlayLocked = locked
@@ -1245,6 +1356,13 @@ class CircleFragment : Fragment() {
// 同步当前选中状态 // 同步当前选中状态
if (currentPage != RecyclerView.NO_POSITION && currentPage < allCompanions.size) { if (currentPage != RecyclerView.NO_POSITION && currentPage < allCompanions.size) {
drawerMenuAdapter.setSelectedPosition(currentPage) drawerMenuAdapter.setSelectedPosition(currentPage)
// 初始加载完成后,持久化当前页角色信息供输入法读取
val comp = allCompanions[currentPage]
if (comp.id > 0) {
AiRolePreferences.saveLastCompanion(
requireContext(), comp.id, comp.name, comp.avatarUrl
)
}
} }
} }
} }
@@ -1303,19 +1421,23 @@ class CircleFragment : Fragment() {
// 更新菜单选中状态 // 更新菜单选中状态
drawerMenuAdapter.setSelectedPosition(targetPosition) drawerMenuAdapter.setSelectedPosition(targetPosition)
// 关闭抽屉
drawerLayout.closeDrawer(GravityCompat.START)
// 隐藏键盘 // 隐藏键盘
val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(searchEdit.windowToken, 0) imm.hideSoftInputFromWindow(searchEdit.windowToken, 0)
searchEdit.clearFocus() searchEdit.clearFocus()
drawerLayout.closeDrawer(GravityCompat.START)
// 滚动到目标页面 // 滚动到目标页面
pageRv.scrollToPosition(targetPosition) pageRv.scrollToPosition(targetPosition)
currentPage = targetPosition currentPage = targetPosition
// 持久化当前角色信息到 SharedPreferences供输入法读取
AiRolePreferences.saveLastCompanion(
requireContext(), companion.id,
companion.name, companion.avatarUrl
)
// 预加载前后页面 // 预加载前后页面
repository.preloadAround(targetPosition) repository.preloadAround(targetPosition)
} }

View File

@@ -27,6 +27,7 @@ import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.ImeGuideActivity import com.example.myapplication.ImeGuideActivity
import com.example.myapplication.ui.common.LoadingOverlay import com.example.myapplication.ui.common.LoadingOverlay
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.utils.ImeUtils
import com.example.myapplication.network.AddPersonaClick import com.example.myapplication.network.AddPersonaClick
import com.example.myapplication.network.AuthEvent import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus import com.example.myapplication.network.AuthEventBus
@@ -46,6 +47,11 @@ import kotlin.math.abs
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
companion object {
// 本次进程生命周期内是否已弹过输入法引导,进程重启后自动重置
private var hasPromptedImeGuide = false
}
private lateinit var bottomSheet: MaterialCardView private lateinit var bottomSheet: MaterialCardView
private lateinit var bottomSheetBehavior: BottomSheetBehavior<MaterialCardView> private lateinit var bottomSheetBehavior: BottomSheetBehavior<MaterialCardView>
private lateinit var scrim: View private lateinit var scrim: View
@@ -56,6 +62,7 @@ class HomeFragment : Fragment() {
private lateinit var tabList1: TextView private lateinit var tabList1: TextView
private lateinit var tabList2: TextView private lateinit var tabList2: TextView
private lateinit var backgroundImage: ImageView private lateinit var backgroundImage: ImageView
private lateinit var noResultOverlay: View
private var lastList1RenderKey: String? = null private var lastList1RenderKey: String? = null
private lateinit var loadingOverlay: LoadingOverlay private lateinit var loadingOverlay: LoadingOverlay
@@ -63,6 +70,7 @@ class HomeFragment : Fragment() {
private var networkRefreshJob: Job? = null private var networkRefreshJob: Job? = null
private var allPersonaCache: List<listByTagWithNotLogin> = emptyList() private var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>() private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>()
private var list1Loaded = false
private var parentWidth = 0 private var parentWidth = 0
private var parentHeight = 0 private var parentHeight = 0
@@ -80,6 +88,11 @@ class HomeFragment : Fragment() {
private var sheetAdapter: SheetPagerAdapter? = null private var sheetAdapter: SheetPagerAdapter? = null
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
override fun onResume() {
super.onResume()
checkAndPromptImeGuide()
}
override fun onDestroyView() { override fun onDestroyView() {
preloadJob?.cancel() preloadJob?.cancel()
networkRefreshJob?.cancel() networkRefreshJob?.cancel()
@@ -109,11 +122,15 @@ class HomeFragment : Fragment() {
personaCache.clear() personaCache.clear()
allPersonaCache = emptyList() allPersonaCache = emptyList()
lastList1RenderKey = null lastList1RenderKey = null
list1Loaded = false
setNoResultVisible(false)
// 2) 重新拉列表1登录态接口会变 // 2) 重新拉列表1登录态接口会变
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
allPersonaCache = fetchAllPersonaList() allPersonaCache = fetchAllPersonaList()
list1Loaded = true
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
} }
// 3) 如果当前在某个 tag 页,也建议重新拉当前页数据 // 3) 如果当前在某个 tag 页,也建议重新拉当前页数据
@@ -124,6 +141,7 @@ class HomeFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
personaCache[tagId] = fetchPersonaByTag(tagId) personaCache[tagId] = fetchPersonaByTag(tagId)
notifyPageChangedOnMain(pos) notifyPageChangedOnMain(pos)
updateNoResultOverlay(pos)
} }
} }
} }
@@ -141,8 +159,10 @@ class HomeFragment : Fragment() {
try { try {
// 1) 列表一:重新拉 // 1) 列表一:重新拉
allPersonaCache = fetchAllPersonaList() allPersonaCache = fetchAllPersonaList()
list1Loaded = true
lastList1RenderKey = null lastList1RenderKey = null
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”) // 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
personaCache.clear() personaCache.clear()
@@ -154,11 +174,12 @@ class HomeFragment : Fragment() {
if (tagId != null) { if (tagId != null) {
// 先刷新一次,让页面进入 loading因为缓存被清了 // 先刷新一次,让页面进入 loading因为缓存被清了
notifyPageChangedOnMain(pos) notifyPageChangedOnMain(pos)
updateNoResultOverlay(pos)
// 再拉当前 tag 的新数据 // 再拉当前 tag 的新数据
val list = fetchPersonaByTag(tagId) val list = fetchPersonaByTag(tagId)
personaCache[tagId] = list personaCache[tagId] = list
notifyPageChangedOnMain(pos) notifyPageChangedOnMain(pos)
updateNoResultOverlay(pos)
} }
} }
@@ -176,8 +197,10 @@ class HomeFragment : Fragment() {
try { try {
// 1) 列表一:重新拉 // 1) 列表一:重新拉
allPersonaCache = fetchAllPersonaList() allPersonaCache = fetchAllPersonaList()
list1Loaded = true
lastList1RenderKey = null lastList1RenderKey = null
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”) // 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
personaCache.clear() personaCache.clear()
@@ -189,11 +212,12 @@ class HomeFragment : Fragment() {
if (tagId != null) { if (tagId != null) {
// 先刷新一次,让页面进入 loading因为缓存被清了 // 先刷新一次,让页面进入 loading因为缓存被清了
notifyPageChangedOnMain(pos) notifyPageChangedOnMain(pos)
updateNoResultOverlay(pos)
// 再拉当前 tag 的新数据 // 再拉当前 tag 的新数据
val list = fetchPersonaByTag(tagId) val list = fetchPersonaByTag(tagId)
personaCache[tagId] = list personaCache[tagId] = list
notifyPageChangedOnMain(pos) notifyPageChangedOnMain(pos)
updateNoResultOverlay(pos)
} }
} }
@@ -242,6 +266,7 @@ class HomeFragment : Fragment() {
viewPager.isSaveEnabled = false viewPager.isSaveEnabled = false
viewPager.offscreenPageLimit = 2 viewPager.offscreenPageLimit = 2
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage) backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay)
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator) val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage) val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
@@ -282,14 +307,18 @@ class HomeFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show() loadingOverlay.show()
try { try {
list1Loaded = false
setNoResultVisible(false)
val list = fetchAllPersonaList() val list = fetchAllPersonaList()
if (!isAdded) return@launch if (!isAdded) return@launch
allPersonaCache = list allPersonaCache = list
list1Loaded = true
// ✅ 关键:数据变了就清 renderKey允许重建一次 UI // ✅ 关键:数据变了就清 renderKey允许重建一次 UI
lastList1RenderKey = null lastList1RenderKey = null
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("1314520-HomeFragment", "获取列表一失败", e) Log.e("1314520-HomeFragment", "获取列表一失败", e)
} finally { } finally {
@@ -340,6 +369,7 @@ class HomeFragment : Fragment() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
if (!isAdded) return if (!isAdded) return
updateTabsAndTags(position) updateTabsAndTags(position)
updateNoResultOverlay(position)
// ✅ 修复当切换到标签页且缓存已有数据时强制刷新UI // ✅ 修复当切换到标签页且缓存已有数据时强制刷新UI
if (position > 0) { if (position > 0) {
@@ -362,6 +392,27 @@ class HomeFragment : Fragment() {
} }
} }
private fun updateNoResultOverlay(position: Int = viewPager.currentItem) {
if (!::noResultOverlay.isInitialized) return
if (!::viewPager.isInitialized) return
if (position != viewPager.currentItem) return
val show = when (position) {
0 -> list1Loaded && allPersonaCache.isEmpty()
else -> {
val tagId = tags.getOrNull(position - 1)?.id ?: return setNoResultVisible(false)
val cached = personaCache[tagId] ?: return setNoResultVisible(false)
cached.isEmpty()
}
}
setNoResultVisible(show)
}
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
// ---------------- 方案A成功后“造新数据(copy)替换缓存”并刷新 ---------------- // ---------------- 方案A成功后“造新数据(copy)替换缓存”并刷新 ----------------
private fun applyAddedToggle(personaId: Int, newAdded: Boolean) { private fun applyAddedToggle(personaId: Int, newAdded: Boolean) {
@@ -378,6 +429,7 @@ class HomeFragment : Fragment() {
// renderList1 有 renderKey必须清一下 // renderList1 有 renderKey必须清一下
lastList1RenderKey = null lastList1RenderKey = null
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
} }
} }
@@ -405,6 +457,7 @@ class HomeFragment : Fragment() {
if (changedCurrentTagPage) { if (changedCurrentTagPage) {
notifyPageChangedOnMain(viewPager.currentItem) notifyPageChangedOnMain(viewPager.currentItem)
updateNoResultOverlay(viewPager.currentItem)
} }
} }
@@ -660,12 +713,16 @@ class HomeFragment : Fragment() {
preloadJob?.cancel() preloadJob?.cancel()
loadingOverlay.show() loadingOverlay.show()
try { try {
list1Loaded = false
setNoResultVisible(false)
val list = fetchAllPersonaList() val list = fetchAllPersonaList()
if (!isAdded) return@launch if (!isAdded) return@launch
allPersonaCache = list allPersonaCache = list
list1Loaded = true
lastList1RenderKey = null lastList1RenderKey = null
personaCache.clear() personaCache.clear()
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
val response = RetrofitClient.apiService.tagList() val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch if (!isAdded) return@launch
@@ -709,6 +766,7 @@ class HomeFragment : Fragment() {
val thisPos = 1 + idx val thisPos = 1 + idx
if (idx >= 0 && viewPager.currentItem == thisPos) { if (idx >= 0 && viewPager.currentItem == thisPos) {
notifyPageChangedOnMain(thisPos) notifyPageChangedOnMain(thisPos)
updateNoResultOverlay(thisPos)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -1092,6 +1150,18 @@ class HomeFragment : Fragment() {
return EncryptedSharedPreferencesUtil.contains(ctx, "user") return EncryptedSharedPreferencesUtil.contains(ctx, "user")
} }
/**
* 检查输入法是否已激活并选中,未就绪则跳转引导页。
* 本次 App 进程内只弹一次,用户返回后不再弹,下次启动 App 再检查。
*/
private fun checkAndPromptImeGuide() {
if (hasPromptedImeGuide) return
val ctx = context ?: return
if (ImeUtils.isImeReady(ctx)) return
hasPromptedImeGuide = true
startActivity(Intent(ctx, ImeGuideActivity::class.java))
}
// ✅ 统一安全导航stateSaved 防护(切很快/后台回来时很重要) // ✅ 统一安全导航stateSaved 防护(切很快/后台回来时很重要)
private fun safeNavigate(actionId: Int) { private fun safeNavigate(actionId: Int) {
if (!isAdded) return if (!isAdded) return

View File

@@ -32,6 +32,7 @@ class MyKeyboard : Fragment() {
private lateinit var adapter: KeyboardAdapter private lateinit var adapter: KeyboardAdapter
private lateinit var loadingOverlay: LoadingOverlay private lateinit var loadingOverlay: LoadingOverlay
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var noResultOverlay: View
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -49,6 +50,7 @@ class MyKeyboard : Fragment() {
rv = view.findViewById(R.id.rv_keyboard) rv = view.findViewById(R.id.rv_keyboard)
btnSave = view.findViewById(R.id.btn_keyboard) btnSave = view.findViewById(R.id.btn_keyboard)
swipeRefresh = view.findViewById(R.id.swipeRefresh) swipeRefresh = view.findViewById(R.id.swipeRefresh)
noResultOverlay = view.findViewById(R.id.noResultOverlay)
swipeRefresh.setOnRefreshListener { loadList() } swipeRefresh.setOnRefreshListener { loadList() }
adapter = KeyboardAdapter( adapter = KeyboardAdapter(
@@ -122,18 +124,34 @@ class MyKeyboard : Fragment() {
val resp = getlistByUser() val resp = getlistByUser()
if (resp?.code == 0 && resp.data != null) { if (resp?.code == 0 && resp.data != null) {
adapter.submitList(resp.data) adapter.submitList(resp.data)
updateNoResultOverlay(resp.data.size)
Log.d("1314520-list", resp.data.toString()) Log.d("1314520-list", resp.data.toString())
} else { } else {
if (adapter.itemCount == 0) updateNoResultOverlay(0)
Toast.makeText(requireContext(), resp?.message ?: getString(R.string.Pop_up_window_my_keyboard_3), Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), resp?.message ?: getString(R.string.Pop_up_window_my_keyboard_3), Toast.LENGTH_SHORT).show()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.d("MyKeyboard-loadList", e.message.toString()) Log.d("MyKeyboard-loadList", e.message.toString())
if (adapter.itemCount == 0) updateNoResultOverlay(0)
} finally { } finally {
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
} }
} }
private fun updateNoResultOverlay(count: Int = adapter.itemCount) {
if (!::noResultOverlay.isInitialized) return
setNoResultVisible(count <= 0)
}
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
if (::btnSave.isInitialized) {
btnSave.visibility = if (visible) View.GONE else View.VISIBLE
}
}
// 获取用户人设列表 // 获取用户人设列表
private suspend fun getlistByUser(): ApiResponse<List<ListByUserWithNot>>? = private suspend fun getlistByUser(): ApiResponse<List<ListByUserWithNot>>? =
runCatching { RetrofitClient.apiService.listByUser() }.getOrNull() runCatching { RetrofitClient.apiService.listByUser() }.getOrNull()

View File

@@ -6,6 +6,7 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -91,6 +92,25 @@ class LoginFragment : Fragment() {
emailEditText.setText(email) emailEditText.setText(email)
} }
// 邮箱输入框:禁止换行,回车跳转到密码框
emailEditText.setSingleLine(true)
emailEditText.imeOptions = EditorInfo.IME_ACTION_NEXT
emailEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
passwordEditText.requestFocus()
true
} else false
}
// 密码输入框:回车直接触发登录
passwordEditText.imeOptions = EditorInfo.IME_ACTION_GO
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_GO) {
loginButton.performClick()
true
} else false
}
// 初始是隐藏密码状态 // 初始是隐藏密码状态
passwordEditText.inputType = passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
@@ -117,7 +137,7 @@ class LoginFragment : Fragment() {
// // 登录按钮逻辑 // // 登录按钮逻辑
loginButton.setOnClickListener { loginButton.setOnClickListener {
val pwd = passwordEditText.text?.toString().orEmpty() val pwd = passwordEditText.text?.toString().orEmpty()
val email = emailEditText.text?.toString().orEmpty() val email = emailEditText.text?.toString().orEmpty().trim()
if (pwd.isEmpty() || email.isEmpty()) { if (pwd.isEmpty() || email.isEmpty()) {
// 输入框不能为空 // 输入框不能为空
Toast.makeText(requireContext(), getString(R.string.Pop_up_window_LoginFragment_1), Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), getString(R.string.Pop_up_window_LoginFragment_1), Toast.LENGTH_SHORT).show()

View File

@@ -3,6 +3,7 @@ package com.example.myapplication.ui.mine
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -41,6 +42,7 @@ import com.example.myapplication.network.BehaviorReporter
class MineFragment : Fragment() { class MineFragment : Fragment() {
private lateinit var nickname: TextView private lateinit var nickname: TextView
private lateinit var vipIcon: ImageView
private lateinit var time: TextView private lateinit var time: TextView
private lateinit var logout: TextView private lateinit var logout: TextView
private lateinit var avatar: CircleImageView private lateinit var avatar: CircleImageView
@@ -69,6 +71,7 @@ class MineFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
nickname = view.findViewById(R.id.nickname) nickname = view.findViewById(R.id.nickname)
vipIcon = view.findViewById(R.id.vip_icon)
time = view.findViewById(R.id.time) time = view.findViewById(R.id.time)
logout = view.findViewById(R.id.logout) logout = view.findViewById(R.id.logout)
avatar = view.findViewById(R.id.avatar) avatar = view.findViewById(R.id.avatar)
@@ -82,17 +85,25 @@ class MineFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show() loadingOverlay.show()
try { try {
val response = getinviteCode() val response = getinviteCode()
response?.data?.h5Link?.let { link -> response?.data?.h5Link?.let { link ->
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("h5Link", link)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, getString(R.string.copy_invite_link_success), Toast.LENGTH_LONG).show()
BehaviorReporter.report( BehaviorReporter.report(
isNewUser = false, isNewUser = false,
"page_id" to "my", "page_id" to "my",
"element_id" to "invite_copy", "element_id" to "invite_copy",
) )
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, link)
}
val chooser = Intent.createChooser(shareIntent, null)
if (chooser.resolveActivity(requireContext().packageManager) != null) {
startActivity(chooser)
} else {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("h5Link", link))
Toast.makeText(context, getString(R.string.copy_invite_link_success), Toast.LENGTH_LONG).show()
}
} }
} finally { } finally {
loadingOverlay.hide() loadingOverlay.hide()
@@ -216,6 +227,7 @@ class MineFragment : Fragment() {
) )
nickname.text = cached?.nickName ?: "" nickname.text = cached?.nickName ?: ""
time.text = cached?.vipExpiry?.let { getString(R.string.mine_vip_due_on, it) } ?: "" time.text = cached?.vipExpiry?.let { getString(R.string.mine_vip_due_on, it) } ?: ""
renderVip(cached?.isVip, cached?.vipLevel)
cached?.avatarUrl?.let { url -> cached?.avatarUrl?.let { url ->
Glide.with(requireContext()) Glide.with(requireContext())
.load(url) .load(url)
@@ -247,6 +259,8 @@ class MineFragment : Fragment() {
time.text = u?.vipExpiry?.let { getString(R.string.mine_vip_due_on, it) } ?: "" time.text = u?.vipExpiry?.let { getString(R.string.mine_vip_due_on, it) } ?: ""
renderVip(u?.isVip, u?.vipLevel)
u?.avatarUrl?.let { url -> u?.avatarUrl?.let { url ->
Glide.with(requireContext()) Glide.with(requireContext())
.load(url) .load(url)
@@ -277,6 +291,7 @@ class MineFragment : Fragment() {
// 清空 UI // 清空 UI
nickname.text = "" nickname.text = ""
time.text = "" time.text = ""
renderVip(false, null)
Glide.with(requireContext()) Glide.with(requireContext())
.load(R.drawable.default_avatar) .load(R.drawable.default_avatar)
.into(avatar) .into(avatar)
@@ -292,6 +307,22 @@ class MineFragment : Fragment() {
} }
} }
private fun renderVip(isVip: Boolean?, vipLevel: Int?) {
val show = isVip == true && (vipLevel == 1 || vipLevel == 2)
if (!show) {
vipIcon.visibility = View.GONE
return
}
vipIcon.visibility = View.VISIBLE
val iconRes = if (vipLevel == 2) {
R.drawable.mine_svip_icon
} else {
R.drawable.mine_vip_icon
}
vipIcon.setImageResource(iconRes)
}
private fun isLoggedIn(): Boolean { private fun isLoggedIn(): Boolean {
val ctx = context ?: return false val ctx = context ?: return false
return EncryptedSharedPreferencesUtil.contains(ctx, "user") return EncryptedSharedPreferencesUtil.contains(ctx, "user")

View File

@@ -4,7 +4,6 @@ import android.graphics.Color
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -12,29 +11,17 @@ import com.example.myapplication.R
import com.example.myapplication.network.TransactionRecord import com.example.myapplication.network.TransactionRecord
class TransactionAdapter( class TransactionAdapter(
private val data: MutableList<TransactionRecord>, private val data: MutableList<TransactionRecord>
private val onCloseClick: () -> Unit,
private val onRechargeClick: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object { companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1 private const val TYPE_ITEM = 1
private const val TYPE_FOOTER = 2 private const val TYPE_FOOTER = 2
} }
// Header: balance
private var headerBalanceText: String = "0.00"
// Footer state
private var showFooter: Boolean = false private var showFooter: Boolean = false
private var footerNoMore: Boolean = false private var footerNoMore: Boolean = false
fun updateHeaderBalance(text: Any?) {
headerBalanceText = (text ?: "0.00").toString()
notifyItemChanged(0)
}
fun setFooterLoading() { fun setFooterLoading() {
showFooter = true showFooter = true
footerNoMore = false footerNoMore = false
@@ -61,19 +48,17 @@ class TransactionAdapter(
fun append(list: List<TransactionRecord>) { fun append(list: List<TransactionRecord>) {
if (list.isEmpty()) return if (list.isEmpty()) return
val start = 1 + data.size // header占1 val start = data.size
data.addAll(list) data.addAll(list)
notifyItemRangeInserted(start, list.size) notifyItemRangeInserted(start, list.size)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
// header + items + optional footer return data.size + if (showFooter) 1 else 0
return 1 + data.size + if (showFooter) 1 else 0
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when { return when {
position == 0 -> TYPE_HEADER
showFooter && position == itemCount - 1 -> TYPE_FOOTER showFooter && position == itemCount - 1 -> TYPE_FOOTER
else -> TYPE_ITEM else -> TYPE_ITEM
} }
@@ -82,10 +67,6 @@ class TransactionAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
TYPE_HEADER -> {
val v = inflater.inflate(R.layout.layout_consumption_record_header, parent, false)
HeaderVH(v, onCloseClick, onRechargeClick)
}
TYPE_FOOTER -> { TYPE_FOOTER -> {
val v = inflater.inflate(R.layout.item_loading_footer, parent, false) val v = inflater.inflate(R.layout.item_loading_footer, parent, false)
FooterVH(v) FooterVH(v)
@@ -99,41 +80,8 @@ class TransactionAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is HeaderVH -> holder.bind(headerBalanceText)
is FooterVH -> holder.bind(footerNoMore) is FooterVH -> holder.bind(footerNoMore)
is ItemVH -> holder.bind(data[position - 1]) // position-1 because header is ItemVH -> holder.bind(data[position])
}
}
class HeaderVH(
itemView: View,
onCloseClick: () -> Unit,
onRechargeClick: () -> Unit
) : RecyclerView.ViewHolder(itemView) {
private val balance: TextView = itemView.findViewById(R.id.balance)
init {
itemView.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener { onCloseClick() }
itemView.findViewById<View>(R.id.rechargeButton).setOnClickListener { onRechargeClick() }
}
fun bind(balanceText: String) {
balance.text = balanceText
adjustBalanceTextSize(balance, balanceText)
}
private fun adjustBalanceTextSize(tv: TextView, text: String) {
tv.textSize = when (text.length) {
in 0..3 -> 40f
4 -> 36f
5 -> 32f
6 -> 28f
7 -> 24f
8 -> 22f
9 -> 20f
else -> 16f
}
} }
} }
@@ -146,12 +94,11 @@ class TransactionAdapter(
tvTime.text = item.createdAt tvTime.text = item.createdAt
tvDesc.text = item.description tvDesc.text = item.description
tvAmount.text = "${item.amount}" tvAmount.text = "${item.amount}"
// 根据type设置字体颜色
val color = when (item.type) { val color = when (item.type) {
1 -> Color.parseColor("#CD2853") // 收入 - 红色 1 -> Color.parseColor("#CD2853")
2 -> Color.parseColor("#66CD7C") // 支出 - 绿色 2 -> Color.parseColor("#66CD7C")
else -> tvAmount.currentTextColor // 保持当前颜色 else -> tvAmount.currentTextColor
} }
tvAmount.setTextColor(color) tvAmount.setTextColor(color)
} }

View File

@@ -6,6 +6,8 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -28,6 +30,8 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var rv: RecyclerView private lateinit var rv: RecyclerView
private lateinit var adapter: TransactionAdapter private lateinit var adapter: TransactionAdapter
private lateinit var noResultOverlay: View
private lateinit var balance: TextView
private val listData = arrayListOf<TransactionRecord>() private val listData = arrayListOf<TransactionRecord>()
@@ -49,6 +53,12 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
swipeRefresh = view.findViewById(R.id.swipeRefresh) swipeRefresh = view.findViewById(R.id.swipeRefresh)
rv = view.findViewById(R.id.rvTransactions) rv = view.findViewById(R.id.rvTransactions)
noResultOverlay = view.findViewById(R.id.noResultOverlay)
balance = view.findViewById(R.id.balance)
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener { closeByNav() }
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
setupRecycler() setupRecycler()
setupRefresh() setupRefresh()
@@ -83,11 +93,7 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
private fun setupRecycler() { private fun setupRecycler() {
adapter = TransactionAdapter( adapter = TransactionAdapter(
data = listData, data = listData
onCloseClick = { closeByNav() }, // ✅ 改这里:不要 dismiss()
onRechargeClick = {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
) )
rv.layoutManager = LinearLayoutManager(requireContext()) rv.layoutManager = LinearLayoutManager(requireContext())
@@ -122,12 +128,14 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
totalPages = Int.MAX_VALUE totalPages = Int.MAX_VALUE
isLoading = false isLoading = false
val hadData = listData.isNotEmpty()
adapter.hideFooter() adapter.hideFooter()
adapter.replaceAll(emptyList()) adapter.replaceAll(emptyList())
if (hadData) setNoResultVisible(false)
val walletResp = getwalletBalance() val walletResp = getwalletBalance()
val balanceText = walletResp?.data?.balanceDisplay ?: "0.00" val balanceText = walletResp?.data?.balanceDisplay ?: "0.00"
adapter.updateHeaderBalance(balanceText) updateHeaderBalance(balanceText)
loadPage(targetPage = 1, isRefresh = true) loadPage(targetPage = 1, isRefresh = true)
@@ -156,6 +164,7 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
val records = data.records val records = data.records
if (isRefresh) adapter.replaceAll(records) else adapter.append(records) if (isRefresh) adapter.replaceAll(records) else adapter.append(records)
updateNoResultOverlay(records.isEmpty() && listData.isEmpty())
if (pageNum >= totalPages) adapter.setFooterNoMore() else adapter.hideFooter() if (pageNum >= totalPages) adapter.setFooterNoMore() else adapter.hideFooter()
@@ -167,6 +176,7 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
} }
} else { } else {
adapter.hideFooter() adapter.hideFooter()
if (listData.isEmpty()) updateNoResultOverlay(true)
} }
isLoading = false isLoading = false
@@ -178,4 +188,34 @@ class ConsumptionRecordFragment : BottomSheetDialogFragment() {
private suspend fun gettransactions(body: transactionsRequest): ApiResponse<transactionsResponse>? = private suspend fun gettransactions(body: transactionsRequest): ApiResponse<transactionsResponse>? =
runCatching { RetrofitClient.apiService.transactions(body) }.getOrNull() runCatching { RetrofitClient.apiService.transactions(body) }.getOrNull()
private fun updateHeaderBalance(text: Any?) {
val value = (text ?: "0.00").toString()
balance.text = value
adjustBalanceTextSize(balance, value)
}
private fun adjustBalanceTextSize(tv: TextView, text: String) {
tv.textSize = when (text.length) {
in 0..3 -> 40f
4 -> 36f
5 -> 32f
6 -> 28f
7 -> 24f
8 -> 22f
9 -> 20f
else -> 16f
}
}
private fun updateNoResultOverlay(show: Boolean) {
if (!::noResultOverlay.isInitialized) return
setNoResultVisible(show)
}
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
} }

View File

@@ -53,6 +53,7 @@ class PersonalSettings : BottomSheetDialogFragment() {
private lateinit var tvUserId: TextView private lateinit var tvUserId: TextView
private lateinit var loadingOverlay: LoadingOverlay private lateinit var loadingOverlay: LoadingOverlay
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var noResultOverlay: View
/** /**
* ✅ Android Photo Picker * ✅ Android Photo Picker
@@ -87,6 +88,7 @@ class PersonalSettings : BottomSheetDialogFragment() {
loadingOverlay = LoadingOverlay.attach(requireView() as ViewGroup) loadingOverlay = LoadingOverlay.attach(requireView() as ViewGroup)
swipeRefresh = view.findViewById(R.id.swipeRefresh) swipeRefresh = view.findViewById(R.id.swipeRefresh)
swipeRefresh.setOnRefreshListener { loadUser() } swipeRefresh.setOnRefreshListener { loadUser() }
noResultOverlay = view.findViewById(R.id.noResultOverlay)
// 关闭 // 关闭
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener { view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
@@ -186,14 +188,17 @@ class PersonalSettings : BottomSheetDialogFragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
try { try {
if (user != null) setNoResultVisible(false)
val resp = getUserdata() val resp = getUserdata()
val u = resp?.data // ???????????? ApiResponse ???????????? data?????????????????????????????? val u = resp?.data // ???????????? ApiResponse ???????????? data??????????????????????????????
if (u == null) { if (u == null) {
Toast.makeText(requireContext(), getString(R.string.Pop_up_window_my_keyboard_3), Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), getString(R.string.Pop_up_window_my_keyboard_3), Toast.LENGTH_SHORT).show()
setNoResultVisible(true)
return@launch return@launch
} }
user = u user = u
renderUser(u) renderUser(u)
setNoResultVisible(false)
} finally { } finally {
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
@@ -417,4 +422,9 @@ class PersonalSettings : BottomSheetDialogFragment() {
loadingOverlay.remove() loadingOverlay.remove()
super.onDestroyView() super.onDestroyView()
} }
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
} }

View File

@@ -27,6 +27,7 @@ import com.example.myapplication.network.*
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.network.BehaviorReporter
@@ -42,10 +43,12 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
private lateinit var balance: TextView private lateinit var balance: TextView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var shopTitle: TextView private lateinit var shopTitle: TextView
private lateinit var noResultOverlay: View
// ===== Data ===== // ===== Data =====
private var tabTitles: List<Theme> = emptyList() private var tabTitles: List<Theme> = emptyList()
private var styleIds: List<Int> = emptyList() private var styleIds: List<Int> = emptyList()
private var themeListLoaded = false
// ===== ViewModel ===== // ===== ViewModel =====
private lateinit var vm: ShopViewModel private lateinit var vm: ShopViewModel
@@ -72,6 +75,14 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.styleData.collect {
updateNoResultOverlay()
}
}
}
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
ShopEventBus.events.collect { event -> ShopEventBus.events.collect { event ->
@@ -142,16 +153,24 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
updateBalance(getwalletBalance()) updateBalance(getwalletBalance())
// 主题 // 主题
themeListLoaded = false
setNoResultVisible(false)
val themeResp = getThemeList() val themeResp = getThemeList()
tabTitles = themeResp?.data ?: emptyList() tabTitles = themeResp?.data ?: emptyList()
styleIds = tabTitles.map { it.id } styleIds = tabTitles.map { it.id }
themeListLoaded = true
// Fragment 可能在语言切换重建时被销毁/未附着,避免在未附着状态下创建子 Fragment // Fragment 可能在语言切换重建时被销毁/未附着,避免在未附着状态下创建子 Fragment
if (!isAdded) return@launch if (!isAdded) return@launch
setupViewPagerOnce() setupViewPagerOnce()
setupTagsOnce() setupTagsOnce()
updateNoResultOverlay(0)
styleIds.firstOrNull()?.let { vm.loadStyleIfNeeded(it) } styleIds.firstOrNull()?.let { vm.loadStyleIfNeeded(it) }
} }
} }
@@ -168,20 +187,31 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
try { try {
updateBalance(getwalletBalance()) updateBalance(getwalletBalance())
themeListLoaded = false
setNoResultVisible(false)
val newThemes = getThemeList()?.data ?: emptyList() val newThemes = getThemeList()?.data ?: emptyList()
if (!isAdded) return@launch if (!isAdded) return@launch
if (newThemes != tabTitles) { if (newThemes != tabTitles) {
tabTitles = newThemes tabTitles = newThemes
styleIds = tabTitles.map { it.id } styleIds = tabTitles.map { it.id }
themeListLoaded = true
setupViewPagerOnce(force = true) setupViewPagerOnce(force = true)
setupTagsOnce(force = true) setupTagsOnce(force = true)
updateNoResultOverlay(0)
vm.clearCache() vm.clearCache()
styleIds.forEach { vm.forceLoadStyle(it) } styleIds.forEach { vm.forceLoadStyle(it) }
} else { } else {
themeListLoaded = true
styleIds.getOrNull(viewPager.currentItem) styleIds.getOrNull(viewPager.currentItem)
?.let { vm.forceLoadStyle(it) } ?.let { vm.forceLoadStyle(it) }
updateNoResultOverlay(viewPager.currentItem)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ShopFragment", "refresh error", e) Log.e("ShopFragment", "refresh error", e)
@@ -207,6 +237,7 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
updateTagState(position) updateTagState(position)
styleIds.getOrNull(position)?.let { vm.loadStyleIfNeeded(it) } styleIds.getOrNull(position)?.let { vm.loadStyleIfNeeded(it) }
updateNoResultOverlay(position)
} }
} }
viewPager.registerOnPageChangeCallback(pageCallback!!) viewPager.registerOnPageChangeCallback(pageCallback!!)
@@ -268,6 +299,34 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
} }
} }
private fun updateNoResultOverlay(position: Int = viewPager.currentItem) {
if (!::noResultOverlay.isInitialized) return
if (!::viewPager.isInitialized) return
if (!themeListLoaded) {
setNoResultVisible(false)
return
}
if (styleIds.isEmpty()) {
setNoResultVisible(true)
return
}
val styleId = styleIds.getOrNull(position) ?: run {
setNoResultVisible(false)
return
}
val cached = vm.styleData.value[styleId] ?: run {
setNoResultVisible(false)
return
}
setNoResultVisible(cached.isEmpty())
}
private fun setNoResultVisible(visible: Boolean) {
if (!::noResultOverlay.isInitialized) return
noResultOverlay.visibility = if (visible) View.VISIBLE else View.GONE
}
// ========================== UI helpers ========================== // ========================== UI helpers ==========================
private fun updateBalance(walletResp: ApiResponse<Wallet>?) { private fun updateBalance(walletResp: ApiResponse<Wallet>?) {
@@ -427,6 +486,7 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
tagContainer = view.findViewById(R.id.tagContainer) tagContainer = view.findViewById(R.id.tagContainer)
balance = view.findViewById(R.id.balance) balance = view.findViewById(R.id.balance)
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout) swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
noResultOverlay = view.findViewById(R.id.noResultOverlay)
swipeRefreshLayout.isEnabled = true swipeRefreshLayout.isEnabled = true
swipeRefreshLayout.setColorSchemeColors( swipeRefreshLayout.setColorSchemeColors(

View File

@@ -27,6 +27,9 @@ class MySkin : Fragment() {
private lateinit var adapter: MySkinAdapter private lateinit var adapter: MySkinAdapter
private lateinit var swipeRefreshLayout: SwipeRefreshLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var noResultOverlay: View
private lateinit var tvEditor: TextView
private lateinit var tvTitle: TextView
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -39,11 +42,13 @@ class MySkin : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val tvEditor = view.findViewById<TextView>(R.id.tvEditor) tvEditor = view.findViewById<TextView>(R.id.tvEditor)
tvTitle = view.findViewById<TextView>(R.id.tvTitle)
val bottomBar = view.findViewById<View>(R.id.bottomEditBar) val bottomBar = view.findViewById<View>(R.id.bottomEditBar)
val tvSelectedCount = view.findViewById<TextView>(R.id.tvSelectedCount) val tvSelectedCount = view.findViewById<TextView>(R.id.tvSelectedCount)
val btnDelete = view.findViewById<TextView>(R.id.btnDelete) val btnDelete = view.findViewById<TextView>(R.id.btnDelete)
swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout) swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
noResultOverlay = view.findViewById(R.id.noResultOverlay)
// 设置下拉刷新监听器 // 设置下拉刷新监听器
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
@@ -141,6 +146,7 @@ class MySkin : Fragment() {
adapter.exitEditMode() adapter.exitEditMode()
tvEditor.text = "Editor" tvEditor.text = "Editor"
hideBottomBar() hideBottomBar()
updateNoResultOverlay()
} }
} }
} }
@@ -153,6 +159,7 @@ class MySkin : Fragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val resp = getPurchasedThemeList() val resp = getPurchasedThemeList()
adapter.submitList(resp?.data ?: emptyList()) adapter.submitList(resp?.data ?: emptyList())
updateNoResultOverlay()
} }
} }
@@ -161,9 +168,11 @@ class MySkin : Fragment() {
try { try {
val resp = getPurchasedThemeList() val resp = getPurchasedThemeList()
adapter.submitList(resp?.data ?: emptyList()) adapter.submitList(resp?.data ?: emptyList())
updateNoResultOverlay()
Log.d("1314520-MySkin", "下拉刷新完成") Log.d("1314520-MySkin", "下拉刷新完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("1314520-MySkin", "下拉刷新失败", e) Log.e("1314520-MySkin", "下拉刷新失败", e)
updateNoResultOverlay()
} finally { } finally {
// 停止刷新动画 // 停止刷新动画
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
@@ -171,7 +180,29 @@ class MySkin : Fragment() {
} }
} }
private suspend fun getPurchasedThemeList(): ApiResponse<List<themeStyle>>? {
private fun updateNoResultOverlay() {
if (!::noResultOverlay.isInitialized) return
val empty = adapter.itemCount <= 0
noResultOverlay.visibility = if (empty) View.VISIBLE else View.GONE
if (::tvEditor.isInitialized) {
tvEditor.visibility = if (empty) View.GONE else View.VISIBLE
}
if (::tvTitle.isInitialized) {
val endMargin = if (empty) {
resources.getDimensionPixelSize(R.dimen.sw_49dp)
} else {
0
}
val params = tvTitle.layoutParams as? ViewGroup.MarginLayoutParams
if (params != null && params.marginEnd != endMargin) {
params.marginEnd = endMargin
tvTitle.layoutParams = params
}
}
}
private suspend fun getPurchasedThemeList(): ApiResponse<List<themeStyle>>? {
return try { RetrofitClient.apiService.purchasedThemeList() } return try { RetrofitClient.apiService.purchasedThemeList() }
catch (e: Exception) { Log.e("MySkin", "获取已购买主题失败", e); null } catch (e: Exception) { Log.e("MySkin", "获取已购买主题失败", e); null }
} }

View File

@@ -0,0 +1,58 @@
package com.example.myapplication.utils
import android.content.ComponentName
import android.content.Context
import android.provider.Settings
import android.util.Log
import android.view.inputmethod.InputMethodManager
import com.example.myapplication.MyInputMethodService
/**
* 输入法状态检查工具
*/
object ImeUtils {
private const val TAG = "ImeUtils"
/** 本应用输入法是否已在系统中启用 */
fun isImeEnabled(context: Context): Boolean {
return try {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val myComponent = ComponentName(context, MyInputMethodService::class.java)
imm.enabledInputMethodList.any { imeInfo ->
imeInfo.packageName == myComponent.packageName &&
imeInfo.serviceName == myComponent.className
}
} catch (e: Exception) {
Log.e(TAG, "isImeEnabled 出错", e)
false
}
}
/** 本应用输入法是否已被选为当前默认输入法 */
fun isImeSelected(context: Context): Boolean {
return try {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val myComponent = ComponentName(context, MyInputMethodService::class.java)
val currentImeId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
) ?: return false
val currentImeInfo = imm.enabledInputMethodList.firstOrNull { imeInfo ->
imeInfo.id == currentImeId
} ?: return false
currentImeInfo.packageName == myComponent.packageName &&
currentImeInfo.serviceName == myComponent.className
} catch (e: Exception) {
Log.e(TAG, "isImeSelected 出错", e)
false
}
}
/** 输入法是否已完全就绪(启用且选中) */
fun isImeReady(context: Context): Boolean {
return isImeEnabled(context) && isImeSelected(context)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/sw_24dp"
android:height="@dimen/sw_24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1B1F1A"
android:pathData="M11.99 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm6.93 6h-2.95c-.19-1.26-.54-2.45-1.03-3.5 1.84.74 3.27 2.27 3.98 4.1zm-6.93-4.9c.86 1.25 1.45 2.7 1.73 4.28h-3.46c.28-1.57.87-3.02 1.73-4.28zm-7.46 9.4c-.2-.63-.33-1.3-.33-2 0-.7.12-1.37.33-2h2.71c-.06.66-.1 1.32-.1 2 0 .68.04 1.34.1 2h-2.71zm.53 2h2.95c.19 1.26.54 2.45 1.03 3.5-1.84-.74-3.27-2.27-3.98-4.1zm2.95-6h-2.95c.71-1.83 2.14-3.36 3.98-4.1-.49 1.05-.84 2.24-1.03 3.5zm4.98 9.9c-.86-1.25-1.45-2.7-1.73-4.28h3.46c-.28 1.57-.87 3.02-1.73 4.28zm2.1-6.28h-4.2c-.07-.66-.11-1.32-.11-2 0-.68.04-1.34.11-2h4.19c.07.66.11 1.32.11 2 0 .68-.04 1.34-.1 2zm.26 2h2.95c-.71 1.83-2.14 3.36-3.98 4.1.49-1.05.84-2.24 1.03-3.5zm0-4h2.95c.2.63.33 1.3.33 2 0 .7-.12 1.37-.33 2h-2.95c.06-.66.1-1.32.1-2 0-.68-.04-1.34-.1-2z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -39,7 +39,7 @@
<TextView <TextView
android:id="@+id/stepTips" android:id="@+id/stepTips"
android:textSize="@dimen/sw_4sp" android:textSize="@dimen/sw_14sp"
android:layout_width="@dimen/sw_175dp" android:layout_width="@dimen/sw_175dp"
android:layout_marginTop="@dimen/sw_18dp" android:layout_marginTop="@dimen/sw_18dp"
android:textColor="#A1A1A1" android:textColor="#A1A1A1"

View File

@@ -23,6 +23,16 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/key_airole"
android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_34dp"
android:layout_marginStart="@dimen/sw_10dp"
android:clickable="true"
app:layout_constraintStart_toEndOf="@id/key_abc"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageView <ImageView
android:id="@+id/key_vip" android:id="@+id/key_vip"
android:layout_width="@dimen/sw_115dp" android:layout_width="@dimen/sw_115dp"

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.keyboard.FixedHeightFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/airole_panel_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true">
<!-- 消息列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/airole_panel_messages"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="@dimen/sw_8dp"
android:paddingBottom="@dimen/sw_8dp"
android:overScrollMode="always" />
<!-- 未登录提示 -->
<TextView
android:id="@+id/airole_panel_login_hint"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_200dp"
android:gravity="center"
android:text="@string/Pop_up_window_ai_1"
android:textColor="#999999"
android:textSize="@dimen/sw_14sp"
android:visibility="gone" />
<!-- 关闭按钮(右上角,叠在消息列表上方) -->
<ImageView
android:id="@+id/airole_panel_close"
android:layout_width="@dimen/sw_30dp"
android:layout_height="@dimen/sw_30dp"
android:layout_gravity="top|end"
android:layout_marginTop="@dimen/sw_8dp"
android:layout_marginEnd="@dimen/sw_8dp"
android:padding="@dimen/sw_4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="close"
android:clickable="true"
android:focusable="true" />
</com.example.myapplication.keyboard.FixedHeightFrameLayout>

View File

@@ -124,7 +124,7 @@
android:gravity="center" android:gravity="center"
android:layout_width="@dimen/sw_60dp" android:layout_width="@dimen/sw_60dp"
android:layout_marginTop="@dimen/sw_50dp" android:layout_marginTop="@dimen/sw_50dp"
android:layout_height="@dimen/sw_8dp" android:layout_height="@dimen/sw_28dp"
android:background="@drawable/round_bg_one"> android:background="@drawable/round_bg_one">
<ImageView <ImageView
android:id="@+id/add_first_icon" android:id="@+id/add_first_icon"

View File

@@ -61,6 +61,18 @@
android:focusable="false" /> android:focusable="false" />
</com.example.myapplication.ui.circle.GradientMaskLayout> </com.example.myapplication.ui.circle.GradientMaskLayout>
<View
android:id="@+id/imeDismissOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="fill"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="true"
android:elevation="@dimen/sw_2dp"
android:visibility="gone"
android:importantForAccessibility="no" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/inputOverlay" android:id="@+id/inputOverlay"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -220,6 +232,46 @@
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no"
android:elevation="@dimen/sw_10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/noResultImage"
android:layout_width="@dimen/sw_175dp"
android:layout_height="@dimen/sw_177dp"
android:src="@drawable/no_search_result"
android:scaleType="fitCenter"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 抽屉 --> <!-- 抽屉 -->
<FrameLayout <FrameLayout

View File

@@ -16,13 +16,65 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/login_bg"> android:background="@drawable/login_bg">
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/rvTransactions"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:overScrollMode="never" android:orientation="vertical">
android:clipToPadding="false"
android:paddingBottom="@dimen/sw_16dp" /> <include layout="@layout/layout_consumption_record_header" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTransactions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:clipToPadding="false"
android:paddingBottom="@dimen/sw_16dp" />
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/noResultImage"
android:layout_width="@dimen/sw_175dp"
android:layout_height="@dimen/sw_177dp"
android:src="@drawable/no_search_result"
android:scaleType="fitCenter"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -305,6 +305,62 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:orientation="vertical"
android:gravity="center_horizontal">
<!-- 顶部“可点击的头部区域”,点击可展开/收起 -->
<LinearLayout
android:id="@+id/bottomSheetHeader"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_48dp"
android:gravity="center"
android:paddingTop="@dimen/sw_8dp"
android:paddingBottom="@dimen/sw_8dp"
android:orientation="vertical">
<!-- 小横条指示器 -->
<View
android:layout_width="@dimen/sw_40dp"
android:layout_height="@dimen/sw_4dp"
android:background="@drawable/bs_handle_bg" />
</LinearLayout>
<ImageView
android:id="@+id/noResultImage"
android:layout_width="@dimen/sw_175dp"
android:layout_height="@dimen/sw_177dp"
android:src="@drawable/no_search_result"
android:scaleType="fitCenter"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- 浮动按钮 --> <!-- 浮动按钮 -->

View File

@@ -99,18 +99,32 @@
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="@dimen/sw_10dp" android:layout_marginStart="@dimen/sw_10dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:layout_width="match_parent" <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/nickname" android:gravity="center_vertical"
android:text="@string/mine_username" orientation="horizontal">
android:textColor="#1B1F1A" <TextView
android:textStyle="bold" android:layout_width="match_parent"
android:ellipsize="end" android:layout_height="wrap_content"
android:singleLine="true" android:id="@+id/nickname"
android:maxLines="1" android:text="@string/mine_username"
android:layout_weight="1" android:textColor="#1B1F1A"
android:textSize="@dimen/sw_20sp" /> android:textStyle="bold"
android:ellipsize="end"
android:singleLine="true"
android:maxLines="1"
android:layout_weight="1"
android:textSize="@dimen/sw_20sp" />
<ImageView
android:id="@+id/vip_icon"
android:layout_width="@dimen/sw_55dp"
android:layout_height="@dimen/sw_19dp"
android:layout_marginStart="@dimen/sw_5dp"
android:layout_marginEnd="@dimen/sw_5dp"/>
</LinearLayout>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -253,8 +267,8 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:layout_width="@dimen/sw_20dp" android:layout_width="@dimen/sw_25dp"
android:layout_height="@dimen/sw_24dp" android:layout_height="@dimen/sw_29dp"
android:layout_marginStart="@dimen/sw_10dp" android:layout_marginStart="@dimen/sw_10dp"
android:layout_marginEnd="@dimen/sw_10dp" android:layout_marginEnd="@dimen/sw_10dp"
android:src="@drawable/ic_language" /> android:src="@drawable/ic_language" />

View File

@@ -264,5 +264,42 @@
android:layout_marginBottom="@dimen/sw_30dp" android:layout_marginBottom="@dimen/sw_30dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:importantForAccessibility="no"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/noResultImage"
android:layout_width="@dimen/sw_175dp"
android:layout_height="@dimen/sw_177dp"
android:src="@drawable/no_search_result"
android:scaleType="fitCenter"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.example.myapplication.widget.NoHorizontalInterceptSwipeRefreshLayout> </com.example.myapplication.widget.NoHorizontalInterceptSwipeRefreshLayout>

View File

@@ -20,14 +20,22 @@ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/key_ai" android:id="@+id/key_ai"
android:layout_width="@dimen/sw_34dp" android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_34dp" android:layout_height="@dimen/sw_34dp"
android:textSize="12sp" android:textSize="@dimen/sw_12sp"
android:textColor="#A9A9A9" android:textColor="#A9A9A9"
android:clickable="true" android:clickable="true"
android:gravity="center"/> android:gravity="center"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/key_airole"
android:layout_marginStart="@dimen/sw_10dp"
android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_34dp"
android:clickable="true" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@@ -69,12 +69,57 @@
android:background="@drawable/mykeyboard_bg" android:background="@drawable/mykeyboard_bg"
android:orientation="vertical"> android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <FrameLayout
android:id="@+id/rv_keyboard" android:id="@+id/keyboardListContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:overScrollMode="never" android:minHeight="@dimen/sw_200dp">
tools:listitem="@layout/item_keyboard_character" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
tools:listitem="@layout/item_keyboard_character" />
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/noResultImage"
android:layout_width="@dimen/sw_175dp"
android:layout_height="@dimen/sw_177dp"
android:src="@drawable/no_search_result"
android:scaleType="fitCenter"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -13,70 +13,130 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F6F7FB" android:background="#F6F7FB"
tools:context=".ui.shop.myskin.MySkin"> tools:context=".ui.shop.myskin.MySkin">
<androidx.core.widget.NestedScrollView
<LinearLayout
android:id="@+id/contentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fillViewport="true" android:orientation="vertical">
android:overScrollMode="never"> <!-- 标题和返-->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:orientation="horizontal"
android:orientation="vertical"> android:padding="@dimen/sw_16dp"
<!-- 标题和返回 --> android:gravity="center_vertical">
<LinearLayout <!-- 返回按钮 -->
android:layout_width="match_parent" <FrameLayout
android:layout_height="wrap_content" android:id="@+id/iv_close"
android:orientation="horizontal" android:layout_width="@dimen/sw_46dp"
android:padding="@dimen/sw_16dp" android:layout_height="@dimen/sw_46dp">
android:gravity="center_vertical"> <ImageView
<!-- 返回按钮 --> android:layout_width="@dimen/sw_13dp"
<FrameLayout android:layout_height="@dimen/sw_13dp"
android:id="@+id/iv_close" android:layout_gravity="center"
android:layout_width="@dimen/sw_46dp" android:src="@drawable/more_icons"
android:layout_height="@dimen/sw_46dp"> android:rotation="180"
<ImageView android:scaleType="fitCenter" />
android:layout_width="@dimen/sw_13dp" </FrameLayout>
android:layout_height="@dimen/sw_13dp"
android:layout_gravity="center"
android:src="@drawable/more_icons"
android:rotation="180"
android:scaleType="fitCenter" />
</FrameLayout>
<TextView <TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:textStyle="bold"
android:text="@string/skin_title"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_16sp" />
<TextView
android:id="@+id/tvEditor"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/sw_4dp"
android:gravity="center"
android:textStyle="bold"
android:text="@string/skin_editor"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_13sp" />
</LinearLayout>
<!-- 内容 -->
<FrameLayout
android:id="@+id/contentFrame"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/sw_200dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvThemes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
tools:listitem="@layout/item_myskin_theme"/>
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no">
<LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_gravity="center"
android:gravity="center" android:orientation="vertical"
android:textStyle="bold" android:gravity="center_horizontal">
android:text="@string/skin_title"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_16sp" />
<TextView <ImageView
android:id="@+id/tvEditor" android:id="@+id/noResultImage"
android:layout_width="wrap_content" android:layout_width="@dimen/sw_175dp"
android:layout_height="match_parent" android:layout_height="@dimen/sw_177dp"
android:layout_marginEnd="@dimen/sw_4dp" android:src="@drawable/no_search_result"
android:gravity="center" android:scaleType="fitCenter"
android:textStyle="bold" android:contentDescription="@null"
android:text="@string/skin_editor" android:importantForAccessibility="no" />
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_13sp" />
</LinearLayout>
<!-- 内容 --> <TextView
<androidx.recyclerview.widget.RecyclerView android:id="@+id/noResultText"
android:id="@+id/rvThemes" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:gravity="center"
android:nestedScrollingEnabled="false" android:textAlignment="center"
tools:listitem="@layout/item_myskin_theme"/> android:text="@string/search_not_data"
</LinearLayout> android:textSize="@dimen/sw_13sp"
</androidx.core.widget.NestedScrollView> android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
</LinearLayout>
<!-- 底部编辑--> <!-- 底部编辑-->
<LinearLayout <LinearLayout
android:id="@+id/bottomEditBar" android:id="@+id/bottomEditBar"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -28,6 +28,13 @@
android:textColor="#A9A9A9" android:textColor="#A9A9A9"
android:gravity="center" android:gravity="center"
android:clickable="true"/> android:clickable="true"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/key_airole"
android:layout_marginStart="@dimen/sw_10dp"
android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_34dp"
android:clickable="true" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@@ -7,243 +7,288 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F6F7FB" android:background="#F6F7FB"
tools:context=".ui.home.myotherpages.PersonalSettings"> tools:context=".ui.home.myotherpages.PersonalSettings">
<!-- 内容 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <LinearLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
<androidx.core.widget.NestedScrollView android:orientation="vertical">
<!-- -->
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:fillViewport="true" android:padding="@dimen/sw_16dp"
android:overScrollMode="never"> android:background="#F8F8F8"
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent" android:gravity="center_vertical"
android:layout_height="wrap_content" android:clickable="true"
android:padding="@dimen/sw_16dp" android:focusable="true">
android:background="#F8F8F8"
android:orientation="vertical">
<!-- 标题和返回 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 返回按钮 -->
<FrameLayout
android:id="@+id/iv_close"
android:layout_width="@dimen/sw_46dp"
android:layout_height="@dimen/sw_46dp">
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_13dp"
android:layout_gravity="center"
android:src="@drawable/more_icons"
android:rotation="180"
android:scaleType="fitCenter" />
</FrameLayout>
<TextView <FrameLayout
android:layout_width="wrap_content" android:id="@+id/iv_close"
android:layout_height="wrap_content" android:layout_width="@dimen/sw_46dp"
android:layout_weight="1" android:layout_height="@dimen/sw_46dp">
android:layout_marginEnd="@dimen/sw_49dp"
android:gravity="center"
android:textStyle="bold"
android:text="@string/personal_settings_title"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_16sp" />
</LinearLayout>
<!-- 头像 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_130dp"
android:padding="@dimen/sw_16dp"
android:gravity="center"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="@dimen/sw_88dp"
android:layout_height="@dimen/sw_88dp"
android:src="@drawable/default_avatar"
android:elevation="@dimen/sw_1dp"
android:clickable="true"
android:focusable="true"/>
<ImageView <ImageView
android:layout_width="@dimen/sw_18dp" android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_18dp" android:layout_height="@dimen/sw_13dp"
android:layout_marginStart="@dimen/_sw_22dp" android:layout_gravity="center"
android:layout_marginTop="@dimen/sw_34dp" android:src="@drawable/more_icons"
android:elevation="@dimen/sw_2dp" android:rotation="180"
android:src="@drawable/avatar_modification" android:scaleType="fitCenter" />
android:scaleType="centerCrop"/> </FrameLayout>
</LinearLayout>
<!-- 修改 -->
<TextView <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="@dimen/sw_49dp"
android:gravity="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:text="Modify" android:text="@string/personal_settings_title"
android:textColor="#1B1F1A" android:textColor="#1B1F1A"
android:textSize="@dimen/sw_18sp" /> android:textSize="@dimen/sw_16sp" />
</LinearLayout>
<!-- 其他设置 --> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
<LinearLayout android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:layout_marginTop="@dimen/sw_24dp"
android:background="@drawable/settings" <androidx.core.widget.NestedScrollView
android:orientation="vertical">
<!-- Nickname -->
<LinearLayout
android:id="@+id/row_nickname"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp" android:layout_height="match_parent"
android:gravity="center_vertical" android:fillViewport="true"
android:orientation="horizontal"> android:overScrollMode="never">
<TextView
android:layout_width="wrap_content" <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp" android:padding="@dimen/sw_16dp"
android:text="@string/personal_settings_nickname" android:background="#F8F8F8"
android:textColor="#1B1F1A" android:orientation="vertical">
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" /> <!-- ?? -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_130dp"
android:padding="@dimen/sw_16dp"
android:gravity="center"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="@dimen/sw_88dp"
android:layout_height="@dimen/sw_88dp"
android:src="@drawable/default_avatar"
android:elevation="@dimen/sw_1dp"
android:clickable="true"
android:focusable="true"/>
<ImageView
android:layout_width="@dimen/sw_18dp"
android:layout_height="@dimen/sw_18dp"
android:layout_marginStart="@dimen/_sw_22dp"
android:layout_marginTop="@dimen/sw_34dp"
android:elevation="@dimen/sw_2dp"
android:src="@drawable/avatar_modification"
android:scaleType="centerCrop"/>
</LinearLayout>
<!-- ?? -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textStyle="bold"
android:text="Modify"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_18sp" />
<!-- ???? -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_24dp"
android:background="@drawable/settings"
android:orientation="vertical">
<!-- Nickname -->
<LinearLayout
android:id="@+id/row_nickname"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp"
android:text="@string/personal_settings_nickname"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_nickname_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Nickname"
android:gravity="end"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView
android:layout_width="@dimen/sw_9dp"
android:layout_height="@dimen/sw_13dp"
android:tint="#AFAFAF"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/more_icons" />
</LinearLayout>
</LinearLayout>
<!-- Gender -->
<LinearLayout
android:id="@+id/row_gender"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp"
android:text="@string/personal_settings_gender"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_gender_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Gender"
android:gravity="end"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView
android:layout_width="@dimen/sw_9dp"
android:layout_height="@dimen/sw_13dp"
android:tint="#AFAFAF"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/more_icons" />
</LinearLayout>
</LinearLayout>
<!-- User ID -->
<LinearLayout
android:id="@+id/row_userid"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp"
android:text="UID"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_userid_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="88888888"
android:gravity="end"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView
android:layout_width="@dimen/sw_14dp"
android:layout_height="@dimen/sw_14dp"
android:tint="#AFAFAF"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/copy" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:id="@+id/noResultOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp" android:layout_gravity="center"
android:gravity="center_vertical" android:orientation="vertical"
android:orientation="horizontal"> android:gravity="center_horizontal">
<TextView
android:id="@+id/tv_nickname_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Nickname"
android:gravity="end"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView <ImageView
android:layout_width="@dimen/sw_9dp" android:id="@+id/noResultImage"
android:layout_height="@dimen/sw_13dp" android:layout_width="@dimen/sw_175dp"
android:tint="#AFAFAF" android:layout_height="@dimen/sw_177dp"
android:layout_marginStart="@dimen/sw_12dp" android:src="@drawable/no_search_result"
android:layout_marginEnd="@dimen/sw_16dp" android:scaleType="fitCenter"
android:src="@drawable/more_icons" /> android:contentDescription="@null"
</LinearLayout> android:importantForAccessibility="no" />
</LinearLayout>
<!-- Gender -->
<LinearLayout
android:id="@+id/row_gender"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp"
android:text="@string/personal_settings_gender"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/tv_gender_value" android:id="@+id/noResultText"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Gender" android:gravity="center"
android:gravity="end" android:textAlignment="center"
android:text="@string/search_not_data"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A" android:textColor="#1B1F1A"
android:textStyle="bold" android:includeFontPadding="false" />
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView
android:layout_width="@dimen/sw_9dp"
android:layout_height="@dimen/sw_13dp"
android:tint="#AFAFAF"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/more_icons" />
</LinearLayout> </LinearLayout>
</LinearLayout> </FrameLayout>
</FrameLayout>
<!-- User ID --> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout </LinearLayout>
android:id="@+id/row_userid"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_16dp"
android:text="UID"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:textSize="@dimen/sw_16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_userid_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="88888888"
android:gravity="end"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="@dimen/sw_16sp" />
<ImageView
android:layout_width="@dimen/sw_14dp"
android:layout_height="@dimen/sw_14dp"
android:tint="#AFAFAF"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/copy" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/sw_63dp"
android:layout_marginTop="@dimen/sw_273dp"
android:layout_marginBottom="@dimen/sw_20dp"
android:gravity="center"
android:text="Log Out"
android:textColor="#FF0000"
android:textSize="@dimen/sw_16sp"
android:textStyle="bold"
android:background="@drawable/settings"/>
<!-- ````````` -->
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -23,6 +23,12 @@
<!-- ai --> <!-- ai -->
<TextView android:id="@+id/key_ai" android:layout_width="@dimen/sw_34dp" android:layout_height="@dimen/sw_34dp" android:textSize="@dimen/sw_12sp" android:textColor="#A9A9A9" android:gravity="center" android:clickable="true"/> <TextView android:id="@+id/key_ai" android:layout_width="@dimen/sw_34dp" android:layout_height="@dimen/sw_34dp" android:textSize="@dimen/sw_12sp" android:textColor="#A9A9A9" android:gravity="center" android:clickable="true"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/key_airole"
android:layout_marginStart="@dimen/sw_10dp"
android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_34dp"
android:clickable="true" />
</LinearLayout> </LinearLayout>

View File

@@ -113,7 +113,7 @@
<!-- 搜索 --> <!-- 搜索 -->
<string name="search_hint">请输入你要搜索的内容</string> <string name="search_hint">请输入你要搜索的内容</string>
<string name="search_search">搜索</string> <string name="search_search">搜索</string>
<string name="search_not_data">目前暂无相关数据</string> <string name="search_not_data">目前暂无相关数据</string>
<string name="search_historical">历史搜索</string> <string name="search_historical">历史搜索</string>
<!-- 详情 --> <!-- 详情 -->