This commit is contained in:
pengxiaolong
2026-02-27 20:50:08 +08:00
parent acf4d39892
commit c018243116
85 changed files with 2031 additions and 485 deletions

View File

@@ -4,7 +4,9 @@
"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" "WebSearch",
"Bash(grep:*)",
"Bash(find:*)"
] ]
} }
} }

0
_nul Normal file
View File

View File

@@ -21,9 +21,13 @@ import android.graphics.Rect
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsAnimationCompat
import android.widget.ImageView import android.widget.ImageView
import android.text.TextWatcher import android.text.TextWatcher
import android.text.Editable import android.text.Editable
import kotlin.math.max
import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.network.BehaviorReporter
class GuideActivity : AppCompatActivity() { class GuideActivity : AppCompatActivity() {
@@ -36,6 +40,8 @@ class GuideActivity : AppCompatActivity() {
private lateinit var bottomPanel: LinearLayout private lateinit var bottomPanel: LinearLayout
private lateinit var hintLayout: LinearLayout private lateinit var hintLayout: LinearLayout
private lateinit var titleTextView: TextView private lateinit var titleTextView: TextView
private var lastImeBottom = 0
private var lastSystemBottom = 0
// 我方的预设回复 // 我方的预设回复
@@ -117,45 +123,46 @@ class GuideActivity : AppCompatActivity() {
findViewById<FrameLayout>(R.id.iv_close).setOnClickListener { findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
finish() finish()
} }
//输入框上移 //输入框上移(参考 CircleFragment使用 WindowInsetsCompat 精确区分 IME 和导航栏高度)
// 1. WindowInsetsCompat 监听:处理初始状态和配置变化
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val systemBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
applyImeInsets(imeBottom, systemBottom)
insets
}
// 2. WindowInsetsAnimationCompat跟随键盘动画平滑移动
ViewCompat.setWindowInsetsAnimationCallback(
rootView,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val systemBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
applyImeInsets(imeBottom, systemBottom)
return insets
}
}
)
// 3. ViewTreeObserver 兼容方案:部分旧机型 WindowInsets 不触发,通过布局变化兜底
rootView.viewTreeObserver.addOnGlobalLayoutListener { rootView.viewTreeObserver.addOnGlobalLayoutListener {
val r = Rect() val r = Rect()
// 获取窗口可见区域
rootView.getWindowVisibleDisplayFrame(r) rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = rootView.rootView.height val screenHeight = rootView.rootView.height
val visibleBottom = r.bottom val heightDiff = (screenHeight - r.bottom).coerceAtLeast(0)
val keyboardHeight = screenHeight - visibleBottom val threshold = (screenHeight * 0.15f).toInt()
val heightIme = if (heightDiff > threshold) heightDiff else 0
// 这个阈值防止“状态栏/导航栏变化”被误认为键盘 val rootInsets = ViewCompat.getRootWindowInsets(rootView)
val isKeyboardVisible = keyboardHeight > screenHeight * 0.15 val insetIme = rootInsets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
val imeBottom = max(heightIme, insetIme)
if (isKeyboardVisible) { val systemBottom = rootInsets
// 键盘高度为正,把 bottomPanel 抬上去,但不要抬太高 ?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom
// 只上移键盘高度减去底部面板高度,让输入框刚好在键盘上方 ?: lastSystemBottom
val adjustedTranslation = -(keyboardHeight - bottomPanel.height) applyImeInsets(imeBottom, systemBottom)
bottomPanel.translationY = adjustedTranslation.toFloat()
// 为了让最后一条消息不被挡住,可以给 scrollView 加个 paddingBottom
scrollView.setPadding(
scrollView.paddingLeft,
scrollView.paddingTop,
scrollView.paddingRight,
keyboardHeight + bottomPanel.height
)
// 再滚到底,保证能看到最新消息
scrollToBottom()
} else {
// 键盘收起,复位
bottomPanel.translationY = 0f
scrollView.setPadding(
scrollView.paddingLeft,
scrollView.paddingTop,
scrollView.paddingRight,
bottomPanel.height // 保持底部有一点空隙也可以按你需求调
)
}
} }
// 键盘发送 // 键盘发送
@@ -293,4 +300,33 @@ class GuideActivity : AppCompatActivity() {
scrollView.fullScroll(View.FOCUS_DOWN) scrollView.fullScroll(View.FOCUS_DOWN)
} }
} }
// ======== 键盘弹起/收起时调整 bottom_panel 位置 ========
private fun applyImeInsets(imeBottom: Int, systemBottom: Int) {
if (lastImeBottom == imeBottom && lastSystemBottom == systemBottom) return
lastImeBottom = imeBottom
lastSystemBottom = systemBottom
if (imeBottom > 0) {
// 键盘弹起:上移偏移量 = IME高度 - 导航栏高度(避免重复计算导航栏区域)
val offset = (imeBottom - systemBottom).coerceAtLeast(0)
bottomPanel.translationY = -offset.toFloat()
scrollView.setPadding(
scrollView.paddingLeft,
scrollView.paddingTop,
scrollView.paddingRight,
offset + bottomPanel.height
)
scrollToBottom()
} else {
// 键盘收起:复位
bottomPanel.translationY = 0f
scrollView.setPadding(
scrollView.paddingLeft,
scrollView.paddingTop,
scrollView.paddingRight,
bottomPanel.height
)
}
}
} }

View File

@@ -2,6 +2,7 @@
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
@@ -10,8 +11,10 @@ import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -261,6 +264,14 @@ class MainActivity : AppCompatActivity() {
is AuthEvent.CharacterAdded -> { is AuthEvent.CharacterAdded -> {
// 不需要处理由HomeFragment处理 // 不需要处理由HomeFragment处理
} }
is AuthEvent.ShowChatErrorRecharge -> {
showChatErrorRechargeDialog(event.errorMessage)
}
is AuthEvent.KeyboardChatUpdated -> {
// 由 CircleFragment 自行处理
}
} }
} }
} }
@@ -287,10 +298,25 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// 处理来自键盘的跳转请求
handleRechargeIntent(intent)
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener也能恢复底栏 // ✅ 最终兜底:从后台回来 / 某些场景没触发 listener也能恢复底栏
bottomNav.post { updateBottomNavVisibility() } bottomNav.post { updateBottomNavVisibility() }
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
handleRechargeIntent(intent)
}
private fun handleRechargeIntent(intent: Intent?) {
if (intent?.action == "ACTION_OPEN_RECHARGE") {
intent.action = null
openGlobal(R.id.rechargeFragment)
}
}
override fun onDestroy() { override fun onDestroy() {
// ✅ 防泄漏移除路由监听Activity 销毁时) // ✅ 防泄漏移除路由监听Activity 销毁时)
runCatching { runCatching {
@@ -584,6 +610,29 @@ class MainActivity : AppCompatActivity() {
return globalNavController.currentDestination?.id != R.id.globalEmptyFragment return globalNavController.currentDestination?.id != R.id.globalEmptyFragment
} }
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
val dialogView = LayoutInflater.from(this)
.inflate(R.layout.dialog_chat_error_recharge, null)
val dialog = AlertDialog.Builder(this)
.setView(dialogView)
.setCancelable(true)
.create()
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
dialog.window?.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
dialog.window?.setDimAmount(0.5f)
if (!errorMessage.isNullOrEmpty()) {
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
}
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
dialog.dismiss()
}
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
dialog.dismiss()
openGlobal(R.id.rechargeFragment)
}
dialog.show()
}
private fun setupBackPress() { private fun setupBackPress() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {

View File

@@ -674,7 +674,8 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
Glide.with(this) Glide.with(this)
.load(info.avatarUrl) .load(info.avatarUrl)
.circleCrop() .circleCrop()
.placeholder(android.R.drawable.ic_menu_myplaces) .placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatarView) .into(avatarView)
} else { } else {
avatarView.setImageResource(android.R.drawable.ic_menu_myplaces) avatarView.setImageResource(android.R.drawable.ic_menu_myplaces)

View File

@@ -1,12 +1,16 @@
package com.example.myapplication.keyboard package com.example.myapplication.keyboard
import android.content.Context import android.content.Context
import android.content.Intent
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
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.AuthEventBus
import com.example.myapplication.network.RetrofitClient import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.chatMessageRequest import com.example.myapplication.network.chatMessageRequest
import com.example.myapplication.ui.circle.ChatMessage import com.example.myapplication.ui.circle.ChatMessage
@@ -114,20 +118,15 @@ class AiRolePanelController(private val context: Context) {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "chatMessage failed: ${e.message}", e) Log.e(TAG, "chatMessage failed: ${e.message}", e)
placeholder.text = context.getString(R.string.refresh_failed) removePlaceholder(placeholder)
placeholder.isLoading = false
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
return@launch return@launch
} }
val data = response.data val data = response.data
if (data == null) { if (data == null) {
Log.e(TAG, "chatMessage no data, code=${response.code}") Log.e(TAG, "chatMessage no data, code=${response.code}")
placeholder.text = context.getString(R.string.refresh_failed) removePlaceholder(placeholder)
placeholder.isLoading = false showChatErrorRechargeDialog(response.message)
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
return@launch return@launch
} }
@@ -138,6 +137,11 @@ class AiRolePanelController(private val context: Context) {
adapter?.notifyMessageUpdated(placeholder.id) adapter?.notifyMessageUpdated(placeholder.id)
scrollToBottom() scrollToBottom()
// 通知 CircleFragment 刷新对应角色的聊天记录
AuthEventBus.emit(AuthEvent.KeyboardChatUpdated(currentCompanionId))
// 持久化脏标记,确保应用从后台恢复时也能刷新
AiRolePreferences.markCompanionDirty(context, currentCompanionId)
// 轮询音频 URL // 轮询音频 URL
val audioUrl = fetchAudioUrl(data.audioId) val audioUrl = fetchAudioUrl(data.audioId)
if (!audioUrl.isNullOrBlank()) { if (!audioUrl.isNullOrBlank()) {
@@ -169,6 +173,46 @@ class AiRolePanelController(private val context: Context) {
} }
} }
private fun removePlaceholder(placeholder: ChatMessage) {
val index = messages.indexOfFirst { it.id == placeholder.id }
if (index >= 0) {
messages.removeAt(index)
adapter?.notifyMessageRemoved(index)
}
}
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
val windowToken = recyclerView?.windowToken ?: return
val dialogView = LayoutInflater.from(context)
.inflate(R.layout.dialog_chat_error_recharge, null)
val dialog = android.app.AlertDialog.Builder(context)
.setView(dialogView)
.setCancelable(true)
.create()
dialog.window?.let { window ->
window.setBackgroundDrawableResource(android.R.color.transparent)
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window.setDimAmount(0.5f)
window.setType(android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG)
window.attributes = window.attributes.also { it.token = windowToken }
}
if (!errorMessage.isNullOrEmpty()) {
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
}
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
dialog.dismiss()
}
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
dialog.dismiss()
val intent = Intent(context, com.example.myapplication.MainActivity::class.java).apply {
action = "ACTION_OPEN_RECHARGE"
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
context.startActivity(intent)
}
dialog.show()
}
private fun nextId(): Long = messageIdCounter++ private fun nextId(): Long = messageIdCounter++
fun clearMessages() { fun clearMessages() {

View File

@@ -11,6 +11,7 @@ object AiRolePreferences {
private const val KEY_COMPANION_ID = "companion_id" private const val KEY_COMPANION_ID = "companion_id"
private const val KEY_PERSONA_NAME = "persona_name" private const val KEY_PERSONA_NAME = "persona_name"
private const val KEY_AVATAR_URL = "avatar_url" private const val KEY_AVATAR_URL = "avatar_url"
private const val KEY_DIRTY_COMPANIONS = "dirty_companion_ids"
data class CompanionInfo( data class CompanionInfo(
val companionId: Int, val companionId: Int,
@@ -49,4 +50,26 @@ object AiRolePreferences {
.clear() .clear()
.apply() .apply()
} }
/**
* 标记某个 companionId 的聊天记录需要刷新(键盘聊天成功后调用)
*/
fun markCompanionDirty(context: Context, companionId: Int) {
val sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
val existing = sp.getStringSet(KEY_DIRTY_COMPANIONS, emptySet()) ?: emptySet()
sp.edit()
.putStringSet(KEY_DIRTY_COMPANIONS, existing + companionId.toString())
.apply()
}
/**
* 消费所有待刷新的 companionId返回后自动清除标记
*/
fun consumeDirtyCompanions(context: Context): Set<Int> {
val sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
val raw = sp.getStringSet(KEY_DIRTY_COMPANIONS, null)
if (raw.isNullOrEmpty()) return emptySet()
sp.edit().remove(KEY_DIRTY_COMPANIONS).apply()
return raw.mapNotNull { it.toIntOrNull() }.toSet()
}
} }

View File

@@ -102,6 +102,11 @@ interface ApiService {
@GET("user/inviteCode") @GET("user/inviteCode")
suspend fun inviteCode( suspend fun inviteCode(
): ApiResponse<ShareResponse> ): ApiResponse<ShareResponse>
// 获取客服邮箱
@GET("user/customerMail")
suspend fun delUserCharacter(
): ApiResponse<String>
//===========================================首页================================= //===========================================首页=================================
// 标签列表 // 标签列表
@GET("tag/list") @GET("tag/list")
@@ -197,7 +202,7 @@ interface ApiService {
//恢复已删除的主题 //恢复已删除的主题
@POST("themes/restore") @POST("themes/restore")
suspend fun restoreTheme( suspend fun restoreTheme(
@Query("themeId") themeId: Int @Body body: restoreThemeRequest
): ApiResponse<Unit> ): ApiResponse<Unit>
// =========================================圈子ai陪聊============================================ // =========================================圈子ai陪聊============================================
// 分页查询AI陪聊角色 // 分页查询AI陪聊角色
@@ -285,6 +290,12 @@ interface ApiService {
@Body body: chatSessionResetRequest @Body body: chatSessionResetRequest
): ApiResponse<chatSessionResetResponse> ): ApiResponse<chatSessionResetResponse>
//删除聊天记录
@POST("chat/history/delete")
suspend fun chatDelete(
@Body body: chatDeleteRequest
): ApiResponse<Boolean>

View File

@@ -36,4 +36,6 @@ sealed class AuthEvent {
) : AuthEvent() ) : AuthEvent()
object UserUpdated : AuthEvent() object UserUpdated : AuthEvent()
data class CharacterDeleted(val characterId: Int) : AuthEvent() data class CharacterDeleted(val characterId: Int) : AuthEvent()
data class ShowChatErrorRecharge(val errorMessage: String? = null) : AuthEvent()
data class KeyboardChatUpdated(val companionId: Int) : AuthEvent()
} }

