优化
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
// ====== 按你给的规则固定值 ======
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
// 毛玻璃始终冻结显示,不做切换
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 选中状态显示不同图标和大小
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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打开登录页面
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 设置主题名称
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
app/src/main/res/drawable/bg_chat_error_dialog.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/drawable/bg_chat_error_dialog_button.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
5
app/src/main/res/drawable/bg_chat_message_popup.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/bg_recharge_svip.xml
Normal 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>
|
||||
@@ -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>
|
||||
6
app/src/main/res/drawable/bg_recharge_svip_benefits.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/bg_recharge_svip_card.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
@@ -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>
|
||||
BIN
app/src/main/res/drawable/chat_copy.png
Normal file
|
After Width: | Height: | Size: 670 B |
BIN
app/src/main/res/drawable/chat_delete.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
app/src/main/res/drawable/chat_history_loading.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/drawable/chat_report.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable/circle_not_data_bg.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
app/src/main/res/drawable/component_loading.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1.png
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_1.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_2.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_3.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_4.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_5.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_6.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_7.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_desc_8.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_1_icon.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/drawable/ic_recharge_svip_benefit_2.png
Normal file
|
After Width: | Height: | Size: 892 B |
BIN
app/src/main/res/drawable/ic_recharge_svip_hook.png
Normal file
|
After Width: | Height: | Size: 596 B |
7
app/src/main/res/drawable/recharge_now_bg.xml
Normal 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>
|
||||
6
app/src/main/res/drawable/recharge_tab_bg.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/svip_not_selected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/drawable/svip_select.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
app/src/main/res/drawable/vip_not_selected.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/drawable/vip_select.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
@@ -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 "pay", 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>
|
||||
|
||||
48
app/src/main/res/layout/dialog_chat_error_recharge.xml
Normal 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>
|
||||
@@ -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>
|
||||
<!-- 抽屉 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
14
app/src/main/res/layout/item_chat_loading.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
<!-- 键盘名称和下载量 -->
|
||||
|
||||
@@ -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" />
|
||||
|
||||
448
app/src/main/res/layout/page_recharge_svip.xml
Normal 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>
|
||||
126
app/src/main/res/layout/page_recharge_vip.xml
Normal 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>
|
||||
101
app/src/main/res/layout/popup_chat_message_menu.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||