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(powershell:*)",
"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.KeyEvent
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.text.TextWatcher
import android.text.Editable
import kotlin.math.max
import com.example.myapplication.network.BehaviorReporter
class GuideActivity : AppCompatActivity() {
@@ -36,6 +40,8 @@ class GuideActivity : AppCompatActivity() {
private lateinit var bottomPanel: LinearLayout
private lateinit var hintLayout: LinearLayout
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 {
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 {
val r = Rect()
// 获取窗口可见区域
rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = rootView.rootView.height
val visibleBottom = r.bottom
val keyboardHeight = screenHeight - visibleBottom
// 这个阈值防止“状态栏/导航栏变化”被误认为键盘
val isKeyboardVisible = keyboardHeight > screenHeight * 0.15
if (isKeyboardVisible) {
// 键盘高度为正,把 bottomPanel 抬上去,但不要抬太高
// 只上移键盘高度减去底部面板高度,让输入框刚好在键盘上方
val adjustedTranslation = -(keyboardHeight - bottomPanel.height)
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 // 保持底部有一点空隙也可以按你需求调
)
}
val heightDiff = (screenHeight - r.bottom).coerceAtLeast(0)
val threshold = (screenHeight * 0.15f).toInt()
val heightIme = if (heightDiff > threshold) heightDiff else 0
val rootInsets = ViewCompat.getRootWindowInsets(rootView)
val insetIme = rootInsets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
val imeBottom = max(heightIme, insetIme)
val systemBottom = rootInsets
?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom
?: lastSystemBottom
applyImeInsets(imeBottom, systemBottom)
}
// 键盘发送
@@ -293,4 +300,33 @@ class GuideActivity : AppCompatActivity() {
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.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.ConnectivityManager
import android.net.Network
@@ -10,8 +11,10 @@ import android.net.NetworkRequest
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
@@ -261,6 +264,14 @@ class MainActivity : AppCompatActivity() {
is AuthEvent.CharacterAdded -> {
// 不需要处理由HomeFragment处理
}
is AuthEvent.ShowChatErrorRecharge -> {
showChatErrorRechargeDialog(event.errorMessage)
}
is AuthEvent.KeyboardChatUpdated -> {
// 由 CircleFragment 自行处理
}
}
}
}
@@ -287,10 +298,25 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// 处理来自键盘的跳转请求
handleRechargeIntent(intent)
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener也能恢复底栏
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() {
// ✅ 防泄漏移除路由监听Activity 销毁时)
runCatching {
@@ -584,6 +610,29 @@ class MainActivity : AppCompatActivity() {
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() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {

View File

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

View File

@@ -1,12 +1,16 @@
package com.example.myapplication.keyboard
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.chatMessageRequest
import com.example.myapplication.ui.circle.ChatMessage
@@ -114,20 +118,15 @@ class AiRolePanelController(private val context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "chatMessage failed: ${e.message}", e)
placeholder.text = context.getString(R.string.refresh_failed)
placeholder.isLoading = false
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
removePlaceholder(placeholder)
return@launch
}
val data = response.data
if (data == null) {
Log.e(TAG, "chatMessage no data, code=${response.code}")
placeholder.text = context.getString(R.string.refresh_failed)
placeholder.isLoading = false
placeholder.hasAnimated = true
adapter?.notifyMessageUpdated(placeholder.id)
removePlaceholder(placeholder)
showChatErrorRechargeDialog(response.message)
return@launch
}
@@ -138,6 +137,11 @@ class AiRolePanelController(private val context: Context) {
adapter?.notifyMessageUpdated(placeholder.id)
scrollToBottom()
// 通知 CircleFragment 刷新对应角色的聊天记录
AuthEventBus.emit(AuthEvent.KeyboardChatUpdated(currentCompanionId))
// 持久化脏标记,确保应用从后台恢复时也能刷新
AiRolePreferences.markCompanionDirty(context, currentCompanionId)
// 轮询音频 URL
val audioUrl = fetchAudioUrl(data.audioId)
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++
fun clearMessages() {

View File

@@ -11,6 +11,7 @@ object AiRolePreferences {
private const val KEY_COMPANION_ID = "companion_id"
private const val KEY_PERSONA_NAME = "persona_name"
private const val KEY_AVATAR_URL = "avatar_url"
private const val KEY_DIRTY_COMPANIONS = "dirty_companion_ids"
data class CompanionInfo(
val companionId: Int,
@@ -49,4 +50,26 @@ object AiRolePreferences {
.clear()
.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")
suspend fun inviteCode(
): ApiResponse<ShareResponse>
// 获取客服邮箱
@GET("user/customerMail")
suspend fun delUserCharacter(
): ApiResponse<String>
//===========================================首页=================================
// 标签列表
@GET("tag/list")
@@ -197,7 +202,7 @@ interface ApiService {
//恢复已删除的主题
@POST("themes/restore")
suspend fun restoreTheme(
@Query("themeId") themeId: Int
@Body body: restoreThemeRequest
): ApiResponse<Unit>
// =========================================圈子ai陪聊============================================
// 分页查询AI陪聊角色
@@ -285,6 +290,12 @@ interface ApiService {
@Body body: chatSessionResetRequest
): ApiResponse<chatSessionResetResponse>
//删除聊天记录
@POST("chat/history/delete")
suspend fun chatDelete(
@Body body: chatDeleteRequest
): ApiResponse<Boolean>

View File

@@ -36,4 +36,6 @@ sealed class AuthEvent {
) : AuthEvent()
object UserUpdated : 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"
// TODO改成你的行为服务 baseUrl必须以 / 结尾)
// TODO改成你的行为服务 baseUrl必须以 / 结尾)(上报接口)
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 扁平化规则:
* object: a.b.c
* array : items[0].id
* JSON 扁平化规则(与 iOS KBSignUtils 对齐)
* 仅展开顶层 object 的 key-value
* 数组和嵌套对象直接转为 JSON 字符串作为 value不递归展开
*/
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
when {
elem.isJsonNull -> {
// null 不参与签名(服务端也要一致)
// null 不参与签名
}
elem.isJsonPrimitive -> {
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
for ((k, v) in obj.entrySet()) {
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
flattenJson(v, newKey, out)
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)
}
}
}
elem.isJsonArray -> {
val arr = elem.asJsonArray
for (i in 0 until arr.size()) {
val newKey = "$prefix[$i]"
flattenJson(arr[i], newKey, out)
// 顶层数组场景(极少见),直接转 JSON 字符串
if (prefix.isNotBlank()) {
val jsonStr = Gson().toJson(elem)
if (jsonStr.isNotBlank()) out[prefix] = jsonStr
}
}
}

View File

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

View File

@@ -19,7 +19,8 @@ import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
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"
// ====== 按你给的规则固定值 ======

View File

@@ -10,7 +10,8 @@ import com.example.myapplication.network.FileUploadService
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
@Volatile

View File

@@ -5,6 +5,7 @@ import android.media.MediaPlayer
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
@@ -13,9 +14,20 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHolder>() {
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
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 playingMessageId: Long? = null
@@ -25,11 +37,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
setHasStableIds(true)
}
private fun toDataIndex(adapterPosition: Int): Int =
if (showLoading) adapterPosition - 1 else adapterPosition
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) {
R.layout.item_chat_message_me
} else {
@@ -39,18 +61,31 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
return MessageViewHolder(view)
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
holder.bind(items[position])
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is LoadingViewHolder) {
holder.bind()
return
}
(holder as MessageViewHolder).bind(items[toDataIndex(position)])
}
override fun onViewRecycled(holder: MessageViewHolder) {
holder.onRecycled()
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
when (holder) {
is MessageViewHolder -> holder.onRecycled()
is LoadingViewHolder -> holder.onRecycled()
}
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>) {
items = messages
@@ -60,14 +95,20 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
fun notifyLastInserted() {
val index = items.size - 1
if (index >= 0) {
notifyItemInserted(index)
notifyItemInserted(toAdapterIndex(index))
}
}
fun notifyMessageUpdated(messageId: Long) {
val index = items.indexOfFirst { it.id == messageId }
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)
private val textLoadingAnimation =
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) {
if (boundMessageId != message.id) {
@@ -280,6 +343,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
private companion object {
const val VIEW_TYPE_ME = 1
const val VIEW_TYPE_BOT = 2
const val VIEW_TYPE_LOADING = 3
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)?,
onLikeClick: ((position: Int, companionId: 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
messageAdapter.onMessageLongClick = onMessageLongClick
hasMoreHistory = historyState.hasMore
isLoadingHistory = historyState.isLoading
this.historyStateProvider = historyStateProvider
@@ -114,10 +116,14 @@ class ChatPageViewHolder(
boundCommentCount = data.commentCount
Glide.with(backgroundView.context)
.load(data.backgroundColor)
.placeholder(R.drawable.component_loading)
.error(R.drawable.circle_not_data_bg)
.into(backgroundView)
Glide.with(avatarView.context)
.load(data.avatarUrl)
.placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar)
.into(avatarView)
likeView.setImageResource(
@@ -203,12 +209,15 @@ class ChatPageViewHolder(
val requestedCompanionId = boundCompanionId
val requestedPageId = boundPageId
isLoadingHistory = true
messageAdapter.showLoading = true
callback(position, requestedCompanionId) { result ->
if (requestedCompanionId != boundCompanionId || requestedPageId != boundPageId) {
messageAdapter.showLoading = false
return@callback
}
isLoadingHistory = false
hasMoreHistory = result.hasMore
messageAdapter.showLoading = false
if (result.insertedCount > 0) {
notifyMessagesPrepended(result.insertedCount)
}
@@ -238,7 +247,12 @@ class ChatPageViewHolder(
messageAdapter.notifyMessageUpdated(messageId)
}
fun notifyMessageRemoved(position: Int) {
messageAdapter.notifyMessageRemoved(position)
}
override fun onRecycled() {
messageAdapter.showLoading = false
messageAdapter.release()
chatRv.stopScroll()
}

View File

@@ -48,13 +48,17 @@ class CircleChatRepository(
private var knownTotalPages: Int? = null
@Volatile
private var availablePages: Int = totalPages
@Volatile
private var hasLoadFailure = false
var onTotalPagesChanged: ((Int) -> Unit)? = null
@Volatile
var onPageLoaded: ((position: Int) -> Unit)? = null
// 后台协程用于预加载。
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val messageId = AtomicLong((totalPages.toLong() + 1) * 1_000_000L)
//获取指定位置的聊天页面数据
//获取指定位置的聊天页面数据(非阻塞,缓存未命中时触发后台加载)
fun getPage(position: Int): ChatPageData {
if (position < 0 || position >= availablePages) {
return emptyPage(position)
@@ -62,18 +66,9 @@ class CircleChatRepository(
val cached = synchronized(lock) { cache.get(position) }
if (cached != null) return cached
val page = createPage(position)
return synchronized(lock) {
val existing = cache.get(position)
if (existing != null) {
inFlight.remove(position)
existing
} else {
cache.put(position, page)
inFlight.remove(position)
page
}
}
// 缓存未命中:触发后台预加载,立即返回空占位页避免阻塞调用线程
preloadAround(position)
return emptyPage(position)
}
//主要功能是根据用户当前查看的页面位置,预加载其前后若干页的数据,并将这些数据放入缓存中
@@ -88,6 +83,7 @@ class CircleChatRepository(
// 清除加载失败的缓存页面companionId <= 0使后续 preloadAround 能重新加载
fun invalidateFailedPages() {
synchronized(lock) {
hasLoadFailure = false
val snapshot = cache.snapshot()
for ((position, page) in snapshot) {
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 {
if (hasLoadFailure) return true
synchronized(lock) {
for ((_, page) in cache.snapshot()) {
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 {
synchronized(lock) {
val page = cache.get(position)
@@ -307,6 +331,39 @@ class CircleChatRepository(
// 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() {
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) {
val maxPages = availablePages
if (maxPages <= 0) return
@@ -413,11 +444,20 @@ class CircleChatRepository(
val messages = historyResponse?.records
updateHistoryState(record.id, historyResponse, 1)
val pageData = buildPageData(position, record, messages)
synchronized(lock) {
val wasInserted = synchronized(lock) {
if (cache.get(position) == null) {
cache.put(position, pageData)
inFlight.remove(position)
true
} else {
inFlight.remove(position)
false
}
}
if (wasInserted) {
onPageLoaded?.let { callback ->
scope.launch(Dispatchers.Main) { callback(position) }
}
inFlight.remove(position)
}
}
}
@@ -499,6 +539,9 @@ class CircleChatRepository(
updateAvailablePages(available)
}
shouldMarkFetched = data != null
if (!shouldMarkFetched) {
hasLoadFailure = true
}
} finally {
val startPos = (pageNum - 1) * pageSize
synchronized(lock) {
@@ -603,7 +646,8 @@ class CircleChatRepository(
avatarUrl = "",
likeCount = 0,
commentCount = 0,
liked = false
liked = false,
messageVersion = -1
)
}

View File

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

View File

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

View File

@@ -158,6 +158,16 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
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) {
@@ -297,6 +307,7 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
?: return
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 {
val algorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
RenderEffectBlur()
@@ -307,14 +318,10 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
.setFrameClearDrawable(requireActivity().window.decorView.background)
.setBlurRadius(blurRadius)
.setBlurAutoUpdate(true)
.setOverlayColor(
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
)
.setOverlayColor(overlayColor)
} catch (_: Throwable) {
blurView.visibility = View.GONE
commentCard.setCardBackgroundColor(
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
)
commentCard.setCardBackgroundColor(overlayColor)
}
}
@@ -869,18 +876,6 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
}
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)
.load(item.avatarUrl)
.placeholder(R.drawable.a123123123)
.error(R.drawable.a123123123)
.placeholder(R.drawable.component_loading)
.error(R.drawable.default_avatar)
.into(ivAvatar)
// 选中状态显示不同图标和大小

View File

@@ -33,6 +33,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent
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.aiCompanionLikeRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
@@ -56,7 +58,14 @@ import android.os.Build
import androidx.core.content.ContextCompat
import android.widget.ImageView
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.chatDeleteRequest
import com.example.myapplication.keyboard.AiRolePreferences
import java.io.File
@@ -65,6 +74,7 @@ class CircleFragment : Fragment() {
private lateinit var pageRv: RecyclerView
private lateinit var inputOverlay: View
private lateinit var noResultOverlay: View
private lateinit var noResultSwipeRefresh: SwipeRefreshLayout
private lateinit var imeDismissOverlay: View
private lateinit var inputContainerText: View
private lateinit var inputContainerVoice: View
@@ -195,6 +205,13 @@ class CircleFragment : Fragment() {
pageRv = view.findViewById(R.id.pageRv)
inputOverlay = view.findViewById(R.id.inputOverlay)
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)
inputContainerText = view.findViewById(R.id.inputContainerText)
inputContainerVoice = view.findViewById(R.id.inputContainerVoice)
@@ -307,8 +324,22 @@ class CircleFragment : Fragment() {
sharedPool = sharedChatPool,
onLikeClick = { position, companionId -> handleLikeClick(position, companionId) },
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(
RESULT_COMMENT_COUNT_UPDATED,
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() {
@@ -425,6 +467,8 @@ class CircleFragment : Fragment() {
view?.post { requestOverlayUpdate() }
// 检查是否有加载失败的页面,如果有则重新加载
retryFailedPages()
// 消费后台期间积累的脏标记,刷新对应角色聊天记录
consumePendingDirtyCompanions()
}
// 清除失败缓存并重新加载
@@ -439,8 +483,52 @@ class CircleFragment : Fragment() {
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 在生命周期结束时的状态
override fun onDestroyView() {
if (::repository.isInitialized) {
repository.onPageLoaded = null
}
view?.let { root ->
ViewCompat.setWindowInsetsAnimationCallback(root, null)
root.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener)
@@ -823,7 +911,8 @@ class CircleFragment : Fragment() {
}
} catch (e: Exception) {
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
}
val data = response.data
@@ -832,7 +921,9 @@ class CircleFragment : Fragment() {
"1314520-Circle",
"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
}
@@ -862,6 +953,29 @@ class CircleFragment : Fragment() {
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 中当前选中的页面位置
private fun resolveCurrentPage(): Int {
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) {
if (pagePosition == RecyclerView.NO_POSITION || companionId <= 0) return
if (!likeInFlight.add(companionId)) return
@@ -979,6 +1102,104 @@ class CircleFragment : Fragment() {
.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() {
val lm = pageRv.layoutManager as? LinearLayoutManager ?: return
@@ -1140,6 +1361,9 @@ class CircleFragment : Fragment() {
blur.visibility = View.GONE
}
} else {
// global 页面可见时不恢复底栏
val globalContainer = activity?.findViewById<View>(R.id.global_container)
if (globalContainer != null && globalContainer.visibility == View.VISIBLE) return
// 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE
nav.visibility = View.VISIBLE
prevBottomNavVisibility = null

View File

@@ -10,7 +10,8 @@ class CirclePageAdapter(
private val sharedPool: RecyclerView.RecycledViewPool,
private val onLikeClick: ((position: Int, companionId: 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>() {
// 每页固定为屏幕高度,配合 PagerSnapHelper 使用。
@@ -53,7 +54,8 @@ class CirclePageAdapter(
},
onLikeClick,
onCommentClick,
onAvatarClick
onAvatarClick,
onMessageLongClick
)
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.example.myapplication.ui.home
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.TransitionDrawable
import android.os.Bundle
import android.util.Log
@@ -23,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.ImeGuideActivity
import com.example.myapplication.ui.common.LoadingOverlay
@@ -63,6 +65,7 @@ class HomeFragment : Fragment() {
private lateinit var tabList2: TextView
private lateinit var backgroundImage: ImageView
private lateinit var noResultOverlay: View
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private var lastList1RenderKey: String? = null
private lateinit var loadingOverlay: LoadingOverlay
@@ -265,6 +268,7 @@ class HomeFragment : Fragment() {
viewPager = view.findViewById(R.id.viewPager)
viewPager.isSaveEnabled = false
viewPager.offscreenPageLimit = 2
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay)
@@ -296,6 +300,20 @@ class HomeFragment : Fragment() {
// ✅ setupViewPager 只初始化一次
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 初始为空
setupTags()
@@ -708,25 +726,29 @@ class HomeFragment : Fragment() {
}
private fun refreshHomeAfterNetwork() {
loadingOverlay.show()
refreshAllData { loadingOverlay.hide() }
}
private fun refreshAllData(onFinish: () -> Unit) {
networkRefreshJob?.cancel()
networkRefreshJob = viewLifecycleOwner.lifecycleScope.launch {
preloadJob?.cancel()
loadingOverlay.show()
try {
list1Loaded = false
setNoResultVisible(false)
val list = fetchAllPersonaList()
// 先获取所有数据,不动 UI
val newPersonaList = fetchAllPersonaList()
if (!isAdded) return@launch
allPersonaCache = list
val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
// 数据全部返回后,一次性更新 UI
allPersonaCache = newPersonaList
list1Loaded = true
lastList1RenderKey = null
personaCache.clear()
notifyPageChangedOnMain(0)
updateNoResultOverlay(0)
val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
tags.clear()
response.data?.let { networkTags ->
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
@@ -735,9 +757,9 @@ class HomeFragment : Fragment() {
setupTags()
startPreloadAllTagsFillCacheOnly()
} catch (e: Exception) {
Log.e("HomeFragment", "refresh after network fail", e)
Log.e("HomeFragment", "refresh data fail", e)
} finally {
loadingOverlay.hide()
onFinish()
}
}
}
@@ -952,7 +974,11 @@ class HomeFragment : Fragment() {
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
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 按钮(失败回滚 + 防连点) ----------------
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
@@ -1046,7 +1072,11 @@ class HomeFragment : Fragment() {
addBtn.isVisible = true
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 时恢复
val originBg = addBtn.background

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@@ -21,6 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.bumptech.glide.Glide
import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent
@@ -48,6 +50,7 @@ class MineFragment : Fragment() {
private lateinit var avatar: CircleImageView
private lateinit var share: LinearLayout
private lateinit var loadingOverlay: LoadingOverlay
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private var loadUserJob: Job? = null
@@ -77,6 +80,15 @@ class MineFragment : Fragment() {
avatar = view.findViewById(R.id.avatar)
share = view.findViewById(R.id.click_Share)
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) 先用本地缓存秒出首屏
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 可靠)
@@ -231,6 +267,8 @@ class MineFragment : Fragment() {
cached?.avatarUrl?.let { url ->
Glide.with(requireContext())
.load(url)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar)
}
}
@@ -242,6 +280,7 @@ class MineFragment : Fragment() {
*/
private fun refreshUser(force: Boolean, showToast: Boolean = false) {
if (!isLoggedIn()) {
swipeRefreshLayout.isRefreshing = false
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.not_logged_in_toast), Toast.LENGTH_SHORT).show()
return
}
@@ -264,6 +303,8 @@ class MineFragment : Fragment() {
u?.avatarUrl?.let { url ->
Glide.with(requireContext())
.load(url)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar)
}
@@ -274,6 +315,8 @@ class MineFragment : Fragment() {
if (e is kotlinx.coroutines.CancellationException) return@launch
Log.e(TAG, "getUser failed", e)
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)
Glide.with(requireContext())
.load(R.drawable.default_avatar)
.placeholder(R.drawable.component_loading)
.error(R.drawable.no_search_result)
.into(avatar)
// 触发登出事件让MainActivity打开登录页面

View File

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

View File

@@ -1,18 +1,29 @@
package com.example.myapplication.ui.recharge
import android.graphics.Paint
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.google.android.material.appbar.AppBarLayout
import kotlin.math.abs
class RechargeFragment : Fragment() {
private var currentTab = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -24,15 +35,129 @@ class RechargeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 找到旧价格 TextView
val tvOldPrice = view.findViewById<TextView>(R.id.tvOldPrice)
// 旧价格加删除线
tvOldPrice.paintFlags = tvOldPrice.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
// 设置关闭按钮点击事件
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
// 收起输入法,避免从聊天页跳转过来时键盘残留
val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
val appBar = view.findViewById<AppBarLayout>(R.id.appBarLayout)
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)
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
setNoResultVisible(false)
val newThemes = getThemeList()?.data ?: emptyList()
if (!isAdded) return@launch
setNoResultVisible(false)
if (newThemes != tabTitles) {
tabTitles = newThemes
styleIds = tabTitles.map { it.id }

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.example.myapplication.network.FileDownloader
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.restoreThemeRequest
import com.example.myapplication.theme.ThemeManager
import com.example.myapplication.utils.unzipThemeSmart
import java.io.File
@@ -68,7 +69,8 @@ class ThemeDownloadWorker(
}
try {
val restoreResp = RetrofitClient.apiService.restoreTheme(themeId)
val restoreReq = restoreThemeRequest(themeId = themeId)
val restoreResp = RetrofitClient.apiService.restoreTheme(restoreReq)
if (restoreResp.code != 0) {
Log.e(TAG, "restoreTheme failed: code=${restoreResp.code} msg=${restoreResp.message}")
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:background="#F6F7FB"
tools:context=".ui.home.HomeFragment">
<!-- 内容 -->
<androidx.core.widget.NestedScrollView
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
android:layout_height="wrap_content"
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:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!-- recharge背景 -->
<ImageView
android:layout_width="match_parent"
@@ -24,8 +35,10 @@
android:src="@drawable/recharge_bg"
android:scaleType="fitXY"
android:adjustViewBounds="true" />
<!-- 关闭按钮 -->
<FrameLayout
android:id="@+id/closeContainer"
android:layout_width="@dimen/sw_46dp"
android:layout_height="@dimen/sw_46dp"
android:layout_marginTop="@dimen/_sw_198dp">
@@ -38,268 +51,186 @@
android:rotation="180"
android:scaleType="fitCenter" />
</FrameLayout>
<!-- vip -->
<ImageView
android:id="@+id/iv_close"
android:layout_width="@dimen/sw_321dp"
android:layout_height="@dimen/sw_269dp"
android:layout_marginStart="@dimen/sw_24dp"
android:elevation="@dimen/sw_1dp"
android:src="@drawable/vip_two"/>
android:src="@drawable/vip_two" />
<!-- 权益背景 -->
<ImageView
android:id="@+id/iv_close"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_391dp"
android:elevation="@dimen/sw_2dp"
android:layout_marginTop="@dimen/_sw_198dp"
android:src="@drawable/recharge_equity_bg"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/_sw_380dp"
android:elevation="@dimen/sw_3dp"
android:padding="@dimen/sw_16dp"
android:orientation="vertical">
<TextView
android:id="@+id/member"
android:layout_marginTop="@dimen/sw_28dp"
android:layout_width="@dimen/sw_290dp"
android:layout_height="wrap_content"
android:textSize="@dimen/sw_18sp"
android:textStyle="bold"
android:textColor="#1B1F1A"
android:layout_gravity="center_horizontal"
android:text="Become A Member Of LOVE KEY" />
<TextView
android:id="@+id/Unlock"
android:layout_marginTop="@dimen/sw_3dp"
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_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="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" />
<!-- 旧价格(划线) -->
<TextView
android:id="@+id/tvOldPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sw_8dp"
android:text="$4.49"
android:textSize="@dimen/sw_20sp"
android:textColor="#b3b3b3" />
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
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:padding="@dimen/sw_10dp">
<!-- 头像昵称 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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
android:id="@+id/rechargeButton"
android:layout_marginTop="@dimen/sw_24dp"
android:layout_width="@dimen/sw_343dp"
android:layout_height="@dimen/sw_51dp"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:background="@drawable/button_bg"
android:elevation="@dimen/sw_4dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/sw_15sp"
android:textStyle="bold"
android:gravity="center"
android:textColor="#1B1F1A"
android:text="Recharge now" />
</LinearLayout>
<!-- 协议 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_17dp"
android:textSize="@dimen/sw_10sp"
android:text="By clicking &quot;pay&quot;, you indicate your agreement to the"
android:textColor="#1B1F1A"
android:gravity="center_horizontal"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_4dp"
android:textSize="@dimen/sw_10sp"
android:text="《Embership Agreement》"
android:textColor="#02BEAC"
android:gravity="center_horizontal" />
android:src="@drawable/recharge_equity_bg" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 吸顶区域:不设 scrollFlags → 自动吸顶 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:background="#F6F7FB"
android:elevation="@dimen/sw_3dp"
android:outlineProvider="none">
<!-- 标题栏高度由代码控制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
android:id="@+id/stickyTitle"
android:layout_width="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:textStyle="bold"
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:background="@drawable/recharge_tab_bg">
<!-- 滑动指示器 -->
<View
android:id="@+id/tabIndicator"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- VIP 按钮 -->
<FrameLayout
android:id="@+id/tabVip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/ivVipTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/vip_select"
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>
</FrameLayout>
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:gravity="center_horizontal"
android:background="#F6F7FB"
android:paddingTop="@dimen/sw_12dp"
android:paddingBottom="@dimen/sw_16dp"
android:elevation="@dimen/sw_4dp">
<!-- 按钮 -->
<LinearLayout
android:id="@+id/rechargeButton"
android:layout_width="@dimen/sw_343dp"
android:layout_height="@dimen/sw_51dp"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:background="@drawable/recharge_now_bg"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/sw_15sp"
android:textStyle="bold"
android:gravity="center"
android:textColor="#FFFFFF"
android:text="@string/recharge_btn" />
</LinearLayout>
<!-- 协议 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_17dp"
android:textSize="@dimen/sw_10sp"
android:text="@string/recharge_pay_agreement"
android:textColor="#1B1F1A"
android:gravity="center_horizontal"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sw_4dp"
android:textSize="@dimen/sw_10sp"
android:text="@string/recharge_membership_agreement"
android:textColor="#02BEAC"
android:gravity="center_horizontal" />
</LinearLayout>
</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:elevation="@dimen/sw_10dp">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/noResultSwipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
android:gravity="center">
<ImageView
android:id="@+id/noResultImage"
@@ -271,6 +275,8 @@
android:textColor="#1B1F1A"
android:includeFontPadding="false" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 抽屉 -->

View File

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

View File

@@ -14,6 +14,11 @@
android:layout_height="match_parent"
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
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -144,6 +149,8 @@
<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_20dp">
<ImageView
android:id="@+id/imgLeft"
@@ -383,6 +390,7 @@
<!-- 电子邮件 -->
<LinearLayout
android:id="@+id/click_Email"
android:layout_width="match_parent"
android:layout_height="@dimen/sw_64dp"
android:gravity="center_vertical"
@@ -520,4 +528,6 @@
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</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
android:id="@+id/ivMenuAvatar"
android:layout_width="@dimen/sw_40dp"
android:layout_height="@dimen/sw_40dp"
android:src="@drawable/a123123123" />
android:layout_height="@dimen/sw_40dp"/>
<!-- 昵称和描述 -->
<LinearLayout

View File

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

View File

@@ -125,9 +125,11 @@
android:id="@+id/noResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/sw_50dp"
android:paddingEnd="@dimen/sw_50dp"
android:gravity="center"
android:textAlignment="center"
android:text="@string/search_not_data"
android:text="@string/skin_select_none"
android:textSize="@dimen/sw_13sp"
android:textColor="#1B1F1A"
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_exit_editing">退出编辑</string>
<string name="skin_select_all">个被选中</string>
<string name="skin_select_none">您目前还没有任何皮肤,快去下载吧</string>
<!-- 搜索 -->
<string name="search_hint">请输入你要搜索的内容</string>
@@ -218,6 +220,10 @@
<string name="gender_third">第三性别</string>
<string name="skip">跳过</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="currently_inputting">对方正在输入...</string>
@@ -249,5 +255,19 @@
<string name="Pop_up_window_SearchFragment_1">请输入搜索关键词。</string><!-- SearchFragment --><!-- SearchResultFragment -->
<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>

View File

@@ -113,6 +113,7 @@
<string name="skin_editor">Editor</string>
<string name="skin_exit_editing">Exit editing</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>
@@ -223,6 +224,10 @@
<string name="gender_third">The third gender</string>
<string name="skip">Skip</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="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_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>