View File

@@ -11,7 +11,7 @@ object BehaviorHttpClient {
private const val TAG = "BehaviorHttp" private const val TAG = "BehaviorHttp"
// TODO改成你的行为服务 baseUrl必须以 / 结尾) // TODO改成你的行为服务 baseUrl必须以 / 结尾)(上报接口)
private const val BASE_URL = "http://192.168.2.22:35310/api/" private const val BASE_URL = "http://192.168.2.22:35310/api/"
/** /**

View File

@@ -258,14 +258,14 @@ private fun bodyToString(body: okhttp3.RequestBody): String {
} }
/** /**
* JSON 扁平化规则: * JSON 扁平化规则(与 iOS KBSignUtils 对齐)
* object: a.b.c * 仅展开顶层 object 的 key-value
* array : items[0].id * 数组和嵌套对象直接转为 JSON 字符串作为 value不递归展开
*/ */
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) { private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
when { when {
elem.isJsonNull -> { elem.isJsonNull -> {
// null 不参与签名(服务端也要一致) // null 不参与签名
} }
elem.isJsonPrimitive -> { elem.isJsonPrimitive -> {
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"') if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
@@ -274,14 +274,20 @@ private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<Strin
val obj = elem.asJsonObject val obj = elem.asJsonObject
for ((k, v) in obj.entrySet()) { for ((k, v) in obj.entrySet()) {
val newKey = if (prefix.isBlank()) k else "$prefix.$k" val newKey = if (prefix.isBlank()) k else "$prefix.$k"
if (prefix.isBlank() && (v.isJsonArray || v.isJsonObject)) {
// 顶层的数组/对象:直接转 JSON 字符串,与 iOS 一致
val jsonStr = Gson().toJson(v)
if (jsonStr.isNotBlank()) out[newKey] = jsonStr
} else {
flattenJson(v, newKey, out) flattenJson(v, newKey, out)
} }
} }
}
elem.isJsonArray -> { elem.isJsonArray -> {
val arr = elem.asJsonArray // 顶层数组场景(极少见),直接转 JSON 字符串
for (i in 0 until arr.size()) { if (prefix.isNotBlank()) {
val newKey = "$prefix[$i]" val jsonStr = Gson().toJson(elem)
flattenJson(arr[i], newKey, out) if (jsonStr.isNotBlank()) out[prefix] = jsonStr
} }
} }
} }

View File

@@ -252,6 +252,11 @@ data class purchaseThemeRequest(
val themeId: Int, val themeId: Int,
) )
//恢复主题
data class restoreThemeRequest(
val themeId: Int,
)
// =========================================圈子ai陪聊)============================================ // =========================================圈子ai陪聊)============================================
//分页查询AI陪聊角色 //分页查询AI陪聊角色
@@ -473,3 +478,7 @@ data class chatSessionResetResponse(
val resetVersion: Int, val resetVersion: Int,
val createdAt: String, val createdAt: String,
) )
data class chatDeleteRequest(
val id: Int,
)

View File

@@ -19,7 +19,8 @@ import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
object NetworkClient { object NetworkClient {
private const val BASE_URL = "http://192.168.2.22:7529/api" // private const val BASE_URL = "http://192.168.2.22:7529/api"
private const val BASE_URL = "https://devcallback.loveamorkey.com/api"
private const val TAG = "999-SSE_TALK" private const val TAG = "999-SSE_TALK"
// ====== 按你给的规则固定值 ====== // ====== 按你给的规则固定值 ======

View File

@@ -10,7 +10,8 @@ import com.example.myapplication.network.FileUploadService
object RetrofitClient { object RetrofitClient {
private const val BASE_URL = "http://192.168.2.22:7529/api/" // private const val BASE_URL = "http://192.168.2.22:7529/api/"
private const val BASE_URL = "https://devcallback.loveamorkey.com/api/"
// 保存 ApplicationContext // 保存 ApplicationContext
@Volatile @Volatile

View File

@@ -5,6 +5,7 @@ import android.media.MediaPlayer
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
@@ -13,9 +14,20 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R import com.example.myapplication.R
class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHolder>() { class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<ChatMessage> = mutableListOf() private var items: MutableList<ChatMessage> = mutableListOf()
var onMessageLongClick: ((message: ChatMessage, anchorView: View, rawX: Float, rawY: Float) -> Unit)? = null
var showLoading: Boolean = false
set(value) {
if (field == value) return
field = value
if (value) {
notifyItemInserted(0)
} else {
notifyItemRemoved(0)
}
}
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var playingMessageId: Long? = null private var playingMessageId: Long? = null
@@ -25,11 +37,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
setHasStableIds(true) setHasStableIds(true)
} }
private fun toDataIndex(adapterPosition: Int): Int =
if (showLoading) adapterPosition - 1 else adapterPosition
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return if (items[position].isMine) VIEW_TYPE_ME else VIEW_TYPE_BOT if (showLoading && position == 0) return VIEW_TYPE_LOADING
val dataIndex = toDataIndex(position)
return if (items[dataIndex].isMine) VIEW_TYPE_ME else VIEW_TYPE_BOT
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == VIEW_TYPE_LOADING) {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_chat_loading, parent, false)
return LoadingViewHolder(view)
}
val layout = if (viewType == VIEW_TYPE_ME) { val layout = if (viewType == VIEW_TYPE_ME) {
R.layout.item_chat_message_me R.layout.item_chat_message_me
} else { } else {
@@ -39,18 +61,31 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
return MessageViewHolder(view) return MessageViewHolder(view)
} }
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.bind(items[position]) if (holder is LoadingViewHolder) {
holder.bind()
return
}
(holder as MessageViewHolder).bind(items[toDataIndex(position)])
} }
override fun onViewRecycled(holder: MessageViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
holder.onRecycled() when (holder) {
is MessageViewHolder -> holder.onRecycled()
is LoadingViewHolder -> holder.onRecycled()
}
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size + if (showLoading) 1 else 0
override fun getItemId(position: Int): Long = items[position].id override fun getItemId(position: Int): Long {
if (showLoading && position == 0) return Long.MIN_VALUE
return items[toDataIndex(position)].id
}
private fun toAdapterIndex(dataIndex: Int): Int =
if (showLoading) dataIndex + 1 else dataIndex
fun bindMessages(messages: MutableList<ChatMessage>) { fun bindMessages(messages: MutableList<ChatMessage>) {
items = messages items = messages
@@ -60,14 +95,20 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
fun notifyLastInserted() { fun notifyLastInserted() {
val index = items.size - 1 val index = items.size - 1
if (index >= 0) { if (index >= 0) {
notifyItemInserted(index) notifyItemInserted(toAdapterIndex(index))
} }
} }
fun notifyMessageUpdated(messageId: Long) { fun notifyMessageUpdated(messageId: Long) {
val index = items.indexOfFirst { it.id == messageId } val index = items.indexOfFirst { it.id == messageId }
if (index >= 0) { if (index >= 0) {
notifyItemChanged(index) notifyItemChanged(toAdapterIndex(index))
}
}
fun notifyMessageRemoved(position: Int) {
if (position >= 0) {
notifyItemRemoved(toAdapterIndex(position))
} }
} }
@@ -186,6 +227,28 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading) AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading)
private val textLoadingAnimation = private val textLoadingAnimation =
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_text_loading) AnimationUtils.loadAnimation(itemView.context, R.anim.circle_text_loading)
private var lastTouchRawX = 0f
private var lastTouchRawY = 0f
init {
itemView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
lastTouchRawX = event.rawX
lastTouchRawY = event.rawY
}
false
}
itemView.setOnLongClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION && pos < items.size) {
val msg = items[pos]
if (!msg.isLoading) {
onMessageLongClick?.invoke(msg, itemView, lastTouchRawX, lastTouchRawY)
}
}
true
}
}
fun bind(message: ChatMessage) { fun bind(message: ChatMessage) {
if (boundMessageId != message.id) { if (boundMessageId != message.id) {
@@ -280,6 +343,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
private companion object { private companion object {
const val VIEW_TYPE_ME = 1 const val VIEW_TYPE_ME = 1
const val VIEW_TYPE_BOT = 2 const val VIEW_TYPE_BOT = 2
const val VIEW_TYPE_LOADING = 3
const val TYPE_DELAY_MS = 28L const val TYPE_DELAY_MS = 28L
} }
class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val loadingIcon: ImageView = itemView.findViewById(R.id.loadingIcon)
private val rotateAnim =
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading)
fun bind() {
loadingIcon.startAnimation(rotateAnim)
}
fun onRecycled() {
loadingIcon.clearAnimation()
}
}
} }

View File

@@ -98,9 +98,11 @@ class ChatPageViewHolder(
onLoadMore: ((position: Int, companionId: Int, onResult: (ChatHistoryLoadResult) -> Unit) -> Unit)?, onLoadMore: ((position: Int, companionId: Int, onResult: (ChatHistoryLoadResult) -> Unit) -> Unit)?,
onLikeClick: ((position: Int, companionId: Int) -> Unit)?, onLikeClick: ((position: Int, companionId: Int) -> Unit)?,
onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)?, onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)?,
onAvatarClick: ((companionId: Int) -> Unit)? onAvatarClick: ((companionId: Int) -> Unit)?,
onMessageLongClick: ((message: ChatMessage, anchorView: View, rawX: Float, rawY: Float) -> Unit)? = null
) { ) {
boundCompanionId = data.companionId boundCompanionId = data.companionId
messageAdapter.onMessageLongClick = onMessageLongClick
hasMoreHistory = historyState.hasMore hasMoreHistory = historyState.hasMore
isLoadingHistory = historyState.isLoading isLoadingHistory = historyState.isLoading
this.historyStateProvider = historyStateProvider this.historyStateProvider = historyStateProvider
@@ -114,10 +116,14 @@ class ChatPageViewHolder(
boundCommentCount = data.commentCount boundCommentCount = data.commentCount
Glide.with(backgroundView.context) Glide.with(backgroundView.context)
.load(data.backgroundColor) .load(data.backgroundColor)
.placeholder(R.drawable.component_loading)
.error(R.drawable.circle_not_data_bg)
.into(backgroundView) .into(backgroundView)
Glide.with(avatarView.context) Glide.with(avatarView.context)
.load(data.avatarUrl) .load(data.avatarUrl)
.placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar)
.into(avatarView) .into(avatarView)
likeView.setImageResource( likeView.setImageResource(
@@ -203,12 +209,15 @@ class ChatPageViewHolder(
val requestedCompanionId = boundCompanionId val requestedCompanionId = boundCompanionId
val requestedPageId = boundPageId val requestedPageId = boundPageId
isLoadingHistory = true isLoadingHistory = true
messageAdapter.showLoading = true
callback(position, requestedCompanionId) { result -> callback(position, requestedCompanionId) { result ->
if (requestedCompanionId != boundCompanionId || requestedPageId != boundPageId) { if (requestedCompanionId != boundCompanionId || requestedPageId != boundPageId) {
messageAdapter.showLoading = false
return@callback return@callback
} }
isLoadingHistory = false isLoadingHistory = false
hasMoreHistory = result.hasMore hasMoreHistory = result.hasMore
messageAdapter.showLoading = false
if (result.insertedCount > 0) { if (result.insertedCount > 0) {
notifyMessagesPrepended(result.insertedCount) notifyMessagesPrepended(result.insertedCount)
} }
@@ -238,7 +247,12 @@ class ChatPageViewHolder(
messageAdapter.notifyMessageUpdated(messageId) messageAdapter.notifyMessageUpdated(messageId)
} }
fun notifyMessageRemoved(position: Int) {
messageAdapter.notifyMessageRemoved(position)
}
override fun onRecycled() { override fun onRecycled() {
messageAdapter.showLoading = false
messageAdapter.release() messageAdapter.release()
chatRv.stopScroll() chatRv.stopScroll()
} }

View File

@@ -48,13 +48,17 @@ class CircleChatRepository(
private var knownTotalPages: Int? = null private var knownTotalPages: Int? = null
@Volatile @Volatile
private var availablePages: Int = totalPages private var availablePages: Int = totalPages
@Volatile
private var hasLoadFailure = false
var onTotalPagesChanged: ((Int) -> Unit)? = null var onTotalPagesChanged: ((Int) -> Unit)? = null
@Volatile
var onPageLoaded: ((position: Int) -> Unit)? = null
// 后台协程用于预加载。 // 后台协程用于预加载。
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val messageId = AtomicLong((totalPages.toLong() + 1) * 1_000_000L) private val messageId = AtomicLong((totalPages.toLong() + 1) * 1_000_000L)
//获取指定位置的聊天页面数据 //获取指定位置的聊天页面数据(非阻塞,缓存未命中时触发后台加载)
fun getPage(position: Int): ChatPageData { fun getPage(position: Int): ChatPageData {
if (position < 0 || position >= availablePages) { if (position < 0 || position >= availablePages) {
return emptyPage(position) return emptyPage(position)
@@ -62,18 +66,9 @@ class CircleChatRepository(
val cached = synchronized(lock) { cache.get(position) } val cached = synchronized(lock) { cache.get(position) }
if (cached != null) return cached if (cached != null) return cached
val page = createPage(position) // 缓存未命中:触发后台预加载,立即返回空占位页避免阻塞调用线程
return synchronized(lock) { preloadAround(position)
val existing = cache.get(position) return emptyPage(position)
if (existing != null) {
inFlight.remove(position)
existing
} else {
cache.put(position, page)
inFlight.remove(position)
page
}
}
} }
//主要功能是根据用户当前查看的页面位置,预加载其前后若干页的数据,并将这些数据放入缓存中 //主要功能是根据用户当前查看的页面位置,预加载其前后若干页的数据,并将这些数据放入缓存中
@@ -88,6 +83,7 @@ class CircleChatRepository(
// 清除加载失败的缓存页面companionId <= 0使后续 preloadAround 能重新加载 // 清除加载失败的缓存页面companionId <= 0使后续 preloadAround 能重新加载
fun invalidateFailedPages() { fun invalidateFailedPages() {
synchronized(lock) { synchronized(lock) {
hasLoadFailure = false
val snapshot = cache.snapshot() val snapshot = cache.snapshot()
for ((position, page) in snapshot) { for ((position, page) in snapshot) {
if (page.companionId <= 0) { if (page.companionId <= 0) {
@@ -97,8 +93,24 @@ class CircleChatRepository(
} }
} }
// 检查缓存中是否存在加载失败的页面 // 清除全部缓存和状态,用于下拉刷新等场景强制重新加载所有数据
fun invalidateAll() {
synchronized(lock) {
cache.evictAll()
companionCache.evictAll()
inFlight.clear()
pageInFlight.clear()
pageFetched.clear()
historyStates.clear()
hasLoadFailure = false
knownTotalPages = null
availablePages = totalPages
}
}
// 检查是否存在加载失败的页面
fun hasFailedPages(): Boolean { fun hasFailedPages(): Boolean {
if (hasLoadFailure) return true
synchronized(lock) { synchronized(lock) {
for ((_, page) in cache.snapshot()) { for ((_, page) in cache.snapshot()) {
if (page.companionId <= 0) return true if (page.companionId <= 0) return true
@@ -264,6 +276,18 @@ class CircleChatRepository(
} }
} }
fun removeMessage(position: Int, messageId: Long): Int {
synchronized(lock) {
val page = getPage(position)
val index = page.messages.indexOfFirst { it.id == messageId }
if (index >= 0) {
page.messages.removeAt(index)
page.messageVersion++
}
return index
}
}
fun updateLikeState(position: Int, companionId: Int, liked: Boolean, likeCount: Int): Boolean { fun updateLikeState(position: Int, companionId: Int, liked: Boolean, likeCount: Int): Boolean {
synchronized(lock) { synchronized(lock) {
val page = cache.get(position) val page = cache.get(position)
@@ -307,6 +331,39 @@ class CircleChatRepository(
// return sampleLines[random.nextInt(sampleLines.size)] // return sampleLines[random.nextInt(sampleLines.size)]
// } // }
/**
* 刷新指定 companionId 对应页面的聊天记录(从服务端重新获取第 1 页)。
* 返回被更新的 position 列表,用于通知 UI 刷新。
* 注意:此方法包含网络 IO必须在后台线程调用。
*/
fun refreshCompanionMessages(companionId: Int): List<Int> {
if (companionId <= 0) return emptyList()
val updatedPositions = ArrayList<Int>()
val matchedPositions = synchronized(lock) {
cache.snapshot().filter { it.value.companionId == companionId }.keys.toList()
}
if (matchedPositions.isEmpty()) return emptyList()
val response = fetchChatRecords(companionId, 1, DEFAULT_CHAT_PAGE_SIZE)
val freshMessages = mapChatRecords(response.data?.records)
synchronized(lock) {
for (position in matchedPositions) {
val page = cache.get(position) ?: continue
if (page.companionId != companionId) continue
page.messages.clear()
page.messages.addAll(freshMessages)
page.messageVersion++
updatedPositions.add(position)
}
historyStates.remove(companionId)
if (response.data != null) {
updateHistoryState(companionId, response.data, 1)
}
}
return updatedPositions
}
fun close() { fun close() {
scope.cancel() scope.cancel()
} }
@@ -332,32 +389,6 @@ class CircleChatRepository(
} }
//主要功能是确保指定位置的聊天页面数据已经存在于缓存中。如果指定位置的页面数据不存在,则生成该页面的数据并将其放入缓存中
private fun createPage(position: Int): ChatPageData {
val cachedCompanion = synchronized(lock) { companionCache.get(position) }
val companionInfo = cachedCompanion ?: run {
val pageNum = position / pageFetchSize + 1
val records = fetchCompanionPage(pageNum, pageFetchSize)
val index = position - (pageNum - 1) * pageFetchSize
records.getOrNull(index)
}
if (companionInfo == null) {
return emptyPage(position)
}
val historyResponse = fetchChatRecords(
companionInfo.id,
1,
DEFAULT_CHAT_PAGE_SIZE
).data
val messages = historyResponse?.records
updateHistoryState(companionInfo.id, historyResponse, 1)
Log.d("1314520-CircleChatRepository", "createPage: $position")
return buildPageData(position, companionInfo, messages)
}
private fun preloadRange(start: Int, end: Int, pageSize: Int, chatPageSize: Int) { private fun preloadRange(start: Int, end: Int, pageSize: Int, chatPageSize: Int) {
val maxPages = availablePages val maxPages = availablePages
if (maxPages <= 0) return if (maxPages <= 0) return
@@ -413,11 +444,20 @@ class CircleChatRepository(
val messages = historyResponse?.records val messages = historyResponse?.records
updateHistoryState(record.id, historyResponse, 1) updateHistoryState(record.id, historyResponse, 1)
val pageData = buildPageData(position, record, messages) val pageData = buildPageData(position, record, messages)
synchronized(lock) { val wasInserted = synchronized(lock) {
if (cache.get(position) == null) { if (cache.get(position) == null) {
cache.put(position, pageData) cache.put(position, pageData)
}
inFlight.remove(position) inFlight.remove(position)
true
} else {
inFlight.remove(position)
false
}
}
if (wasInserted) {
onPageLoaded?.let { callback ->
scope.launch(Dispatchers.Main) { callback(position) }
}
} }
} }
} }
@@ -499,6 +539,9 @@ class CircleChatRepository(
updateAvailablePages(available) updateAvailablePages(available)
} }
shouldMarkFetched = data != null shouldMarkFetched = data != null
if (!shouldMarkFetched) {
hasLoadFailure = true
}
} finally { } finally {
val startPos = (pageNum - 1) * pageSize val startPos = (pageNum - 1) * pageSize
synchronized(lock) { synchronized(lock) {
@@ -603,7 +646,8 @@ class CircleChatRepository(
avatarUrl = "", avatarUrl = "",
likeCount = 0, likeCount = 0,
commentCount = 0, commentCount = 0,
liked = false liked = false,
messageVersion = -1
) )
} }

View File

@@ -96,8 +96,8 @@ class CircleCommentAdapter(
Glide.with(avatarView) Glide.with(avatarView)
.load(comment.userAvatar) .load(comment.userAvatar)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(avatarView) .into(avatarView)
userNameView.text = displayName userNameView.text = displayName

View File

@@ -76,8 +76,8 @@ class CircleCommentReplyAdapter(
Glide.with(avatarView) Glide.with(avatarView)
.load(comment.userAvatar) .load(comment.userAvatar)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(avatarView) .into(avatarView)
userNameView.text = displayName userNameView.text = displayName

View File

@@ -158,6 +158,16 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog.window?.setDimAmount(0f) dialog.window?.setDimAmount(0f)
// 给原始 BlurView 设置固定高度,防止 sheet 缩放时触发 onSizeChanged 导致冻结的内部位图失效
commentBlur?.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
height
)
// 延迟冻结原始 BlurView等 sheet 展开动画完成 + 渲染几帧后再冻结
commentBlur?.postDelayed({
commentBlur?.setBlurAutoUpdate(false)
}, 600)
} }
private fun bindViews(view: View) { private fun bindViews(view: View) {
@@ -297,6 +307,7 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
?: return ?: return
val blurRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 14f else 10f val blurRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 14f else 10f
val overlayColor = ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
try { try {
val algorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val algorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
RenderEffectBlur() RenderEffectBlur()
@@ -307,14 +318,10 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
.setFrameClearDrawable(requireActivity().window.decorView.background) .setFrameClearDrawable(requireActivity().window.decorView.background)
.setBlurRadius(blurRadius) .setBlurRadius(blurRadius)
.setBlurAutoUpdate(true) .setBlurAutoUpdate(true)
.setOverlayColor( .setOverlayColor(overlayColor)
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
)
} catch (_: Throwable) { } catch (_: Throwable) {
blurView.visibility = View.GONE blurView.visibility = View.GONE
commentCard.setCardBackgroundColor( commentCard.setCardBackgroundColor(overlayColor)
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
)
} }
} }
@@ -869,18 +876,6 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
} }
private fun updateBlurForIme(imeVisible: Boolean) { private fun updateBlurForIme(imeVisible: Boolean) {
val blurView = commentBlur ?: return // 毛玻璃始终冻结显示,不做切换
if (imeVisible) {
// 键盘出来:禁用毛玻璃,避免错位
blurView.visibility = View.GONE
commentCard.setCardBackgroundColor(
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
)
} else {
// 键盘收起:恢复毛玻璃
blurView.visibility = View.VISIBLE
blurView.invalidate()
}
} }
} }

View File

@@ -60,8 +60,8 @@ class CircleDrawerMenuAdapter(
Glide.with(itemView.context) Glide.with(itemView.context)
.load(item.avatarUrl) .load(item.avatarUrl)
.placeholder(R.drawable.a123123123) .placeholder(R.drawable.component_loading)
.error(R.drawable.a123123123) .error(R.drawable.default_avatar)
.into(ivAvatar) .into(ivAvatar)
// 选中状态显示不同图标和大小 // 选中状态显示不同图标和大小

View File

@@ -33,6 +33,7 @@ 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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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
@@ -41,6 +42,7 @@ 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
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max import kotlin.math.max
@@ -56,7 +58,14 @@ import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import android.widget.ImageView import android.widget.ImageView
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.widget.PopupWindow
import com.example.myapplication.network.AiCompanion import com.example.myapplication.network.AiCompanion
import com.example.myapplication.network.chatDeleteRequest
import com.example.myapplication.keyboard.AiRolePreferences import com.example.myapplication.keyboard.AiRolePreferences
import java.io.File import java.io.File
@@ -65,6 +74,7 @@ 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 noResultOverlay: View
private lateinit var noResultSwipeRefresh: SwipeRefreshLayout
private lateinit var imeDismissOverlay: 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
@@ -195,6 +205,13 @@ 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) noResultOverlay = view.findViewById(R.id.noResultOverlay)
noResultSwipeRefresh = view.findViewById(R.id.noResultSwipeRefresh)
noResultSwipeRefresh.setColorSchemeColors(
Color.parseColor("#02BEAC"),
Color.parseColor("#1B1F1A"),
Color.parseColor("#9F9F9F")
)
noResultSwipeRefresh.setOnRefreshListener { refreshAllCircleData() }
imeDismissOverlay = view.findViewById(R.id.imeDismissOverlay) 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)
@@ -307,8 +324,22 @@ class CircleFragment : Fragment() {
sharedPool = sharedChatPool, sharedPool = sharedChatPool,
onLikeClick = { position, companionId -> handleLikeClick(position, companionId) }, onLikeClick = { position, companionId -> handleLikeClick(position, companionId) },
onCommentClick = { companionId, commentCount -> showCommentSheet(companionId, commentCount) }, onCommentClick = { companionId, commentCount -> showCommentSheet(companionId, commentCount) },
onAvatarClick = { companionId -> openCharacterDetails(companionId) } onAvatarClick = { companionId -> openCharacterDetails(companionId) },
onMessageLongClick = { message, anchorView, rawX, rawY ->
showChatMessagePopup(message, anchorView, rawX, rawY)
}
) )
// 后台预加载完成时刷新对应的列表项
repository.onPageLoaded = { position ->
if (isAdded && view != null) {
pageAdapter.notifyItemChanged(position)
val page = repository.getPage(position)
if (page.companionId > 0) {
setNoResultVisible(false)
noResultSwipeRefresh.isRefreshing = false
}
}
}
parentFragmentManager.setFragmentResultListener( parentFragmentManager.setFragmentResultListener(
RESULT_COMMENT_COUNT_UPDATED, RESULT_COMMENT_COUNT_UPDATED,
viewLifecycleOwner viewLifecycleOwner
@@ -418,6 +449,17 @@ class CircleFragment : Fragment() {
} }
} }
} }
// 监听输入法聊天更新事件,刷新对应角色的聊天记录
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
AuthEventBus.events.collect { event ->
if (event is AuthEvent.KeyboardChatUpdated) {
handleKeyboardChatUpdated(event.companionId)
}
}
}
}
} }
override fun onResume() { override fun onResume() {
@@ -425,6 +467,8 @@ class CircleFragment : Fragment() {
view?.post { requestOverlayUpdate() } view?.post { requestOverlayUpdate() }
// 检查是否有加载失败的页面,如果有则重新加载 // 检查是否有加载失败的页面,如果有则重新加载
retryFailedPages() retryFailedPages()
// 消费后台期间积累的脏标记,刷新对应角色聊天记录
consumePendingDirtyCompanions()
} }
// 清除失败缓存并重新加载 // 清除失败缓存并重新加载
@@ -439,8 +483,52 @@ class CircleFragment : Fragment() {
updateNoResultOverlayFromFirstPage() updateNoResultOverlayFromFirstPage()
} }
// 输入法聊天更新后,刷新对应角色在缓存中的聊天记录
private fun handleKeyboardChatUpdated(companionId: Int) {
if (!::repository.isInitialized) return
viewLifecycleOwner.lifecycleScope.launch {
val updatedPositions = withContext(Dispatchers.IO) {
repository.refreshCompanionMessages(companionId)
}
for (position in updatedPositions) {
pageAdapter.notifyItemChanged(position)
}
}
}
// 消费后台期间通过 SharedPreferences 持久化的脏标记
private fun consumePendingDirtyCompanions() {
if (!::repository.isInitialized) return
val dirtyIds = AiRolePreferences.consumeDirtyCompanions(requireContext())
if (dirtyIds.isEmpty()) return
for (companionId in dirtyIds) {
handleKeyboardChatUpdated(companionId)
}
}
// 下拉刷新:清除全部缓存,重新加载所有接口数据
private fun refreshAllCircleData() {
repository.invalidateAll()
currentPage = RecyclerView.NO_POSITION
pageAdapter.notifyDataSetChanged()
loadDrawerMenuData()
repository.preloadInitialPages()
updateNoResultOverlayFromFirstPage()
// 超时保护10秒后无论如何停止刷新动画
viewLifecycleOwner.lifecycleScope.launch {
delay(10_000)
if (::noResultSwipeRefresh.isInitialized) {
noResultSwipeRefresh.isRefreshing = false
}
}
}
//清理和恢复输入框的高CircleFragment 在生命周期结束时的状态 //清理和恢复输入框的高CircleFragment 在生命周期结束时的状态
override fun onDestroyView() { override fun onDestroyView() {
if (::repository.isInitialized) {
repository.onPageLoaded = null
}
view?.let { root -> view?.let { root ->
ViewCompat.setWindowInsetsAnimationCallback(root, null) ViewCompat.setWindowInsetsAnimationCallback(root, null)
root.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener) root.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener)
@@ -823,7 +911,8 @@ class CircleFragment : Fragment() {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("1314520-Circle", "chatMessage request failed: ${e.message}", e) Log.e("1314520-Circle", "chatMessage request failed: ${e.message}", e)
markBotPlaceholderFailed(page, placeholder, requestFailedText) val removedIndex = repository.removeMessage(page, placeholder.id)
notifyMessageRemoved(page, removedIndex)
return@launch return@launch
} }
val data = response.data val data = response.data
@@ -832,7 +921,9 @@ class CircleFragment : Fragment() {
"1314520-Circle", "1314520-Circle",
"chatMessage failed code=${response.code} message=${response.message}" "chatMessage failed code=${response.code} message=${response.message}"
) )
markBotPlaceholderFailed(page, placeholder, requestFailedText) val removedIndex = repository.removeMessage(page, placeholder.id)
notifyMessageRemoved(page, removedIndex)
showChatErrorRechargeDialog(response.message)
return@launch return@launch
} }
@@ -862,6 +953,29 @@ class CircleFragment : Fragment() {
notifyMessageUpdated(page, placeholder.id) notifyMessageUpdated(page, placeholder.id)
} }
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
val dialogView = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_chat_error_recharge, null)
val dialog = android.app.AlertDialog.Builder(requireContext())
.setView(dialogView)
.setCancelable(true)
.create()
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
dialog.window?.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
dialog.window?.setDimAmount(0.5f)
if (!errorMessage.isNullOrEmpty()) {
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
}
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
dialog.dismiss()
}
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
dialog.dismiss()
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
}
dialog.show()
}
//确定 RecyclerView 中当前选中的页面位置 //确定 RecyclerView 中当前选中的页面位置
private fun resolveCurrentPage(): Int { private fun resolveCurrentPage(): Int {
if (currentPage != RecyclerView.NO_POSITION) return currentPage if (currentPage != RecyclerView.NO_POSITION) return currentPage
@@ -897,6 +1011,15 @@ class CircleFragment : Fragment() {
} }
} }
private fun notifyMessageRemoved(pagePosition: Int, messageIndex: Int) {
val holder = pageRv.findViewHolderForAdapterPosition(pagePosition) as? ChatPageViewHolder
if (holder != null) {
holder.notifyMessageRemoved(messageIndex)
} else {
pageAdapter.notifyItemChanged(pagePosition)
}
}
private fun handleLikeClick(pagePosition: Int, companionId: Int) { private fun handleLikeClick(pagePosition: Int, companionId: Int) {
if (pagePosition == RecyclerView.NO_POSITION || companionId <= 0) return if (pagePosition == RecyclerView.NO_POSITION || companionId <= 0) return
if (!likeInFlight.add(companionId)) return if (!likeInFlight.add(companionId)) return
@@ -979,6 +1102,104 @@ class CircleFragment : Fragment() {
.show(fm, CircleCommentSheet.TAG) .show(fm, CircleCommentSheet.TAG)
} }
private fun showChatMessagePopup(
message: ChatMessage,
anchorView: View,
rawX: Float,
rawY: Float
) {
val ctx = context ?: return
val popupView = LayoutInflater.from(ctx)
.inflate(R.layout.popup_chat_message_menu, null)
// AI 消息显示:复制、删除、举报;用户消息显示:复制、删除
if (message.isMine) {
popupView.findViewById<View>(R.id.menuReport).visibility = View.GONE
popupView.findViewById<View>(R.id.divider2).visibility = View.GONE
}
popupView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
val popupW = popupView.measuredWidth
val popupH = popupView.measuredHeight
val popupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
popupWindow.elevation = 8f
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// 复制
popupView.findViewById<View>(R.id.menuCopy).setOnClickListener {
popupWindow.dismiss()
val clipboard = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("chat_message", message.text))
Toast.makeText(ctx, R.string.chat_copy_success, Toast.LENGTH_SHORT).show()
}
// 删除
popupView.findViewById<View>(R.id.menuDelete).setOnClickListener {
popupWindow.dismiss()
handleDeleteMessage(message)
}
// 举报
popupView.findViewById<View>(R.id.menuReport).setOnClickListener {
popupWindow.dismiss()
val page = resolveCurrentPage()
if (page == RecyclerView.NO_POSITION) return@setOnClickListener
val companionId = repository.getPage(page).companionId
if (companionId <= 0) return@setOnClickListener
AuthEventBus.emit(
AuthEvent.OpenCirclePage(
R.id.circleAiCharacterReportFragment,
bundleOf(ARG_COMPANION_ID to companionId)
)
)
}
// 智能定位:优先显示在触摸点上方,空间不够则显示在下方
val screenWidth = resources.displayMetrics.widthPixels
val screenHeight = resources.displayMetrics.heightPixels
val touchX = rawX.toInt()
val touchY = rawY.toInt()
val margin = (8 * resources.displayMetrics.density).toInt()
val x = (touchX - popupW / 2).coerceIn(margin, screenWidth - popupW - margin)
val y = if (touchY - popupH - margin > 0) {
touchY - popupH - margin
} else {
touchY + margin
}
popupWindow.showAtLocation(pageRv, Gravity.NO_GRAVITY, x, y)
}
private fun handleDeleteMessage(message: ChatMessage) {
val page = resolveCurrentPage()
if (page == RecyclerView.NO_POSITION) return
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
RetrofitClient.apiService.chatDelete(chatDeleteRequest(id = message.id.toInt()))
}
} catch (e: Exception) {
Log.e("1314520-Circle", "chatDelete failed: ${e.message}", e)
return@launch
}
val removedIndex = repository.removeMessage(page, message.id)
if (removedIndex >= 0) {
notifyMessageRemoved(page, removedIndex)
}
}
}
//同步当前页面的选中状态 //同步当前页面的选中状态
private fun syncCurrentPage() { private fun syncCurrentPage() {
val lm = pageRv.layoutManager as? LinearLayoutManager ?: return val lm = pageRv.layoutManager as? LinearLayoutManager ?: return
@@ -1140,6 +1361,9 @@ class CircleFragment : Fragment() {
blur.visibility = View.GONE blur.visibility = View.GONE
} }
} else { } else {
// global 页面可见时不恢复底栏
val globalContainer = activity?.findViewById<View>(R.id.global_container)
if (globalContainer != null && globalContainer.visibility == View.VISIBLE) return
// 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE // 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE
nav.visibility = View.VISIBLE nav.visibility = View.VISIBLE
prevBottomNavVisibility = null prevBottomNavVisibility = null

View File

@@ -10,7 +10,8 @@ class CirclePageAdapter(
private val sharedPool: RecyclerView.RecycledViewPool, private val sharedPool: RecyclerView.RecycledViewPool,
private val onLikeClick: ((position: Int, companionId: Int) -> Unit)? = null, private val onLikeClick: ((position: Int, companionId: Int) -> Unit)? = null,
private val onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)? = null, private val onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)? = null,
private val onAvatarClick: ((companionId: Int) -> Unit)? = null private val onAvatarClick: ((companionId: Int) -> Unit)? = null,
private val onMessageLongClick: ((message: ChatMessage, anchorView: android.view.View, rawX: Float, rawY: Float) -> Unit)? = null
) : RecyclerView.Adapter<PageViewHolder>() { ) : RecyclerView.Adapter<PageViewHolder>() {
// 每页固定为屏幕高度,配合 PagerSnapHelper 使用。 // 每页固定为屏幕高度,配合 PagerSnapHelper 使用。
@@ -53,7 +54,8 @@ class CirclePageAdapter(
}, },
onLikeClick, onLikeClick,
onCommentClick, onCommentClick,
onAvatarClick onAvatarClick,
onMessageLongClick
) )
} }

View File

@@ -17,11 +17,12 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
var allowParentInterceptAtTop: (() -> Boolean)? = null var allowParentInterceptAtTop: (() -> Boolean)? = null
var onTopPull: (() -> Unit)? = null var onTopPull: (() -> Unit)? = null
override fun onTouchEvent(e: MotionEvent): Boolean { override fun dispatchTouchEvent(e: MotionEvent): Boolean {
when (e.actionMasked) { when (e.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
lastY = e.y lastY = e.y
topPullTriggered = false topPullTriggered = false
// 在分发阶段就抢占触摸,防止外层 pageRv 拦截
parent?.requestDisallowInterceptTouchEvent(true) parent?.requestDisallowInterceptTouchEvent(true)
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
@@ -32,6 +33,14 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
val canScrollDown = canScrollVertically(1) val canScrollDown = canScrollVertically(1)
val scrollingDown = dy > 0 val scrollingDown = dy > 0
if (!canScrollUp && !canScrollDown) {
// 列表内容不足以滚动,放行给父级翻页
if (scrollingDown && !topPullTriggered) {
topPullTriggered = true
onTopPull?.invoke()
}
parent?.requestDisallowInterceptTouchEvent(false)
} else {
val disallow = if (scrollingDown) { val disallow = if (scrollingDown) {
if (!canScrollUp) { if (!canScrollUp) {
if (!topPullTriggered) { if (!topPullTriggered) {
@@ -42,19 +51,20 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
!allowParent !allowParent
} else { } else {
topPullTriggered = false topPullTriggered = false
canScrollUp true
} }
} else { } else {
canScrollDown canScrollDown
} }
parent?.requestDisallowInterceptTouchEvent(disallow) parent?.requestDisallowInterceptTouchEvent(disallow)
} }
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_CANCEL -> {
parent?.requestDisallowInterceptTouchEvent(false) parent?.requestDisallowInterceptTouchEvent(false)
topPullTriggered = false topPullTriggered = false
} }
} }
return super.onTouchEvent(e) return super.dispatchTouchEvent(e)
} }
} }

View File

@@ -40,8 +40,8 @@ class ThumbsUpAdapter(
fun bind(item: companionLikedResponse) { fun bind(item: companionLikedResponse) {
Glide.with(ivAvatar) Glide.with(ivAvatar)
.load(item.avatarUrl) .load(item.avatarUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(ivAvatar) .into(ivAvatar)
tvName.text = item.name tvName.text = item.name
@@ -96,8 +96,8 @@ class ChattingAdapter(
fun bind(item: companionChattedResponse) { fun bind(item: companionChattedResponse) {
Glide.with(ivAvatar) Glide.with(ivAvatar)
.load(item.avatarUrl) .load(item.avatarUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(ivAvatar) .into(ivAvatar)
tvName.text = item.name tvName.text = item.name

View File

@@ -123,8 +123,8 @@ class CircleCharacterDetailsFragment : Fragment() {
introTextView.text = data.introText introTextView.text = data.introText
Glide.with(coverImageView) Glide.with(coverImageView)
.load(data.coverImageUrl) .load(data.coverImageUrl)
.placeholder(R.drawable.bg) .placeholder(R.drawable.component_loading)
.error(R.drawable.bg) .error(R.drawable.no_search_result)
.transition(DrawableTransitionOptions.withCrossFade(180)) .transition(DrawableTransitionOptions.withCrossFade(180))
.listener(object : RequestListener<Drawable> { .listener(object : RequestListener<Drawable> {
override fun onLoadFailed( override fun onLoadFailed(

View File

@@ -1,6 +1,7 @@
package com.example.myapplication.ui.home package com.example.myapplication.ui.home
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.TransitionDrawable import android.graphics.drawable.TransitionDrawable
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@@ -23,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager2.widget.ViewPager2 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
@@ -63,6 +65,7 @@ class HomeFragment : Fragment() {
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 lateinit var noResultOverlay: View
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private var lastList1RenderKey: String? = null private var lastList1RenderKey: String? = null
private lateinit var loadingOverlay: LoadingOverlay private lateinit var loadingOverlay: LoadingOverlay
@@ -265,6 +268,7 @@ class HomeFragment : Fragment() {
viewPager = view.findViewById(R.id.viewPager) viewPager = view.findViewById(R.id.viewPager)
viewPager.isSaveEnabled = false viewPager.isSaveEnabled = false
viewPager.offscreenPageLimit = 2 viewPager.offscreenPageLimit = 2
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage) backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay) noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay)
@@ -296,6 +300,20 @@ class HomeFragment : Fragment() {
// ✅ setupViewPager 只初始化一次 // ✅ setupViewPager 只初始化一次
setupViewPagerOnce() setupViewPagerOnce()
// 下拉刷新
swipeRefreshLayout.setColorSchemeColors(
Color.parseColor("#02BEAC"),
Color.parseColor("#1B1F1A"),
Color.parseColor("#9F9F9F")
)
swipeRefreshLayout.setOnRefreshListener {
refreshAllData { swipeRefreshLayout.isRefreshing = false }
}
swipeRefreshLayout.setOnChildScrollUpCallback { _, _ ->
// BottomSheet 未折叠时,让 BottomSheet 自己处理拖拽,不触发刷新
bottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED
}
// 标签 UI 初始为空 // 标签 UI 初始为空
setupTags() setupTags()
@@ -708,25 +726,29 @@ class HomeFragment : Fragment() {
} }
private fun refreshHomeAfterNetwork() { private fun refreshHomeAfterNetwork() {
loadingOverlay.show()
refreshAllData { loadingOverlay.hide() }
}
private fun refreshAllData(onFinish: () -> Unit) {
networkRefreshJob?.cancel() networkRefreshJob?.cancel()
networkRefreshJob = viewLifecycleOwner.lifecycleScope.launch { networkRefreshJob = viewLifecycleOwner.lifecycleScope.launch {
preloadJob?.cancel() preloadJob?.cancel()
loadingOverlay.show()
try { try {
list1Loaded = false // 先获取所有数据,不动 UI
setNoResultVisible(false) val newPersonaList = fetchAllPersonaList()
val list = fetchAllPersonaList()
if (!isAdded) return@launch if (!isAdded) return@launch
allPersonaCache = list val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
// 数据全部返回后,一次性更新 UI
allPersonaCache = newPersonaList
list1Loaded = true list1Loaded = true
lastList1RenderKey = null lastList1RenderKey = null
personaCache.clear() personaCache.clear()
notifyPageChangedOnMain(0) notifyPageChangedOnMain(0)
updateNoResultOverlay(0) updateNoResultOverlay(0)
val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
tags.clear() tags.clear()
response.data?.let { networkTags -> response.data?.let { networkTags ->
tags.addAll(networkTags.map { Tag(it.id, it.tagName) }) tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
@@ -735,9 +757,9 @@ class HomeFragment : Fragment() {
setupTags() setupTags()
startPreloadAllTagsFillCacheOnly() startPreloadAllTagsFillCacheOnly()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("HomeFragment", "refresh after network fail", e) Log.e("HomeFragment", "refresh data fail", e)
} finally { } finally {
loadingOverlay.hide() onFinish()
} }
} }
} }
@@ -952,7 +974,11 @@ class HomeFragment : Fragment() {
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: "" itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar) val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv) com.bumptech.glide.Glide.with(iv)
.load(p.avatarUrl)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(iv)
// ---------------- add 按钮(失败回滚 + 防连点) ---------------- // ---------------- add 按钮(失败回滚 + 防连点) ----------------
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add) val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
@@ -1046,7 +1072,11 @@ class HomeFragment : Fragment() {
addBtn.isVisible = true addBtn.isVisible = true
name.text = item.characterName ?: "" name.text = item.characterName ?: ""
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar) com.bumptech.glide.Glide.with(avatar)
.load(item.avatarUrl)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar)
// ✅ 记录“原始背景/原始icon”用于 added=false 时恢复 // ✅ 记录“原始背景/原始icon”用于 added=false 时恢复
val originBg = addBtn.background val originBg = addBtn.background

View File

@@ -41,8 +41,8 @@ class PersonaAdapter(
Glide.with(itemView.context) Glide.with(itemView.context)
.load(item.avatarUrl) .load(item.avatarUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(ivAvatar) .into(ivAvatar)
val isAdded = item.added val isAdded = item.added

View File

@@ -68,8 +68,8 @@ class PersonaDetailDialogFragment : DialogFragment() {
Glide.with(requireContext()) Glide.with(requireContext())
.load(data.avatarUrl) .load(data.avatarUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(ivAvatar) .into(ivAvatar)
btnAdd.setOnClickListener { btnAdd.setOnClickListener {

View File

@@ -27,6 +27,7 @@ import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.SubjectTag import com.example.myapplication.network.SubjectTag
import com.example.myapplication.network.themeDetail import com.example.myapplication.network.themeDetail
import com.example.myapplication.network.purchaseThemeRequest import com.example.myapplication.network.purchaseThemeRequest
import com.example.myapplication.network.restoreThemeRequest
import com.example.myapplication.ui.shop.ThemeCardAdapter import com.example.myapplication.ui.shop.ThemeCardAdapter
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,6 +39,7 @@ import com.example.myapplication.ui.shop.ShopEventBus
import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.network.BehaviorReporter
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.ui.common.LoadingOverlay
class KeyboardDetailFragment : Fragment() { class KeyboardDetailFragment : Fragment() {
@@ -53,6 +55,7 @@ class KeyboardDetailFragment : Fragment() {
private lateinit var enabledButtonText: TextView private lateinit var enabledButtonText: TextView
private lateinit var progressBar: android.widget.ProgressBar private lateinit var progressBar: android.widget.ProgressBar
private lateinit var swipeRefreshLayout: SwipeRefreshLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var loadingOverlay: LoadingOverlay
private var themeDetailResp: themeDetail? = null private var themeDetailResp: themeDetail? = null
override fun onCreateView( override fun onCreateView(
@@ -77,21 +80,22 @@ class KeyboardDetailFragment : Fragment() {
enabledButtonText = view.findViewById<TextView>(R.id.enabledButtonText) enabledButtonText = view.findViewById<TextView>(R.id.enabledButtonText)
progressBar = view.findViewById<android.widget.ProgressBar>(R.id.progressBar) progressBar = view.findViewById<android.widget.ProgressBar>(R.id.progressBar)
swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout) swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
// 设置按钮始终防止事件穿透的触摸监听 // 设置按钮始终防止事件穿透的触摸监听<EFBFBD>?
enabledButton.setOnTouchListener { _, event -> enabledButton.setOnTouchListener { _, event ->
// 如果按钮被禁用,消耗所有触摸事件防止穿 // 如果按钮被禁用,消耗所有触摸事件防止穿<EFBFBD>?
if (!enabledButton.isEnabled) { if (!enabledButton.isEnabled) {
return@setOnTouchListener true return@setOnTouchListener true
} }
// 如果按钮启用,不消耗事件,让按钮正常处理点 // 如果按钮启用,不消耗事件,让按钮正常处理点<EFBFBD>?
return@setOnTouchListener false return@setOnTouchListener false
} }
// 初始化RecyclerView // 初始化RecyclerView
setupRecyclerView() setupRecyclerView()
// 设置下拉刷新监听 // 设置下拉刷新监听<EFBFBD>?
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
loadData() loadData()
} }
@@ -109,7 +113,7 @@ class KeyboardDetailFragment : Fragment() {
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
} }
//充值主 //充值主<EFBFBD>?
rechargeButton.setOnClickListener { rechargeButton.setOnClickListener {
showPurchaseConfirmationDialog(themeId) showPurchaseConfirmationDialog(themeId)
} }
@@ -136,13 +140,18 @@ class KeyboardDetailFragment : Fragment() {
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
// 下拉刷新时已有自带动画仅首次加载显<E8BDBD>?overlay
if (!swipeRefreshLayout.isRefreshing) {
loadingOverlay.show()
}
try { try {
themeDetailResp = getThemeDetail(themeId)?.data themeDetailResp = getThemeDetail(themeId)?.data
val recommendThemeListResp = getrecommendThemeList()?.data val recommendThemeListResp = getrecommendThemeList()?.data
Glide.with(requireView().context) Glide.with(requireView().context)
.load(themeDetailResp?.themePreviewImageUrl) .load(themeDetailResp?.themePreviewImageUrl)
.placeholder(R.drawable.bg) .placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(shapeableImageView) .into(shapeableImageView)
tvKeyboardName.text = themeDetailResp?.themeName tvKeyboardName.text = themeDetailResp?.themeName
@@ -162,7 +171,7 @@ class KeyboardDetailFragment : Fragment() {
renderTags(tags) renderTags(tags)
} }
// 渲染推荐主题列表剔除当前themeId // 渲染推荐主题列表剔除当前themeId<EFBFBD>?
recommendThemeListResp?.let { themes -> recommendThemeListResp?.let { themes ->
val filteredThemes = themes.filter { it.id != themeId } val filteredThemes = themes.filter { it.id != themeId }
themeCardAdapter.submitList(filteredThemes) themeCardAdapter.submitList(filteredThemes)
@@ -173,6 +182,7 @@ class KeyboardDetailFragment : Fragment() {
} finally { } finally {
// 停止刷新动画 // 停止刷新动画
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
loadingOverlay.hide()
} }
} }
} }
@@ -183,9 +193,9 @@ class KeyboardDetailFragment : Fragment() {
if (tags.isEmpty()) return if (tags.isEmpty()) return
val context = layoutTagsContainer.context val context = layoutTagsContainer.context
val tagsPerRow = 5 // 每行固定显示5个标 val tagsPerRow = 5 // 每行固定显示5个标<EFBFBD>?
// 将标签分组每行6 // 将标签分组每行6<EFBFBD>?
val rows = tags.chunked(tagsPerRow) val rows = tags.chunked(tagsPerRow)
rows.forEach { rowTags -> rows.forEach { rowTags ->
@@ -212,7 +222,7 @@ class KeyboardDetailFragment : Fragment() {
setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
gravity = Gravity.CENTER gravity = Gravity.CENTER
// 设置内边距左右12dp下5dp // 设置内边距左右12dp<EFBFBD>?dp
val horizontalPadding = TypedValue.applyDimension( val horizontalPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 12f, resources.displayMetrics TypedValue.COMPLEX_UNIT_DIP, 12f, resources.displayMetrics
).toInt() ).toInt()
@@ -221,14 +231,14 @@ class KeyboardDetailFragment : Fragment() {
).toInt() ).toInt()
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding) setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
// 设置背景50dp圆角 // 设置背景<EFBFBD>?0dp圆角<EFBFBD>?
background = ContextCompat.getDrawable(context, R.drawable.tag_background)?.mutate() background = ContextCompat.getDrawable(context, R.drawable.tag_background)?.mutate()
background?.setTint(android.graphics.Color.parseColor(tag.color)) background?.setTint(android.graphics.Color.parseColor(tag.color))
} }
// 使用权重布局,让标签自适应间距 // 使用权重布局,让标签自适应间距
val layoutParams = LinearLayout.LayoutParams( val layoutParams = LinearLayout.LayoutParams(
0, // 宽度设为0使用权 0, // 宽度设为0使用权<EFBFBD>?
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT,
tagWeight tagWeight
).apply { ).apply {
@@ -241,13 +251,13 @@ class KeyboardDetailFragment : Fragment() {
rowLayout.addView(tagView, layoutParams) rowLayout.addView(tagView, layoutParams)
} }
// 如果当前行标签数量不足6添加空View填充剩余空间 // 如果当前行标签数量不<EFBFBD>?添加空View填充剩余空间
val remainingTags = tagsPerRow - rowTags.size val remainingTags = tagsPerRow - rowTags.size
if (remainingTags > 0) { if (remainingTags > 0) {
repeat(remainingTags) { repeat(remainingTags) {
val emptyView = View(context) val emptyView = View(context)
val layoutParams = LinearLayout.LayoutParams( val layoutParams = LinearLayout.LayoutParams(
0, // 宽度设为0使用权 0, // 宽度设为0使用权<EFBFBD>?
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT,
tagWeight tagWeight
).apply { ).apply {
@@ -274,7 +284,8 @@ class KeyboardDetailFragment : Fragment() {
private suspend fun setrestoreTheme(themeId: Int): ApiResponse<Unit>? { private suspend fun setrestoreTheme(themeId: Int): ApiResponse<Unit>? {
return try { return try {
RetrofitClient.apiService.restoreTheme(themeId) val restoreThemeRequest = restoreThemeRequest(themeId = themeId)
RetrofitClient.apiService.restoreTheme(restoreThemeRequest)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KeyboardDetailFragment", "恢复已删除的主题失败", e) Log.e("KeyboardDetailFragment", "恢复已删除的主题失败", e)
null null
@@ -294,7 +305,7 @@ class KeyboardDetailFragment : Fragment() {
val purchaseThemeRequest = purchaseThemeRequest(themeId = purchaseId) val purchaseThemeRequest = purchaseThemeRequest(themeId = purchaseId)
val response = RetrofitClient.apiService.purchaseTheme(purchaseThemeRequest) val response = RetrofitClient.apiService.purchaseTheme(purchaseThemeRequest)
// 购买成功后触发刷新(成功状态码为0 // 购买成功后触发刷新(成功状态码<EFBFBD>?<3F>?
if (response?.code == 0) { if (response?.code == 0) {
loadData() loadData()
} }
@@ -308,7 +319,7 @@ class KeyboardDetailFragment : Fragment() {
//=============================RecyclerView=================================== //=============================RecyclerView===================================
private fun setupRecyclerView() { private fun setupRecyclerView() {
// 设置GridLayoutManager每行显示2个item // 设置GridLayoutManager每行显<EFBFBD>?个item
val layoutManager = GridLayoutManager(requireContext(), 2) val layoutManager = GridLayoutManager(requireContext(), 2)
recyclerRecommendList.layoutManager = layoutManager recyclerRecommendList.layoutManager = layoutManager
@@ -325,7 +336,7 @@ class KeyboardDetailFragment : Fragment() {
val dialog = Dialog(requireContext()) val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.dialog_purchase_confirmation) dialog.setContentView(R.layout.dialog_purchase_confirmation)
// 设置弹窗属 // 设置弹窗属<EFBFBD>?
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
dialog.window?.setLayout( dialog.window?.setLayout(
android.view.WindowManager.LayoutParams.WRAP_CONTENT, android.view.WindowManager.LayoutParams.WRAP_CONTENT,
@@ -357,7 +368,7 @@ class KeyboardDetailFragment : Fragment() {
} }
/** /**
* URL 中提zip 包名(去掉路径和查询参数,去.zip 扩展名) * <EFBFBD>?URL 中提<EFBFBD>?zip 包名(去掉路径和查询参数,去<EFBFBD>?.zip 扩展名)
*/ */
private fun extractZipNameFromUrl(url: String): String { private fun extractZipNameFromUrl(url: String): String {
// 提取文件名部分(去掉路径和查询参数) // 提取文件名部分(去掉路径和查询参数)
@@ -367,7 +378,7 @@ class KeyboardDetailFragment : Fragment() {
url.substring(url.lastIndexOf('/') + 1) url.substring(url.lastIndexOf('/') + 1)
} }
// 去掉 .zip 扩展 // 去掉 .zip 扩展<EFBFBD>?
return if (fileName.endsWith(".zip")) { return if (fileName.endsWith(".zip")) {
fileName.substring(0, fileName.length - 4) fileName.substring(0, fileName.length - 4)
} else { } else {
@@ -425,7 +436,7 @@ class KeyboardDetailFragment : Fragment() {
enabledButton.isEnabled = false enabledButton.isEnabled = false
enabledButton.isClickable = false enabledButton.isClickable = false
enabledButton.isFocusable = false enabledButton.isFocusable = false
// 防止点击事件穿- 消耗所有触摸事 // 防止点击事件穿<EFBFBD>?- 消耗所有触摸事<EFBFBD>?
enabledButton.setOnTouchListener { _, _ -> true } enabledButton.setOnTouchListener { _, _ -> true }
// 添加视觉上的禁用效果 // 添加视觉上的禁用效果
enabledButton.alpha = 0.6f enabledButton.alpha = 0.6f
@@ -446,7 +457,7 @@ class KeyboardDetailFragment : Fragment() {
enabledButton.isFocusable = true enabledButton.isFocusable = true
// 移除触摸监听器,恢复正常触摸事件处理 // 移除触摸监听器,恢复正常触摸事件处理
enabledButton.setOnTouchListener(null) enabledButton.setOnTouchListener(null)
// 恢复正常的视觉效 // 恢复正常的视觉效<EFBFBD>?
enabledButton.alpha = 1.0f enabledButton.alpha = 1.0f
} }
@@ -464,3 +475,5 @@ class KeyboardDetailFragment : Fragment() {
Log.e("1314520-KeyboardDetailFragment", "Error: $message") Log.e("1314520-KeyboardDetailFragment", "Error: $message")
} }
} }

View File

@@ -4,6 +4,7 @@ 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.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -21,6 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent import com.example.myapplication.network.AuthEvent
@@ -48,6 +50,7 @@ class MineFragment : Fragment() {
private lateinit var avatar: CircleImageView private lateinit var avatar: CircleImageView
private lateinit var share: LinearLayout private lateinit var share: LinearLayout
private lateinit var loadingOverlay: LoadingOverlay private lateinit var loadingOverlay: LoadingOverlay
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private var loadUserJob: Job? = null private var loadUserJob: Job? = null
@@ -77,6 +80,15 @@ class MineFragment : Fragment() {
avatar = view.findViewById(R.id.avatar) avatar = view.findViewById(R.id.avatar)
share = view.findViewById(R.id.click_Share) share = view.findViewById(R.id.click_Share)
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator)) loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
swipeRefreshLayout.setColorSchemeColors(
Color.parseColor("#02BEAC"),
Color.parseColor("#1B1F1A"),
Color.parseColor("#9F9F9F")
)
swipeRefreshLayout.setOnRefreshListener {
refreshUser(force = true, showToast = true)
}
// 1) 先用本地缓存秒出首屏 // 1) 先用本地缓存秒出首屏
renderFromCache() renderFromCache()
@@ -178,6 +190,30 @@ class MineFragment : Fragment() {
) )
} }
view.findViewById<LinearLayout>(R.id.click_Email).setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val response = RetrofitClient.apiService.delUserCharacter()
val email = response.data
if (!isAdded) return@launch
if (email.isNullOrBlank()) {
Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
return@launch
}
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("email", email))
Toast.makeText(requireContext(), getString(R.string.email_copy_success), Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) return@launch
Log.e(TAG, "customerMail failed", e)
if (isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
} finally {
loadingOverlay.hide()
}
}
}
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠) // ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
@@ -231,6 +267,8 @@ class MineFragment : Fragment() {
cached?.avatarUrl?.let { url -> cached?.avatarUrl?.let { url ->
Glide.with(requireContext()) Glide.with(requireContext())
.load(url) .load(url)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar) .into(avatar)
} }
} }
@@ -242,6 +280,7 @@ class MineFragment : Fragment() {
*/ */
private fun refreshUser(force: Boolean, showToast: Boolean = false) { private fun refreshUser(force: Boolean, showToast: Boolean = false) {
if (!isLoggedIn()) { if (!isLoggedIn()) {
swipeRefreshLayout.isRefreshing = false
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.not_logged_in_toast), Toast.LENGTH_SHORT).show() if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.not_logged_in_toast), Toast.LENGTH_SHORT).show()
return return
} }
@@ -264,6 +303,8 @@ class MineFragment : Fragment() {
u?.avatarUrl?.let { url -> u?.avatarUrl?.let { url ->
Glide.with(requireContext()) Glide.with(requireContext())
.load(url) .load(url)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar) .into(avatar)
} }
@@ -274,6 +315,8 @@ class MineFragment : Fragment() {
if (e is kotlinx.coroutines.CancellationException) return@launch if (e is kotlinx.coroutines.CancellationException) return@launch
Log.e(TAG, "getUser failed", e) Log.e(TAG, "getUser failed", e)
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show() if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
} finally {
swipeRefreshLayout.isRefreshing = false
} }
} }
} }
@@ -294,6 +337,8 @@ class MineFragment : Fragment() {
renderVip(false, null) renderVip(false, null)
Glide.with(requireContext()) Glide.with(requireContext())
.load(R.drawable.default_avatar) .load(R.drawable.default_avatar)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar) .into(avatar)
// 触发登出事件让MainActivity打开登录页面 // 触发登出事件让MainActivity打开登录页面

View File

@@ -212,8 +212,8 @@ class PersonalSettings : BottomSheetDialogFragment() {
Glide.with(this) Glide.with(this)
.load(u.avatarUrl) .load(u.avatarUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar) .error(R.drawable.no_search_result)
.into(avatar) .into(avatar)
} }
@@ -287,6 +287,8 @@ class PersonalSettings : BottomSheetDialogFragment() {
private fun handleImageResult(uri: Uri) { private fun handleImageResult(uri: Uri) {
Glide.with(this) Glide.with(this)
.load(uri) .load(uri)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar) .into(avatar)
lifecycleScope.launch { lifecycleScope.launch {

View File

@@ -1,18 +1,29 @@
package com.example.myapplication.ui.recharge package com.example.myapplication.ui.recharge
import android.graphics.Paint import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle 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.TextView import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
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.google.android.material.appbar.AppBarLayout
import kotlin.math.abs
class RechargeFragment : Fragment() { class RechargeFragment : Fragment() {
private var currentTab = 0
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -24,15 +35,129 @@ class RechargeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// 找到旧价格 TextView // 收起输入法,避免从聊天页跳转过来时键盘残留
val tvOldPrice = view.findViewById<TextView>(R.id.tvOldPrice) val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
// 旧价格加删除线 imm.hideSoftInputFromWindow(view.windowToken, 0)
tvOldPrice.paintFlags = tvOldPrice.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
// 设置关闭按钮点击事件 val appBar = view.findViewById<AppBarLayout>(R.id.appBarLayout)
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener { val headerContainer = view.findViewById<LinearLayout>(R.id.headerContainer)
val stickyTitleBar = view.findViewById<FrameLayout>(R.id.stickyTitleBar)
val stickyTitle = view.findViewById<TextView>(R.id.stickyTitle)
val ivStickyClose = view.findViewById<ImageView>(R.id.iv_sticky_close)
val ivClose = view.findViewById<ImageView>(R.id.iv_close)
val viewPager = view.findViewById<ViewPager2>(R.id.viewPager)
val tabVip = view.findViewById<FrameLayout>(R.id.tabVip)
val tabSvip = view.findViewById<FrameLayout>(R.id.tabSvip)
val ivVipTab = view.findViewById<ImageView>(R.id.ivVipTab)
val ivSvipTab = view.findViewById<ImageView>(R.id.ivSvipTab)
val titleBarMaxHeight = resources.getDimensionPixelSize(R.dimen.sw_46dp)
// 显式计算 headerContainer 高度
// 原始布局流式位置bg(224) + close(-198+46) + vip(269) + equity(-198+391) + collapse(-380) + spacing(16) = 170dp
// LinearLayout.Math.max 阻止负 margin 缩减测量高度,因此必须在代码中显式设置
val headerHeight =
resources.getDimensionPixelSize(R.dimen.sw_224dp) +
resources.getDimensionPixelSize(R.dimen._sw_198dp) +
resources.getDimensionPixelSize(R.dimen.sw_46dp) +
resources.getDimensionPixelSize(R.dimen.sw_269dp) +
resources.getDimensionPixelSize(R.dimen._sw_198dp) +
resources.getDimensionPixelSize(R.dimen.sw_391dp) +
resources.getDimensionPixelSize(R.dimen._sw_380dp) +
resources.getDimensionPixelSize(R.dimen.sw_16dp)
headerContainer.layoutParams.height = headerHeight
// 关闭按钮(头部原位 + 标题栏共用同一逻辑)
val closeAction = View.OnClickListener {
AuthEventBus.emit(AuthEvent.UserUpdated) AuthEventBus.emit(AuthEvent.UserUpdated)
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
} }
ivClose.setOnClickListener(closeAction)
ivStickyClose.setOnClickListener(closeAction)
// ViewPager2 适配器
val pageLayouts = intArrayOf(R.layout.page_recharge_vip, R.layout.page_recharge_svip)
viewPager.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(pageLayouts[viewType], parent, false)
return object : RecyclerView.ViewHolder(itemView) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
override fun getItemViewType(position: Int) = position
override fun getItemCount() = 2
}
// 页面切换回调
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
switchTab(position, ivVipTab, ivSvipTab)
}
})
// Tab 点击
tabVip.setOnClickListener { viewPager.setCurrentItem(0, true) }
tabSvip.setOnClickListener { viewPager.setCurrentItem(1, true) }
// AppBarLayout 滚动监听:渐进展开标题栏(仿 Shop 页面)
appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val totalRange = appBarLayout.totalScrollRange
if (totalRange == 0) return@OnOffsetChangedListener
val ratio = abs(verticalOffset).toFloat() / totalRange
// 最后 20% 渐进展开标题栏
val progress = ((ratio - 0.8f) / 0.2f).coerceIn(0f, 1f)
// 标题栏高度 0 → titleBarMaxHeight防抖优化
val newHeight = (progress * titleBarMaxHeight).toInt()
val lp = stickyTitleBar.layoutParams
if (lp.height != newHeight) {
lp.height = newHeight
stickyTitleBar.layoutParams = lp
}
// 标题文字和关闭按钮渐显
stickyTitle.alpha = progress
ivStickyClose.alpha = progress
})
}
private fun switchTab(position: Int, ivVipTab: ImageView, ivSvipTab: ImageView) {
if (currentTab == position) return
currentTab = position
val selectedView = if (position == 0) ivVipTab else ivSvipTab
val unselectedView = if (position == 0) ivSvipTab else ivVipTab
AnimatorSet().apply {
playTogether(
ObjectAnimator.ofFloat(selectedView, "scaleX", 0.85f, 1f),
ObjectAnimator.ofFloat(selectedView, "scaleY", 0.85f, 1f),
ObjectAnimator.ofFloat(selectedView, "alpha", 0.7f, 1f),
ObjectAnimator.ofFloat(unselectedView, "scaleX", 1f, 0.85f),
ObjectAnimator.ofFloat(unselectedView, "scaleY", 1f, 0.85f),
ObjectAnimator.ofFloat(unselectedView, "alpha", 1f, 0.7f)
)
duration = 250
interpolator = AccelerateDecelerateInterpolator()
start()
}
if (position == 0) {
ivVipTab.setImageResource(R.drawable.vip_select)
ivSvipTab.setImageResource(R.drawable.svip_not_selected)
} else {
ivVipTab.setImageResource(R.drawable.vip_not_selected)
ivSvipTab.setImageResource(R.drawable.svip_select)
}
unselectedView.scaleX = 1f
unselectedView.scaleY = 1f
unselectedView.alpha = 1f
selectedView.scaleX = 1f
selectedView.scaleY = 1f
selectedView.alpha = 1f
} }
} }

View File

@@ -189,10 +189,11 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
themeListLoaded = false themeListLoaded = false
setNoResultVisible(false)
val newThemes = getThemeList()?.data ?: emptyList() val newThemes = getThemeList()?.data ?: emptyList()
if (!isAdded) return@launch if (!isAdded) return@launch
setNoResultVisible(false)
if (newThemes != tabTitles) { if (newThemes != tabTitles) {
tabTitles = newThemes tabTitles = newThemes
styleIds = tabTitles.map { it.id } styleIds = tabTitles.map { it.id }

View File

@@ -42,7 +42,8 @@ class ThemeCardAdapter : ListAdapter<themeStyle, ThemeCardAdapter.ThemeCardViewH
// 加载主题图片 // 加载主题图片
Glide.with(itemView.context) Glide.with(itemView.context)
.load(theme.themePreviewImageUrl) .load(theme.themePreviewImageUrl)
.placeholder(R.drawable.bg) .placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(themeImage) .into(themeImage)
// 设置主题名称 // 设置主题名称

View File

@@ -81,7 +81,8 @@ class MySkinAdapter(
Glide.with(holder.itemView) Glide.with(holder.itemView)
.load(item.themePreviewImageUrl) .load(item.themePreviewImageUrl)
.placeholder(R.drawable.default_avatar) .placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(holder.ivPreview) .into(holder.ivPreview)
val selected = selectedIds.contains(item.id) val selected = selectedIds.contains(item.id)

View File

@@ -15,6 +15,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.example.myapplication.network.FileDownloader import com.example.myapplication.network.FileDownloader
import com.example.myapplication.network.RetrofitClient import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.restoreThemeRequest
import com.example.myapplication.theme.ThemeManager import com.example.myapplication.theme.ThemeManager
import com.example.myapplication.utils.unzipThemeSmart import com.example.myapplication.utils.unzipThemeSmart
import java.io.File import java.io.File
@@ -68,7 +69,8 @@ class ThemeDownloadWorker(
} }
try { try {
val restoreResp = RetrofitClient.apiService.restoreTheme(themeId) val restoreReq = restoreThemeRequest(themeId = themeId)
val restoreResp = RetrofitClient.apiService.restoreTheme(restoreReq)
if (restoreResp.code != 0) { if (restoreResp.code != 0) {
Log.e(TAG, "restoreTheme failed: code=${restoreResp.code} msg=${restoreResp.message}") Log.e(TAG, "restoreTheme failed: code=${restoreResp.code} msg=${restoreResp.message}")
return Result.failure() return Result.failure()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#BA000000" />
<corners android:radius="@dimen/sw_11dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- 背景色 -->
<corners android:radius="@dimen/sw_13dp" /> <!-- 圆角半径,越大越圆 -->
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FEFEFE" />
<corners android:radius="@dimen/sw_8dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F6F7FB" />
<corners android:radius="@dimen/sw_13dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="@dimen/sw_14dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

View File

@@ -0,0 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#141414" /> <!-- 背景色 -->
<corners android:radius="@dimen/sw_10dp" /> <!-- 圆角半径,越大越圆 -->
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="@dimen/sw_233dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -7,16 +7,27 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F6F7FB" android:background="#F6F7FB"
tools:context=".ui.home.HomeFragment"> tools:context=".ui.home.HomeFragment">
<!-- 内容 -->
<androidx.core.widget.NestedScrollView <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:id="@+id/appBarLayout"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:background="#F6F7FB"
android:elevation="0dp"
android:stateListAnimator="@null"
android:clipChildren="false"
android:clipToPadding="false">
<!-- 可滚动头部区域:随滚动消失 -->
<LinearLayout
android:id="@+id/headerContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!-- recharge背景 --> <!-- recharge背景 -->
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,8 +35,10 @@
android:src="@drawable/recharge_bg" android:src="@drawable/recharge_bg"
android:scaleType="fitXY" android:scaleType="fitXY"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
<FrameLayout <FrameLayout
android:id="@+id/closeContainer"
android:layout_width="@dimen/sw_46dp" android:layout_width="@dimen/sw_46dp"
android:layout_height="@dimen/sw_46dp" android:layout_height="@dimen/sw_46dp"
android:layout_marginTop="@dimen/_sw_198dp"> android:layout_marginTop="@dimen/_sw_198dp">
@@ -38,237 +51,156 @@
android:rotation="180" android:rotation="180"
android:scaleType="fitCenter" /> android:scaleType="fitCenter" />
</FrameLayout> </FrameLayout>
<!-- vip --> <!-- vip -->
<ImageView <ImageView
android:id="@+id/iv_close"
android:layout_width="@dimen/sw_321dp" android:layout_width="@dimen/sw_321dp"
android:layout_height="@dimen/sw_269dp" android:layout_height="@dimen/sw_269dp"
android:layout_marginStart="@dimen/sw_24dp" android:layout_marginStart="@dimen/sw_24dp"
android:elevation="@dimen/sw_1dp" android:elevation="@dimen/sw_1dp"
android:src="@drawable/vip_two" /> android:src="@drawable/vip_two" />
<!-- 权益背景 --> <!-- 权益背景 -->
<ImageView <ImageView
android:id="@+id/iv_close"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/sw_391dp" android:layout_height="@dimen/sw_391dp"
android:elevation="@dimen/sw_2dp" android:elevation="@dimen/sw_2dp"
android:layout_marginTop="@dimen/_sw_198dp" android:layout_marginTop="@dimen/_sw_198dp"
android:src="@drawable/recharge_equity_bg" /> android:src="@drawable/recharge_equity_bg" />
</LinearLayout>
<!-- 吸顶区域:不设 scrollFlags → 自动吸顶 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/_sw_380dp" android:orientation="vertical"
android:gravity="center_horizontal"
android:background="#F6F7FB"
android:elevation="@dimen/sw_3dp" android:elevation="@dimen/sw_3dp"
android:padding="@dimen/sw_16dp" android:outlineProvider="none">
android:orientation="vertical">
<!-- 标题栏高度由代码控制0 → sw_46dp 渐进展开) -->
<FrameLayout
android:id="@+id/stickyTitleBar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipChildren="true"
android:background="#F6F7FB">
<FrameLayout
android:layout_width="@dimen/sw_46dp"
android:layout_height="@dimen/sw_46dp"
android:layout_gravity="start|center_vertical">
<ImageView
android:id="@+id/iv_sticky_close"
android:layout_width="@dimen/sw_26dp"
android:layout_height="@dimen/sw_26dp"
android:layout_gravity="center"
android:src="@drawable/recharge_close"
android:rotation="180"
android:alpha="0"
android:scaleType="fitCenter" />
</FrameLayout>
<TextView <TextView
android:id="@+id/member" android:id="@+id/stickyTitle"
android:layout_marginTop="@dimen/sw_28dp" android:layout_width="wrap_content"
android:layout_width="@dimen/sw_290dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/recharge_title"
android:textColor="#1B1F1A"
android:textSize="@dimen/sw_18sp" android:textSize="@dimen/sw_18sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#1B1F1A" android:alpha="0" />
</FrameLayout>
<!-- 切换按钮 -->
<FrameLayout
android:id="@+id/tabSwitchContainer"
android:layout_width="@dimen/sw_321dp"
android:layout_height="@dimen/sw_39dp"
android:layout_marginTop="@dimen/sw_12dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:text="Become A Member Of LOVE KEY" /> android:background="@drawable/recharge_tab_bg">
<TextView <!-- 滑动指示器 -->
android:id="@+id/Unlock" <View
android:layout_marginTop="@dimen/sw_3dp" android:id="@+id/tabIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/sw_14sp"
android:textColor="#1B1F1A"
android:layout_gravity="center_horizontal"
android:text="Unlock all functions" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_16dp">
<ImageView
android:id="@+id/imgLeft"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_113dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_wireless_sub_ai_dialogue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/imgRight"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_109dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_personalized_keyboard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_10dp">
<ImageView
android:id="@+id/imgLeft"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_122dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_chat_persona"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/imgRight"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_115dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_emotional_counseling"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/sw_16dp"
android:gravity="center_horizontal">
<!-- 卡片 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_75dp"
android:orientation="horizontal"
android:background="@drawable/recharge_card_bg"
android:gravity="center_vertical">
<!-- 左侧文字区域 -->
<LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_weight="1" android:layout_gravity="start|center_vertical" />
android:layout_marginStart="@dimen/sw_16dp"
android:orientation="vertical">
<!-- 标题Monthly Subscription -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Monthly Subscription"
android:textSize="@dimen/sw_14sp"
android:textColor="#1B1F1A" />
<!-- 价格区域:新价格 + 划线旧价格 -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal">
android:layout_marginTop="@dimen/sw_4dp">
<!-- 当前价格 -->
<TextView
android:id="@+id/tvPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$4.49"
android:textSize="@dimen/sw_20sp"
android:textStyle="bold"
android:textColor="#000000" />
<!-- 旧价格(划线) --> <!-- VIP 按钮 -->
<TextView <FrameLayout
android:id="@+id/tvOldPrice" android:id="@+id/tabVip"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginStart="@dimen/sw_8dp" android:layout_weight="1">
android:text="$4.49"
android:textSize="@dimen/sw_20sp"
android:textColor="#b3b3b3" />
</LinearLayout>
</LinearLayout>
<!-- 右侧选中 -->
<ImageView <ImageView
android:id="@+id/ivCheck" android:id="@+id/ivVipTab"
android:layout_width="@dimen/sw_24dp" android:layout_width="wrap_content"
android:layout_height="@dimen/sw_24dp" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/sw_16dp" android:layout_gravity="center"
android:src="@drawable/unchecked" android:src="@drawable/vip_select"
android:scaleType="centerInside" /> android:scaleType="centerInside" />
</FrameLayout>
<!-- SVIP 按钮 -->
<FrameLayout
android:id="@+id/tabSvip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/ivSvipTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/svip_not_selected"
android:scaleType="centerInside" />
</FrameLayout>
</LinearLayout> </LinearLayout>
<!-- ··························· --> </FrameLayout>
</LinearLayout> </LinearLayout>
<!-- 评论 --> </com.google.android.material.appbar.AppBarLayout>
<!-- 内容页面 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:clipToPadding="false"
android:paddingBottom="@dimen/sw_120dp" />
<!-- 底部固定区域:按钮 + 协议 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_gravity="bottom"
android:padding="@dimen/sw_16dp"
android:gravity="center_vertical">
<!-- 卡片 -->
<LinearLayout
android:layout_width="@dimen/sw_204dp"
android:layout_height="@dimen/sw_115dp"
android:background="@drawable/settings"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/sw_10dp"> android:gravity="center_horizontal"
<!-- 头像昵称 --> android:background="#F6F7FB"
<LinearLayout android:paddingTop="@dimen/sw_12dp"
android:layout_width="wrap_content" android:paddingBottom="@dimen/sw_16dp"
android:layout_height="wrap_content" android:elevation="@dimen/sw_4dp">
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="@dimen/sw_24dp"
android:layout_height="@dimen/sw_24dp"
android:src="@drawable/default_avatar" />
<TextView
android:id="@+id/tvNickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_6dp"
android:text="Nickname"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/sw_14sp"
android:textColor="#1B1F1A" />
</LinearLayout>
<!-- 五星好评 -->
<ImageView
android:id="@+id/ivStar"
android:layout_width="@dimen/sw_77dp"
android:layout_height="@dimen/sw_10dp"
android:layout_marginTop="@dimen/sw_8dp"
android:src="@drawable/five_star_review"
android:scaleType="fitXY" />
<!-- 评论内容 -->
<TextView
android:id="@+id/tvComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_6dp"
android:text="I highly recommend this APP. It taught me how to chat"
android:ellipsize="end"
android:maxLines="2"
android:textSize="@dimen/sw_10sp"
android:textColor="#1B1F1A" />
</LinearLayout>
</LinearLayout>
<!-- 按钮 --> <!-- 按钮 -->
<LinearLayout <LinearLayout
android:id="@+id/rechargeButton" android:id="@+id/rechargeButton"
android:layout_marginTop="@dimen/sw_24dp"
android:layout_width="@dimen/sw_343dp" android:layout_width="@dimen/sw_343dp"
android:layout_height="@dimen/sw_51dp" android:layout_height="@dimen/sw_51dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:gravity="center" android:gravity="center"
android:background="@drawable/button_bg" android:background="@drawable/recharge_now_bg"
android:elevation="@dimen/sw_4dp"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@@ -277,8 +209,8 @@
android:textSize="@dimen/sw_15sp" android:textSize="@dimen/sw_15sp"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" android:gravity="center"
android:textColor="#1B1F1A" android:textColor="#FFFFFF"
android:text="Recharge now" /> android:text="@string/recharge_btn" />
</LinearLayout> </LinearLayout>
<!-- 协议 --> <!-- 协议 -->
@@ -287,7 +219,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_17dp" android:layout_marginTop="@dimen/sw_17dp"
android:textSize="@dimen/sw_10sp" android:textSize="@dimen/sw_10sp"
android:text="By clicking &quot;pay&quot;, you indicate your agreement to the" android:text="@string/recharge_pay_agreement"
android:textColor="#1B1F1A" android:textColor="#1B1F1A"
android:gravity="center_horizontal"/> android:gravity="center_horizontal"/>
@@ -296,10 +228,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_4dp" android:layout_marginTop="@dimen/sw_4dp"
android:textSize="@dimen/sw_10sp" android:textSize="@dimen/sw_10sp"
android:text="《Embership Agreement" android:text="@string/recharge_membership_agreement"
android:textColor="#02BEAC" android:textColor="#02BEAC"
android:gravity="center_horizontal" /> android:gravity="center_horizontal" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="@dimen/sw_252dp"
android:layout_height="@dimen/sw_251dp"
android:background="@drawable/bg_chat_error_dialog">
<TextView
android:id="@+id/hintText"
android:layout_width="@dimen/sw_200dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="@dimen/sw_16dp"
android:paddingEnd="@dimen/sw_16dp"
android:text=""
android:textColor="#000000"
android:textSize="12sp"
android:gravity="center"/>
</FrameLayout>
<TextView
android:id="@+id/btnRecharge"
android:layout_width="@dimen/sw_210dp"
android:layout_height="@dimen/sw_53dp"
android:layout_marginTop="@dimen/sw_27dp"
android:text="Go to recharge"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="bold"
android:gravity="center"
android:background="@drawable/bg_chat_error_dialog_button"/>
<ImageView
android:id="@+id/btnClose"
android:layout_width="@dimen/sw_48dp"
android:layout_height="@dimen/sw_48dp"
android:layout_marginTop="@dimen/sw_6dp"
android:padding="@dimen/sw_10dp"
android:src="@drawable/bg_chat_error_dialog_btnclose_button"/>
</LinearLayout>

View File

@@ -244,12 +244,16 @@
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:elevation="@dimen/sw_10dp"> android:elevation="@dimen/sw_10dp">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/noResultSwipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal"> android:gravity="center">
<ImageView <ImageView
android:id="@+id/noResultImage" android:id="@+id/noResultImage"
@@ -271,6 +275,8 @@
android:textColor="#1B1F1A" android:textColor="#1B1F1A"
android:includeFontPadding="false" /> android:includeFontPadding="false" />
</LinearLayout> </LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout> </FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 抽屉 --> <!-- 抽屉 -->

View File

@@ -1,8 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/rootCoordinator" android:id="@+id/rootCoordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -248,7 +253,7 @@
<LinearLayout <LinearLayout
android:id="@+id/topTabs" android:id="@+id/topTabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/sw_40dp" android:layout_height="@dimen/sw_56dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="@dimen/sw_16dp" android:paddingStart="@dimen/sw_16dp"
@@ -376,3 +381,5 @@
android:scaleType="centerInside" /> android:scaleType="centerInside" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -14,6 +14,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F6F7FB"/> android:background="#F6F7FB"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -144,6 +149,8 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_11dp"
android:paddingEnd="@dimen/sw_11dp"
android:layout_marginTop="@dimen/sw_20dp"> android:layout_marginTop="@dimen/sw_20dp">
<ImageView <ImageView
android:id="@+id/imgLeft" android:id="@+id/imgLeft"
@@ -383,6 +390,7 @@
<!-- 电子邮件 --> <!-- 电子邮件 -->
<LinearLayout <LinearLayout
android:id="@+id/click_Email"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp" android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical" android:gravity="center_vertical"
@@ -520,4 +528,6 @@
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/sw_12dp">
<ImageView
android:id="@+id/loadingIcon"
android:layout_width="@dimen/sw_24dp"
android:layout_height="@dimen/sw_24dp"
android:layout_gravity="center"
android:src="@drawable/chat_history_loading"
android:importantForAccessibility="no" />
</FrameLayout>

View File

@@ -11,8 +11,7 @@
<de.hdodenhof.circleimageview.CircleImageView <de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivMenuAvatar" android:id="@+id/ivMenuAvatar"
android:layout_width="@dimen/sw_40dp" android:layout_width="@dimen/sw_40dp"
android:layout_height="@dimen/sw_40dp" android:layout_height="@dimen/sw_40dp"/>
android:src="@drawable/a123123123" />
<!-- 昵称和描述 --> <!-- 昵称和描述 -->
<LinearLayout <LinearLayout

View File

@@ -54,7 +54,7 @@
android:layout_height="@dimen/sw_264dp" android:layout_height="@dimen/sw_264dp"
android:layout_marginTop="@dimen/sw_10dp" android:layout_marginTop="@dimen/sw_10dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/bg" android:src="@drawable/component_loading"
app:shapeAppearanceOverlay="@style/RoundedImageStyle" /> app:shapeAppearanceOverlay="@style/RoundedImageStyle" />
<!-- 键盘名称和下载量 --> <!-- 键盘名称和下载量 -->

View File

@@ -125,9 +125,11 @@
android:id="@+id/noResultText" android:id="@+id/noResultText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_50dp"
android:paddingEnd="@dimen/sw_50dp"
android:gravity="center" android:gravity="center"
android:textAlignment="center" android:textAlignment="center"
android:text="@string/search_not_data" android:text="@string/skin_select_none"
android:textSize="@dimen/sw_13sp" android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A" android:textColor="#1B1F1A"
android:includeFontPadding="false" /> android:includeFontPadding="false" />

View File

@@ -0,0 +1,448 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
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:layout_marginTop="@dimen/sw_16dp"
android:padding="@dimen/sw_16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_recharge_svip"
android:padding="@dimen/sw_6dp"
android:orientation="vertical">
<!-- 支付 -->
<LinearLayout
android:layout_width="match_parent"
androidLlayout_marginTop="@dimen/sw_10dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 卡片 -->
<LinearLayout
android:layout_width="@dimen/sw_105dp"
android:layout_height="@dimen/sw_124dp"
android:background="@drawable/bg_recharge_svip_card"
android:layout_gravity="center_horizontal|center_vertical"
android:orientation="vertical">
<!-- 卡片标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/recharge_svip_1_week"
android:textColor="#3D3D3D"
android:textSize="@dimen/sw_13sp"
android:gravity="center"
android:paddingBottom="@dimen/sw_6dp"
android:paddingTop="@dimen/sw_8dp"
android:textStyle="bold" />
<!-- 卡片价格 -->
<LinearLayout
android:layout_width="@dimen/sw_95dp"
android:layout_height="@dimen/sw_86dp"
android:background="@drawable/bg_recharge_svip_card_price"
android:layout_gravity="center_horizontal|center_vertical"
android:gravity="center"
android:orientation="horizontal">
<!-- 美元符 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$ "
android:textColor="#797979"
android:gravity="center"
android:textSize="@dimen/sw_10sp"/>
<!-- 价格 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100"
android:gravity="center"
android:textColor="#797979"
android:textSize="@dimen/sw_22sp"/>
</LinearLayout>
</LinearLayout>
<!-- `````` -->
</LinearLayout>
<!-- 权益 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_20dp"
android:background="@drawable/bg_recharge_svip_benefits"
android:padding="@dimen/sw_8dp"
android:orientation="vertical">
<!-- 权益标题 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/sw_6dp"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/sw_52dp"
android:layout_height="@dimen/sw_1dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_benefit_1" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/sw_17dp"
android:layout_marginStart="@dimen/sw_17dp">
<ImageView
android:layout_width="@dimen/sw_135dp"
android:layout_height="@dimen/sw_7dp"
android:layout_gravity="bottom|center_horizontal"
android:src="@drawable/ic_recharge_svip_benefit_1_icon" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recharge_svip_membership_benefits"
android:textColor="#3D3D3D"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:gravity="center"
android:textStyle="bold" />
</FrameLayout>
<ImageView
android:layout_width="@dimen/sw_52dp"
android:layout_height="@dimen/sw_1dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_benefit_2" />
</LinearLayout>
<!-- 权益描述1 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_50dp"
android:layout_marginTop="@dimen/sw_20dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/home_ai_dialogue"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_34dp"
android:layout_height="@dimen/sw_36dp"
android:layout_marginStart="@dimen/sw_13dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_1" />
</FrameLayout>
<!-- 权益描述2 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_49dp"
android:layout_marginTop="@dimen/sw_11dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/home_ai_keyboard"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_41dp"
android:layout_height="@dimen/sw_37dp"
android:layout_marginStart="@dimen/sw_10dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_2" />
</FrameLayout>
<!-- 权益描述3 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_51dp"
android:layout_marginTop="@dimen/sw_9dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/home_ai_persona"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_26dp"
android:layout_height="@dimen/sw_39dp"
android:layout_marginStart="@dimen/sw_17dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_3" />
</FrameLayout>
<!-- 权益描述4 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_52dp"
android:layout_marginTop="@dimen/sw_8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/home_ai_counseling"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_35dp"
android:layout_height="@dimen/sw_38dp"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_4" />
</FrameLayout>
<!-- 权益描述5 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_52dp"
android:layout_marginTop="@dimen/sw_8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/recharge_svip_longer_chat_history"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_36dp"
android:layout_height="@dimen/sw_37dp"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_5" />
</FrameLayout>
<!-- 权益描述6 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_51dp"
android:layout_marginTop="@dimen/sw_9dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/recharge_svip_unlimited_chatting"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_38dp"
android:layout_height="@dimen/sw_35dp"
android:layout_marginStart="@dimen/sw_12dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_6" />
</FrameLayout>
<!-- 权益描述7 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_51dp"
android:layout_marginTop="@dimen/sw_9dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/recharge_svip_no_speed_limits"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_51dp"
android:layout_height="@dimen/sw_32dp"
android:layout_marginStart="@dimen/sw_3dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_7" />
</FrameLayout>
<!-- 权益描述8 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_54dp"
android:layout_marginTop="@dimen/sw_6dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_43dp"
android:layout_gravity="bottom"
android:paddingStart="@dimen/sw_65dp"
android:background="@drawable/bg_recharge_svip_benefit_desc"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/recharge_svip_coming_soon"
android:textColor="#898B8C"
android:textSize="@dimen/sw_13sp"
android:layout_gravity="center_horizontal|center_vertical"
android:textStyle="bold" />
<ImageView
android:layout_width="@dimen/sw_13dp"
android:layout_height="@dimen/sw_9dp"
android:layout_marginEnd="@dimen/sw_20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_recharge_svip_hook" />
</LinearLayout>
<ImageView
android:layout_width="@dimen/sw_44dp"
android:layout_height="@dimen/sw_34dp"
android:layout_marginStart="@dimen/sw_7dp"
android:layout_gravity="top"
android:src="@drawable/ic_recharge_svip_benefit_1_desc_8" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:padding="@dimen/sw_16dp"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_11dp"
android:paddingEnd="@dimen/sw_11dp"
android:layout_marginTop="@dimen/sw_16dp">
<ImageView
android:id="@+id/imgLeft"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_113dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_wireless_sub_ai_dialogue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/imgRight"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_109dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_personalized_keyboard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_11dp"
android:paddingEnd="@dimen/sw_11dp"
android:layout_marginTop="@dimen/sw_10dp">
<ImageView
android:id="@+id/imgLeft2"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_122dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_chat_persona"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/imgRight2"
android:layout_width="@dimen/sw_150dp"
android:layout_height="@dimen/sw_115dp"
android:scaleType="fitXY"
android:src="@drawable/recharge_emotional_counseling"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/sw_18dp"
android:gravity="center_horizontal">
<!-- 卡片 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/sw_75dp"
android:orientation="horizontal"
android:background="@drawable/recharge_card_bg"
android:gravity="center_vertical">
<!-- 左侧文字区域 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="@dimen/sw_16dp"
android:orientation="vertical">
<!-- 标题Monthly Subscription -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recharge_monthly_subscription"
android:textSize="@dimen/sw_14sp"
android:textColor="#1B1F1A" />
<!-- 价格区域:新价格 + 划线旧价格 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/sw_4dp">
<!-- 当前价格 -->
<TextView
android:id="@+id/tvPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$4.49"
android:textSize="@dimen/sw_20sp"
android:textStyle="bold"
android:textColor="#000000" />
</LinearLayout>
</LinearLayout>
<!-- 右侧选中 -->
<ImageView
android:id="@+id/ivCheck"
android:layout_width="@dimen/sw_24dp"
android:layout_height="@dimen/sw_24dp"
android:layout_marginEnd="@dimen/sw_16dp"
android:src="@drawable/unchecked"
android:scaleType="centerInside" />
</LinearLayout>
<!-- ··························· -->
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/sw_193dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_message_popup"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/menuCopy"
android:layout_width="@dimen/sw_193dp"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_13dp"
android:paddingTop="@dimen/sw_12dp"
android:paddingEnd="@dimen/sw_12dp"
android:paddingBottom="@dimen/sw_12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:text="@string/chat_menu_copy"
android:textColor="#FFFFFF"
android:textSize="@dimen/sw_13sp" />
<ImageView
android:layout_width="@dimen/sw_14dp"
android:layout_height="@dimen/sw_14dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@string/chat_menu_copy"
android:src="@drawable/chat_copy" />
</RelativeLayout>
<View
android:id="@+id/divider1"
android:layout_width="@dimen/sw_193dp"
android:layout_height="@dimen/sw_1dp"
android:background="#1FFFFFFF" />
<RelativeLayout
android:id="@+id/menuDelete"
android:layout_width="@dimen/sw_193dp"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_13dp"
android:paddingTop="@dimen/sw_12dp"
android:paddingEnd="@dimen/sw_12dp"
android:paddingBottom="@dimen/sw_12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:text="@string/delete"
android:textColor="#FFFFFF"
android:textSize="@dimen/sw_13sp" />
<ImageView
android:layout_width="@dimen/sw_14dp"
android:layout_height="@dimen/sw_14dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@string/delete"
android:src="@drawable/chat_delete" />
</RelativeLayout>
<View
android:id="@+id/divider2"
android:layout_width="@dimen/sw_193dp"
android:layout_height="@dimen/sw_1dp"
android:background="#1FFFFFFF" />
<RelativeLayout
android:id="@+id/menuReport"
android:layout_width="@dimen/sw_193dp"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_13dp"
android:paddingTop="@dimen/sw_12dp"
android:paddingEnd="@dimen/sw_12dp"
android:paddingBottom="@dimen/sw_12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:text="@string/chat_menu_report"
android:textColor="#FFFFFF"
android:textSize="@dimen/sw_13sp" />
<ImageView
android:layout_width="@dimen/sw_14dp"
android:layout_height="@dimen/sw_14dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@string/chat_menu_report"
android:src="@drawable/chat_report" />
</RelativeLayout>
</LinearLayout>

View File

@@ -109,6 +109,8 @@
<string name="skin_editor">编辑</string> <string name="skin_editor">编辑</string>
<string name="skin_exit_editing">退出编辑</string> <string name="skin_exit_editing">退出编辑</string>
<string name="skin_select_all">个被选中</string> <string name="skin_select_all">个被选中</string>
<string name="skin_select_none">您目前还没有任何皮肤,快去下载吧</string>
<!-- 搜索 --> <!-- 搜索 -->
<string name="search_hint">请输入你要搜索的内容</string> <string name="search_hint">请输入你要搜索的内容</string>
@@ -218,6 +220,10 @@
<string name="gender_third">第三性别</string> <string name="gender_third">第三性别</string>
<string name="skip">跳过</string> <string name="skip">跳过</string>
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="chat_menu_copy">复制</string>
<string name="chat_menu_report">举报</string>
<string name="chat_copy_success">已复制</string>
<string name="email_copy_success">邮箱已复制</string>
<string name="next">下一步</string> <string name="next">下一步</string>
<string name="currently_inputting">对方正在输入...</string> <string name="currently_inputting">对方正在输入...</string>
@@ -249,5 +255,19 @@
<string name="Pop_up_window_SearchFragment_1">请输入搜索关键词。</string><!-- SearchFragment --><!-- SearchResultFragment --> <string name="Pop_up_window_SearchFragment_1">请输入搜索关键词。</string><!-- SearchFragment --><!-- SearchResultFragment -->
<string name="Pop_up_window_ThemeDownloadWorker_1">皮肤应用成功</string><!-- ThemeDownloadWorker --> <string name="Pop_up_window_ThemeDownloadWorker_1">皮肤应用成功</string><!-- ThemeDownloadWorker -->
<!-- 充值 -->
<string name="recharge_title">会员充值</string>
<string name="recharge_btn">立即充值</string>
<string name="recharge_pay_agreement">点击\"支付\"即表示您同意</string>
<string name="recharge_membership_agreement">《会员协议》</string>
<string name="recharge_monthly_subscription">月度订阅</string>
<!-- SVIP充值 -->
<string name="recharge_svip_1_week">1周</string>
<string name="recharge_svip_membership_benefits">会员权益</string>
<string name="recharge_svip_longer_chat_history">更长的聊天记录</string>
<string name="recharge_svip_unlimited_chatting">无限畅聊</string>
<string name="recharge_svip_no_speed_limits">聊天不限速</string>
<string name="recharge_svip_coming_soon">敬请期待</string>
</resources> </resources>

View File

@@ -113,6 +113,7 @@
<string name="skin_editor">Editor</string> <string name="skin_editor">Editor</string>
<string name="skin_exit_editing">Exit editing</string> <string name="skin_exit_editing">Exit editing</string>
<string name="skin_select_all">items selected</string> <string name="skin_select_all">items selected</string>
<string name="skin_select_none">You currently don\'t have any skin. Hurry up and download it!</string>
<!-- 搜索 --> <!-- 搜索 -->
<string name="search_hint">Please enter the content you want to search for.</string> <string name="search_hint">Please enter the content you want to search for.</string>
@@ -223,6 +224,10 @@
<string name="gender_third">The third gender</string> <string name="gender_third">The third gender</string>
<string name="skip">Skip</string> <string name="skip">Skip</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="chat_menu_copy">Copy</string>
<string name="chat_menu_report">Report</string>
<string name="chat_copy_success">Copied</string>
<string name="email_copy_success">Email copied</string>
<string name="next">Next step</string> <string name="next">Next step</string>
<string name="currently_inputting">The other party is currently inputting...</string> <string name="currently_inputting">The other party is currently inputting...</string>
@@ -254,4 +259,20 @@
<string name="Pop_up_window_SearchFragment_1">Please enter the search term.</string><!-- SearchFragment --><!-- SearchResultFragment --> <string name="Pop_up_window_SearchFragment_1">Please enter the search term.</string><!-- SearchFragment --><!-- SearchResultFragment -->
<string name="Pop_up_window_ThemeDownloadWorker_1">Skin application was successful.</string><!-- ThemeDownloadWorker --> <string name="Pop_up_window_ThemeDownloadWorker_1">Skin application was successful.</string><!-- ThemeDownloadWorker -->
<!-- 充值 -->
<string name="recharge_title">Member recharge</string>
<string name="recharge_btn">Recharge now</string>
<string name="recharge_pay_agreement">By clicking \"pay\", you indicate your agreement to the</string>
<string name="recharge_membership_agreement">《Membership Agreement》</string>
<string name="recharge_monthly_subscription">Monthly Subscription</string>
<!-- SVIP充值 -->
<string name="recharge_svip_1_week">1 Week</string>
<string name="recharge_svip_membership_benefits">Membership Benefits</string>
<string name="recharge_svip_longer_chat_history">Longer chat history</string>
<string name="recharge_svip_unlimited_chatting">Unlimited chatting</string>
<string name="recharge_svip_no_speed_limits">Chat without speed limits</string>
<string name="recharge_svip_coming_soon">Coming soon</string>
</resources> </resources>