优化
@@ -4,7 +4,9 @@
|
|||||||
"Bash(dir:*)",
|
"Bash(dir:*)",
|
||||||
"Bash(powershell:*)",
|
"Bash(powershell:*)",
|
||||||
"Bash(find /d/Test/MyApplication/app/src/main/assets -name \"vocab.txt\" -exec wc -l {} ;)",
|
"Bash(find /d/Test/MyApplication/app/src/main/assets -name \"vocab.txt\" -exec wc -l {} ;)",
|
||||||
"WebSearch"
|
"WebSearch",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(find:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ import android.graphics.Rect
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import kotlin.math.max
|
||||||
import com.example.myapplication.network.BehaviorReporter
|
import com.example.myapplication.network.BehaviorReporter
|
||||||
|
|
||||||
class GuideActivity : AppCompatActivity() {
|
class GuideActivity : AppCompatActivity() {
|
||||||
@@ -36,6 +40,8 @@ class GuideActivity : AppCompatActivity() {
|
|||||||
private lateinit var bottomPanel: LinearLayout
|
private lateinit var bottomPanel: LinearLayout
|
||||||
private lateinit var hintLayout: LinearLayout
|
private lateinit var hintLayout: LinearLayout
|
||||||
private lateinit var titleTextView: TextView
|
private lateinit var titleTextView: TextView
|
||||||
|
private var lastImeBottom = 0
|
||||||
|
private var lastSystemBottom = 0
|
||||||
|
|
||||||
|
|
||||||
// 我方的预设回复
|
// 我方的预设回复
|
||||||
@@ -117,45 +123,46 @@ class GuideActivity : AppCompatActivity() {
|
|||||||
findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
//输入框上移
|
//输入框上移(参考 CircleFragment,使用 WindowInsetsCompat 精确区分 IME 和导航栏高度)
|
||||||
|
// 1. WindowInsetsCompat 监听:处理初始状态和配置变化
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||||
|
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||||
|
val systemBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
|
applyImeInsets(imeBottom, systemBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
// 2. WindowInsetsAnimationCompat:跟随键盘动画平滑移动
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(
|
||||||
|
rootView,
|
||||||
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
||||||
|
override fun onProgress(
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
runningAnimations: MutableList<WindowInsetsAnimationCompat>
|
||||||
|
): WindowInsetsCompat {
|
||||||
|
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||||
|
val systemBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
|
applyImeInsets(imeBottom, systemBottom)
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 3. ViewTreeObserver 兼容方案:部分旧机型 WindowInsets 不触发,通过布局变化兜底
|
||||||
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
val r = Rect()
|
val r = Rect()
|
||||||
// 获取窗口可见区域
|
|
||||||
rootView.getWindowVisibleDisplayFrame(r)
|
rootView.getWindowVisibleDisplayFrame(r)
|
||||||
|
|
||||||
val screenHeight = rootView.rootView.height
|
val screenHeight = rootView.rootView.height
|
||||||
val visibleBottom = r.bottom
|
val heightDiff = (screenHeight - r.bottom).coerceAtLeast(0)
|
||||||
val keyboardHeight = screenHeight - visibleBottom
|
val threshold = (screenHeight * 0.15f).toInt()
|
||||||
|
val heightIme = if (heightDiff > threshold) heightDiff else 0
|
||||||
|
|
||||||
// 这个阈值防止“状态栏/导航栏变化”被误认为键盘
|
val rootInsets = ViewCompat.getRootWindowInsets(rootView)
|
||||||
val isKeyboardVisible = keyboardHeight > screenHeight * 0.15
|
val insetIme = rootInsets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
|
||||||
|
val imeBottom = max(heightIme, insetIme)
|
||||||
if (isKeyboardVisible) {
|
val systemBottom = rootInsets
|
||||||
// 键盘高度为正,把 bottomPanel 抬上去,但不要抬太高
|
?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom
|
||||||
// 只上移键盘高度减去底部面板高度,让输入框刚好在键盘上方
|
?: lastSystemBottom
|
||||||
val adjustedTranslation = -(keyboardHeight - bottomPanel.height)
|
applyImeInsets(imeBottom, systemBottom)
|
||||||
bottomPanel.translationY = adjustedTranslation.toFloat()
|
|
||||||
|
|
||||||
// 为了让最后一条消息不被挡住,可以给 scrollView 加个 paddingBottom
|
|
||||||
scrollView.setPadding(
|
|
||||||
scrollView.paddingLeft,
|
|
||||||
scrollView.paddingTop,
|
|
||||||
scrollView.paddingRight,
|
|
||||||
keyboardHeight + bottomPanel.height
|
|
||||||
)
|
|
||||||
|
|
||||||
// 再滚到底,保证能看到最新消息
|
|
||||||
scrollToBottom()
|
|
||||||
} else {
|
|
||||||
// 键盘收起,复位
|
|
||||||
bottomPanel.translationY = 0f
|
|
||||||
scrollView.setPadding(
|
|
||||||
scrollView.paddingLeft,
|
|
||||||
scrollView.paddingTop,
|
|
||||||
scrollView.paddingRight,
|
|
||||||
bottomPanel.height // 保持底部有一点空隙也可以按你需求调
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 键盘发送
|
// 键盘发送
|
||||||
@@ -293,4 +300,33 @@ class GuideActivity : AppCompatActivity() {
|
|||||||
scrollView.fullScroll(View.FOCUS_DOWN)
|
scrollView.fullScroll(View.FOCUS_DOWN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======== 键盘弹起/收起时调整 bottom_panel 位置 ========
|
||||||
|
private fun applyImeInsets(imeBottom: Int, systemBottom: Int) {
|
||||||
|
if (lastImeBottom == imeBottom && lastSystemBottom == systemBottom) return
|
||||||
|
lastImeBottom = imeBottom
|
||||||
|
lastSystemBottom = systemBottom
|
||||||
|
|
||||||
|
if (imeBottom > 0) {
|
||||||
|
// 键盘弹起:上移偏移量 = IME高度 - 导航栏高度(避免重复计算导航栏区域)
|
||||||
|
val offset = (imeBottom - systemBottom).coerceAtLeast(0)
|
||||||
|
bottomPanel.translationY = -offset.toFloat()
|
||||||
|
scrollView.setPadding(
|
||||||
|
scrollView.paddingLeft,
|
||||||
|
scrollView.paddingTop,
|
||||||
|
scrollView.paddingRight,
|
||||||
|
offset + bottomPanel.height
|
||||||
|
)
|
||||||
|
scrollToBottom()
|
||||||
|
} else {
|
||||||
|
// 键盘收起:复位
|
||||||
|
bottomPanel.translationY = 0f
|
||||||
|
scrollView.setPadding(
|
||||||
|
scrollView.paddingLeft,
|
||||||
|
scrollView.paddingTop,
|
||||||
|
scrollView.paddingRight,
|
||||||
|
bottomPanel.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
@@ -10,8 +11,10 @@ import android.net.NetworkRequest
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -261,6 +264,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
is AuthEvent.CharacterAdded -> {
|
is AuthEvent.CharacterAdded -> {
|
||||||
// 不需要处理,由HomeFragment处理
|
// 不需要处理,由HomeFragment处理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AuthEvent.ShowChatErrorRecharge -> {
|
||||||
|
showChatErrorRechargeDialog(event.errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthEvent.KeyboardChatUpdated -> {
|
||||||
|
// 由 CircleFragment 自行处理
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,10 +298,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
// 处理来自键盘的跳转请求
|
||||||
|
handleRechargeIntent(intent)
|
||||||
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener,也能恢复底栏
|
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener,也能恢复底栏
|
||||||
bottomNav.post { updateBottomNavVisibility() }
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
handleRechargeIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRechargeIntent(intent: Intent?) {
|
||||||
|
if (intent?.action == "ACTION_OPEN_RECHARGE") {
|
||||||
|
intent.action = null
|
||||||
|
openGlobal(R.id.rechargeFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
// ✅ 防泄漏:移除路由监听(Activity 销毁时)
|
// ✅ 防泄漏:移除路由监听(Activity 销毁时)
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -584,6 +610,29 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return globalNavController.currentDestination?.id != R.id.globalEmptyFragment
|
return globalNavController.currentDestination?.id != R.id.globalEmptyFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
|
||||||
|
val dialogView = LayoutInflater.from(this)
|
||||||
|
.inflate(R.layout.dialog_chat_error_recharge, null)
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
dialog.window?.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
dialog.window?.setDimAmount(0.5f)
|
||||||
|
if (!errorMessage.isNullOrEmpty()) {
|
||||||
|
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
openGlobal(R.id.rechargeFragment)
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupBackPress() {
|
private fun setupBackPress() {
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
|
|||||||
@@ -674,7 +674,8 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.load(info.avatarUrl)
|
.load(info.avatarUrl)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.placeholder(android.R.drawable.ic_menu_myplaces)
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatarView)
|
.into(avatarView)
|
||||||
} else {
|
} else {
|
||||||
avatarView.setImageResource(android.R.drawable.ic_menu_myplaces)
|
avatarView.setImageResource(android.R.drawable.ic_menu_myplaces)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.example.myapplication.keyboard
|
package com.example.myapplication.keyboard
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
import com.example.myapplication.network.RetrofitClient
|
import com.example.myapplication.network.RetrofitClient
|
||||||
import com.example.myapplication.network.chatMessageRequest
|
import com.example.myapplication.network.chatMessageRequest
|
||||||
import com.example.myapplication.ui.circle.ChatMessage
|
import com.example.myapplication.ui.circle.ChatMessage
|
||||||
@@ -114,20 +118,15 @@ class AiRolePanelController(private val context: Context) {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "chatMessage failed: ${e.message}", e)
|
Log.e(TAG, "chatMessage failed: ${e.message}", e)
|
||||||
placeholder.text = context.getString(R.string.refresh_failed)
|
removePlaceholder(placeholder)
|
||||||
placeholder.isLoading = false
|
|
||||||
placeholder.hasAnimated = true
|
|
||||||
adapter?.notifyMessageUpdated(placeholder.id)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = response.data
|
val data = response.data
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Log.e(TAG, "chatMessage no data, code=${response.code}")
|
Log.e(TAG, "chatMessage no data, code=${response.code}")
|
||||||
placeholder.text = context.getString(R.string.refresh_failed)
|
removePlaceholder(placeholder)
|
||||||
placeholder.isLoading = false
|
showChatErrorRechargeDialog(response.message)
|
||||||
placeholder.hasAnimated = true
|
|
||||||
adapter?.notifyMessageUpdated(placeholder.id)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +137,11 @@ class AiRolePanelController(private val context: Context) {
|
|||||||
adapter?.notifyMessageUpdated(placeholder.id)
|
adapter?.notifyMessageUpdated(placeholder.id)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 通知 CircleFragment 刷新对应角色的聊天记录
|
||||||
|
AuthEventBus.emit(AuthEvent.KeyboardChatUpdated(currentCompanionId))
|
||||||
|
// 持久化脏标记,确保应用从后台恢复时也能刷新
|
||||||
|
AiRolePreferences.markCompanionDirty(context, currentCompanionId)
|
||||||
|
|
||||||
// 轮询音频 URL
|
// 轮询音频 URL
|
||||||
val audioUrl = fetchAudioUrl(data.audioId)
|
val audioUrl = fetchAudioUrl(data.audioId)
|
||||||
if (!audioUrl.isNullOrBlank()) {
|
if (!audioUrl.isNullOrBlank()) {
|
||||||
@@ -169,6 +173,46 @@ class AiRolePanelController(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removePlaceholder(placeholder: ChatMessage) {
|
||||||
|
val index = messages.indexOfFirst { it.id == placeholder.id }
|
||||||
|
if (index >= 0) {
|
||||||
|
messages.removeAt(index)
|
||||||
|
adapter?.notifyMessageRemoved(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
|
||||||
|
val windowToken = recyclerView?.windowToken ?: return
|
||||||
|
val dialogView = LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.dialog_chat_error_recharge, null)
|
||||||
|
val dialog = android.app.AlertDialog.Builder(context)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
dialog.window?.let { window ->
|
||||||
|
window.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
window.setDimAmount(0.5f)
|
||||||
|
window.setType(android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG)
|
||||||
|
window.attributes = window.attributes.also { it.token = windowToken }
|
||||||
|
}
|
||||||
|
if (!errorMessage.isNullOrEmpty()) {
|
||||||
|
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
val intent = Intent(context, com.example.myapplication.MainActivity::class.java).apply {
|
||||||
|
action = "ACTION_OPEN_RECHARGE"
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun nextId(): Long = messageIdCounter++
|
private fun nextId(): Long = messageIdCounter++
|
||||||
|
|
||||||
fun clearMessages() {
|
fun clearMessages() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ object AiRolePreferences {
|
|||||||
private const val KEY_COMPANION_ID = "companion_id"
|
private const val KEY_COMPANION_ID = "companion_id"
|
||||||
private const val KEY_PERSONA_NAME = "persona_name"
|
private const val KEY_PERSONA_NAME = "persona_name"
|
||||||
private const val KEY_AVATAR_URL = "avatar_url"
|
private const val KEY_AVATAR_URL = "avatar_url"
|
||||||
|
private const val KEY_DIRTY_COMPANIONS = "dirty_companion_ids"
|
||||||
|
|
||||||
data class CompanionInfo(
|
data class CompanionInfo(
|
||||||
val companionId: Int,
|
val companionId: Int,
|
||||||
@@ -49,4 +50,26 @@ object AiRolePreferences {
|
|||||||
.clear()
|
.clear()
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记某个 companionId 的聊天记录需要刷新(键盘聊天成功后调用)
|
||||||
|
*/
|
||||||
|
fun markCompanionDirty(context: Context, companionId: Int) {
|
||||||
|
val sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
|
||||||
|
val existing = sp.getStringSet(KEY_DIRTY_COMPANIONS, emptySet()) ?: emptySet()
|
||||||
|
sp.edit()
|
||||||
|
.putStringSet(KEY_DIRTY_COMPANIONS, existing + companionId.toString())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费所有待刷新的 companionId,返回后自动清除标记
|
||||||
|
*/
|
||||||
|
fun consumeDirtyCompanions(context: Context): Set<Int> {
|
||||||
|
val sp = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS)
|
||||||
|
val raw = sp.getStringSet(KEY_DIRTY_COMPANIONS, null)
|
||||||
|
if (raw.isNullOrEmpty()) return emptySet()
|
||||||
|
sp.edit().remove(KEY_DIRTY_COMPANIONS).apply()
|
||||||
|
return raw.mapNotNull { it.toIntOrNull() }.toSet()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ interface ApiService {
|
|||||||
@GET("user/inviteCode")
|
@GET("user/inviteCode")
|
||||||
suspend fun inviteCode(
|
suspend fun inviteCode(
|
||||||
): ApiResponse<ShareResponse>
|
): ApiResponse<ShareResponse>
|
||||||
|
|
||||||
|
// 获取客服邮箱
|
||||||
|
@GET("user/customerMail")
|
||||||
|
suspend fun delUserCharacter(
|
||||||
|
): ApiResponse<String>
|
||||||
//===========================================首页=================================
|
//===========================================首页=================================
|
||||||
// 标签列表
|
// 标签列表
|
||||||
@GET("tag/list")
|
@GET("tag/list")
|
||||||
@@ -197,7 +202,7 @@ interface ApiService {
|
|||||||
//恢复已删除的主题
|
//恢复已删除的主题
|
||||||
@POST("themes/restore")
|
@POST("themes/restore")
|
||||||
suspend fun restoreTheme(
|
suspend fun restoreTheme(
|
||||||
@Query("themeId") themeId: Int
|
@Body body: restoreThemeRequest
|
||||||
): ApiResponse<Unit>
|
): ApiResponse<Unit>
|
||||||
// =========================================圈子(ai陪聊)============================================
|
// =========================================圈子(ai陪聊)============================================
|
||||||
// 分页查询AI陪聊角色
|
// 分页查询AI陪聊角色
|
||||||
@@ -285,6 +290,12 @@ interface ApiService {
|
|||||||
@Body body: chatSessionResetRequest
|
@Body body: chatSessionResetRequest
|
||||||
): ApiResponse<chatSessionResetResponse>
|
): ApiResponse<chatSessionResetResponse>
|
||||||
|
|
||||||
|
//删除聊天记录
|
||||||
|
@POST("chat/history/delete")
|
||||||
|
suspend fun chatDelete(
|
||||||
|
@Body body: chatDeleteRequest
|
||||||
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,6 @@ sealed class AuthEvent {
|
|||||||
) : AuthEvent()
|
) : AuthEvent()
|
||||||
object UserUpdated : AuthEvent()
|
object UserUpdated : AuthEvent()
|
||||||
data class CharacterDeleted(val characterId: Int) : AuthEvent()
|
data class CharacterDeleted(val characterId: Int) : AuthEvent()
|
||||||
|
data class ShowChatErrorRecharge(val errorMessage: String? = null) : AuthEvent()
|
||||||
|
data class KeyboardChatUpdated(val companionId: Int) : AuthEvent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ object BehaviorHttpClient {
|
|||||||
|
|
||||||
private const val TAG = "BehaviorHttp"
|
private const val TAG = "BehaviorHttp"
|
||||||
|
|
||||||
// TODO:改成你的行为服务 baseUrl(必须以 / 结尾)
|
// TODO:改成你的行为服务 baseUrl(必须以 / 结尾)(上报接口)
|
||||||
private const val BASE_URL = "http://192.168.2.22:35310/api/"
|
private const val BASE_URL = "http://192.168.2.22:35310/api/"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,14 +258,14 @@ private fun bodyToString(body: okhttp3.RequestBody): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON 扁平化规则:
|
* JSON 扁平化规则(与 iOS KBSignUtils 对齐):
|
||||||
* object: a.b.c
|
* 仅展开顶层 object 的 key-value
|
||||||
* array : items[0].id
|
* 数组和嵌套对象直接转为 JSON 字符串作为 value(不递归展开)
|
||||||
*/
|
*/
|
||||||
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
|
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
|
||||||
when {
|
when {
|
||||||
elem.isJsonNull -> {
|
elem.isJsonNull -> {
|
||||||
// null 不参与签名(服务端也要一致)
|
// null 不参与签名
|
||||||
}
|
}
|
||||||
elem.isJsonPrimitive -> {
|
elem.isJsonPrimitive -> {
|
||||||
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
|
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
|
||||||
@@ -274,14 +274,20 @@ private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<Strin
|
|||||||
val obj = elem.asJsonObject
|
val obj = elem.asJsonObject
|
||||||
for ((k, v) in obj.entrySet()) {
|
for ((k, v) in obj.entrySet()) {
|
||||||
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
|
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
|
||||||
|
if (prefix.isBlank() && (v.isJsonArray || v.isJsonObject)) {
|
||||||
|
// 顶层的数组/对象:直接转 JSON 字符串,与 iOS 一致
|
||||||
|
val jsonStr = Gson().toJson(v)
|
||||||
|
if (jsonStr.isNotBlank()) out[newKey] = jsonStr
|
||||||
|
} else {
|
||||||
flattenJson(v, newKey, out)
|
flattenJson(v, newKey, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
elem.isJsonArray -> {
|
elem.isJsonArray -> {
|
||||||
val arr = elem.asJsonArray
|
// 顶层数组场景(极少见),直接转 JSON 字符串
|
||||||
for (i in 0 until arr.size()) {
|
if (prefix.isNotBlank()) {
|
||||||
val newKey = "$prefix[$i]"
|
val jsonStr = Gson().toJson(elem)
|
||||||
flattenJson(arr[i], newKey, out)
|
if (jsonStr.isNotBlank()) out[prefix] = jsonStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ data class purchaseThemeRequest(
|
|||||||
val themeId: Int,
|
val themeId: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//恢复主题
|
||||||
|
data class restoreThemeRequest(
|
||||||
|
val themeId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
// =========================================圈子(ai陪聊)============================================
|
// =========================================圈子(ai陪聊)============================================
|
||||||
|
|
||||||
//分页查询AI陪聊角色
|
//分页查询AI陪聊角色
|
||||||
@@ -473,3 +478,7 @@ data class chatSessionResetResponse(
|
|||||||
val resetVersion: Int,
|
val resetVersion: Int,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class chatDeleteRequest(
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
@@ -19,7 +19,8 @@ import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
|||||||
|
|
||||||
object NetworkClient {
|
object NetworkClient {
|
||||||
|
|
||||||
private const val BASE_URL = "http://192.168.2.22:7529/api"
|
// private const val BASE_URL = "http://192.168.2.22:7529/api"
|
||||||
|
private const val BASE_URL = "https://devcallback.loveamorkey.com/api"
|
||||||
private const val TAG = "999-SSE_TALK"
|
private const val TAG = "999-SSE_TALK"
|
||||||
|
|
||||||
// ====== 按你给的规则固定值 ======
|
// ====== 按你给的规则固定值 ======
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import com.example.myapplication.network.FileUploadService
|
|||||||
|
|
||||||
object RetrofitClient {
|
object RetrofitClient {
|
||||||
|
|
||||||
private const val BASE_URL = "http://192.168.2.22:7529/api/"
|
// private const val BASE_URL = "http://192.168.2.22:7529/api/"
|
||||||
|
private const val BASE_URL = "https://devcallback.loveamorkey.com/api/"
|
||||||
|
|
||||||
// 保存 ApplicationContext
|
// 保存 ApplicationContext
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.media.MediaPlayer
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
@@ -13,9 +14,20 @@ import android.widget.TextView
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
|
|
||||||
class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHolder>() {
|
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private var items: MutableList<ChatMessage> = mutableListOf()
|
private var items: MutableList<ChatMessage> = mutableListOf()
|
||||||
|
var onMessageLongClick: ((message: ChatMessage, anchorView: View, rawX: Float, rawY: Float) -> Unit)? = null
|
||||||
|
var showLoading: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
notifyItemInserted(0)
|
||||||
|
} else {
|
||||||
|
notifyItemRemoved(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var playingMessageId: Long? = null
|
private var playingMessageId: Long? = null
|
||||||
@@ -25,11 +37,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
|
|||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toDataIndex(adapterPosition: Int): Int =
|
||||||
|
if (showLoading) adapterPosition - 1 else adapterPosition
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return if (items[position].isMine) VIEW_TYPE_ME else VIEW_TYPE_BOT
|
if (showLoading && position == 0) return VIEW_TYPE_LOADING
|
||||||
|
val dataIndex = toDataIndex(position)
|
||||||
|
return if (items[dataIndex].isMine) VIEW_TYPE_ME else VIEW_TYPE_BOT
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
if (viewType == VIEW_TYPE_LOADING) {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_chat_loading, parent, false)
|
||||||
|
return LoadingViewHolder(view)
|
||||||
|
}
|
||||||
val layout = if (viewType == VIEW_TYPE_ME) {
|
val layout = if (viewType == VIEW_TYPE_ME) {
|
||||||
R.layout.item_chat_message_me
|
R.layout.item_chat_message_me
|
||||||
} else {
|
} else {
|
||||||
@@ -39,18 +61,31 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
|
|||||||
return MessageViewHolder(view)
|
return MessageViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
holder.bind(items[position])
|
if (holder is LoadingViewHolder) {
|
||||||
|
holder.bind()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(holder as MessageViewHolder).bind(items[toDataIndex(position)])
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewRecycled(holder: MessageViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
holder.onRecycled()
|
when (holder) {
|
||||||
|
is MessageViewHolder -> holder.onRecycled()
|
||||||
|
is LoadingViewHolder -> holder.onRecycled()
|
||||||
|
}
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun getItemCount(): Int = items.size + if (showLoading) 1 else 0
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long = items[position].id
|
override fun getItemId(position: Int): Long {
|
||||||
|
if (showLoading && position == 0) return Long.MIN_VALUE
|
||||||
|
return items[toDataIndex(position)].id
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toAdapterIndex(dataIndex: Int): Int =
|
||||||
|
if (showLoading) dataIndex + 1 else dataIndex
|
||||||
|
|
||||||
fun bindMessages(messages: MutableList<ChatMessage>) {
|
fun bindMessages(messages: MutableList<ChatMessage>) {
|
||||||
items = messages
|
items = messages
|
||||||
@@ -60,14 +95,20 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
|
|||||||
fun notifyLastInserted() {
|
fun notifyLastInserted() {
|
||||||
val index = items.size - 1
|
val index = items.size - 1
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
notifyItemInserted(index)
|
notifyItemInserted(toAdapterIndex(index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyMessageUpdated(messageId: Long) {
|
fun notifyMessageUpdated(messageId: Long) {
|
||||||
val index = items.indexOfFirst { it.id == messageId }
|
val index = items.indexOfFirst { it.id == messageId }
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
notifyItemChanged(index)
|
notifyItemChanged(toAdapterIndex(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyMessageRemoved(position: Int) {
|
||||||
|
if (position >= 0) {
|
||||||
|
notifyItemRemoved(toAdapterIndex(position))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +227,28 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
|
|||||||
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading)
|
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading)
|
||||||
private val textLoadingAnimation =
|
private val textLoadingAnimation =
|
||||||
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_text_loading)
|
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_text_loading)
|
||||||
|
private var lastTouchRawX = 0f
|
||||||
|
private var lastTouchRawY = 0f
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.setOnTouchListener { _, event ->
|
||||||
|
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||||
|
lastTouchRawX = event.rawX
|
||||||
|
lastTouchRawY = event.rawY
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
itemView.setOnLongClickListener {
|
||||||
|
val pos = adapterPosition
|
||||||
|
if (pos != RecyclerView.NO_POSITION && pos < items.size) {
|
||||||
|
val msg = items[pos]
|
||||||
|
if (!msg.isLoading) {
|
||||||
|
onMessageLongClick?.invoke(msg, itemView, lastTouchRawX, lastTouchRawY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(message: ChatMessage) {
|
fun bind(message: ChatMessage) {
|
||||||
if (boundMessageId != message.id) {
|
if (boundMessageId != message.id) {
|
||||||
@@ -280,6 +343,21 @@ class ChatMessageAdapter : RecyclerView.Adapter<ChatMessageAdapter.MessageViewHo
|
|||||||
private companion object {
|
private companion object {
|
||||||
const val VIEW_TYPE_ME = 1
|
const val VIEW_TYPE_ME = 1
|
||||||
const val VIEW_TYPE_BOT = 2
|
const val VIEW_TYPE_BOT = 2
|
||||||
|
const val VIEW_TYPE_LOADING = 3
|
||||||
const val TYPE_DELAY_MS = 28L
|
const val TYPE_DELAY_MS = 28L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val loadingIcon: ImageView = itemView.findViewById(R.id.loadingIcon)
|
||||||
|
private val rotateAnim =
|
||||||
|
AnimationUtils.loadAnimation(itemView.context, R.anim.circle_audio_loading)
|
||||||
|
|
||||||
|
fun bind() {
|
||||||
|
loadingIcon.startAnimation(rotateAnim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRecycled() {
|
||||||
|
loadingIcon.clearAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,9 +98,11 @@ class ChatPageViewHolder(
|
|||||||
onLoadMore: ((position: Int, companionId: Int, onResult: (ChatHistoryLoadResult) -> Unit) -> Unit)?,
|
onLoadMore: ((position: Int, companionId: Int, onResult: (ChatHistoryLoadResult) -> Unit) -> Unit)?,
|
||||||
onLikeClick: ((position: Int, companionId: Int) -> Unit)?,
|
onLikeClick: ((position: Int, companionId: Int) -> Unit)?,
|
||||||
onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)?,
|
onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)?,
|
||||||
onAvatarClick: ((companionId: Int) -> Unit)?
|
onAvatarClick: ((companionId: Int) -> Unit)?,
|
||||||
|
onMessageLongClick: ((message: ChatMessage, anchorView: View, rawX: Float, rawY: Float) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
boundCompanionId = data.companionId
|
boundCompanionId = data.companionId
|
||||||
|
messageAdapter.onMessageLongClick = onMessageLongClick
|
||||||
hasMoreHistory = historyState.hasMore
|
hasMoreHistory = historyState.hasMore
|
||||||
isLoadingHistory = historyState.isLoading
|
isLoadingHistory = historyState.isLoading
|
||||||
this.historyStateProvider = historyStateProvider
|
this.historyStateProvider = historyStateProvider
|
||||||
@@ -114,10 +116,14 @@ class ChatPageViewHolder(
|
|||||||
boundCommentCount = data.commentCount
|
boundCommentCount = data.commentCount
|
||||||
Glide.with(backgroundView.context)
|
Glide.with(backgroundView.context)
|
||||||
.load(data.backgroundColor)
|
.load(data.backgroundColor)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.circle_not_data_bg)
|
||||||
.into(backgroundView)
|
.into(backgroundView)
|
||||||
|
|
||||||
Glide.with(avatarView.context)
|
Glide.with(avatarView.context)
|
||||||
.load(data.avatarUrl)
|
.load(data.avatarUrl)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.default_avatar)
|
||||||
.into(avatarView)
|
.into(avatarView)
|
||||||
|
|
||||||
likeView.setImageResource(
|
likeView.setImageResource(
|
||||||
@@ -203,12 +209,15 @@ class ChatPageViewHolder(
|
|||||||
val requestedCompanionId = boundCompanionId
|
val requestedCompanionId = boundCompanionId
|
||||||
val requestedPageId = boundPageId
|
val requestedPageId = boundPageId
|
||||||
isLoadingHistory = true
|
isLoadingHistory = true
|
||||||
|
messageAdapter.showLoading = true
|
||||||
callback(position, requestedCompanionId) { result ->
|
callback(position, requestedCompanionId) { result ->
|
||||||
if (requestedCompanionId != boundCompanionId || requestedPageId != boundPageId) {
|
if (requestedCompanionId != boundCompanionId || requestedPageId != boundPageId) {
|
||||||
|
messageAdapter.showLoading = false
|
||||||
return@callback
|
return@callback
|
||||||
}
|
}
|
||||||
isLoadingHistory = false
|
isLoadingHistory = false
|
||||||
hasMoreHistory = result.hasMore
|
hasMoreHistory = result.hasMore
|
||||||
|
messageAdapter.showLoading = false
|
||||||
if (result.insertedCount > 0) {
|
if (result.insertedCount > 0) {
|
||||||
notifyMessagesPrepended(result.insertedCount)
|
notifyMessagesPrepended(result.insertedCount)
|
||||||
}
|
}
|
||||||
@@ -238,7 +247,12 @@ class ChatPageViewHolder(
|
|||||||
messageAdapter.notifyMessageUpdated(messageId)
|
messageAdapter.notifyMessageUpdated(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun notifyMessageRemoved(position: Int) {
|
||||||
|
messageAdapter.notifyMessageRemoved(position)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRecycled() {
|
override fun onRecycled() {
|
||||||
|
messageAdapter.showLoading = false
|
||||||
messageAdapter.release()
|
messageAdapter.release()
|
||||||
chatRv.stopScroll()
|
chatRv.stopScroll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,13 +48,17 @@ class CircleChatRepository(
|
|||||||
private var knownTotalPages: Int? = null
|
private var knownTotalPages: Int? = null
|
||||||
@Volatile
|
@Volatile
|
||||||
private var availablePages: Int = totalPages
|
private var availablePages: Int = totalPages
|
||||||
|
@Volatile
|
||||||
|
private var hasLoadFailure = false
|
||||||
var onTotalPagesChanged: ((Int) -> Unit)? = null
|
var onTotalPagesChanged: ((Int) -> Unit)? = null
|
||||||
|
@Volatile
|
||||||
|
var onPageLoaded: ((position: Int) -> Unit)? = null
|
||||||
|
|
||||||
// 后台协程用于预加载。
|
// 后台协程用于预加载。
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private val messageId = AtomicLong((totalPages.toLong() + 1) * 1_000_000L)
|
private val messageId = AtomicLong((totalPages.toLong() + 1) * 1_000_000L)
|
||||||
|
|
||||||
//获取指定位置的聊天页面数据
|
//获取指定位置的聊天页面数据(非阻塞,缓存未命中时触发后台加载)
|
||||||
fun getPage(position: Int): ChatPageData {
|
fun getPage(position: Int): ChatPageData {
|
||||||
if (position < 0 || position >= availablePages) {
|
if (position < 0 || position >= availablePages) {
|
||||||
return emptyPage(position)
|
return emptyPage(position)
|
||||||
@@ -62,18 +66,9 @@ class CircleChatRepository(
|
|||||||
val cached = synchronized(lock) { cache.get(position) }
|
val cached = synchronized(lock) { cache.get(position) }
|
||||||
if (cached != null) return cached
|
if (cached != null) return cached
|
||||||
|
|
||||||
val page = createPage(position)
|
// 缓存未命中:触发后台预加载,立即返回空占位页避免阻塞调用线程
|
||||||
return synchronized(lock) {
|
preloadAround(position)
|
||||||
val existing = cache.get(position)
|
return emptyPage(position)
|
||||||
if (existing != null) {
|
|
||||||
inFlight.remove(position)
|
|
||||||
existing
|
|
||||||
} else {
|
|
||||||
cache.put(position, page)
|
|
||||||
inFlight.remove(position)
|
|
||||||
page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//主要功能是根据用户当前查看的页面位置,预加载其前后若干页的数据,并将这些数据放入缓存中
|
//主要功能是根据用户当前查看的页面位置,预加载其前后若干页的数据,并将这些数据放入缓存中
|
||||||
@@ -88,6 +83,7 @@ class CircleChatRepository(
|
|||||||
// 清除加载失败的缓存页面(companionId <= 0),使后续 preloadAround 能重新加载
|
// 清除加载失败的缓存页面(companionId <= 0),使后续 preloadAround 能重新加载
|
||||||
fun invalidateFailedPages() {
|
fun invalidateFailedPages() {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
|
hasLoadFailure = false
|
||||||
val snapshot = cache.snapshot()
|
val snapshot = cache.snapshot()
|
||||||
for ((position, page) in snapshot) {
|
for ((position, page) in snapshot) {
|
||||||
if (page.companionId <= 0) {
|
if (page.companionId <= 0) {
|
||||||
@@ -97,8 +93,24 @@ class CircleChatRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查缓存中是否存在加载失败的页面
|
// 清除全部缓存和状态,用于下拉刷新等场景强制重新加载所有数据
|
||||||
|
fun invalidateAll() {
|
||||||
|
synchronized(lock) {
|
||||||
|
cache.evictAll()
|
||||||
|
companionCache.evictAll()
|
||||||
|
inFlight.clear()
|
||||||
|
pageInFlight.clear()
|
||||||
|
pageFetched.clear()
|
||||||
|
historyStates.clear()
|
||||||
|
hasLoadFailure = false
|
||||||
|
knownTotalPages = null
|
||||||
|
availablePages = totalPages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在加载失败的页面
|
||||||
fun hasFailedPages(): Boolean {
|
fun hasFailedPages(): Boolean {
|
||||||
|
if (hasLoadFailure) return true
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
for ((_, page) in cache.snapshot()) {
|
for ((_, page) in cache.snapshot()) {
|
||||||
if (page.companionId <= 0) return true
|
if (page.companionId <= 0) return true
|
||||||
@@ -264,6 +276,18 @@ class CircleChatRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeMessage(position: Int, messageId: Long): Int {
|
||||||
|
synchronized(lock) {
|
||||||
|
val page = getPage(position)
|
||||||
|
val index = page.messages.indexOfFirst { it.id == messageId }
|
||||||
|
if (index >= 0) {
|
||||||
|
page.messages.removeAt(index)
|
||||||
|
page.messageVersion++
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateLikeState(position: Int, companionId: Int, liked: Boolean, likeCount: Int): Boolean {
|
fun updateLikeState(position: Int, companionId: Int, liked: Boolean, likeCount: Int): Boolean {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
val page = cache.get(position)
|
val page = cache.get(position)
|
||||||
@@ -307,6 +331,39 @@ class CircleChatRepository(
|
|||||||
// return sampleLines[random.nextInt(sampleLines.size)]
|
// return sampleLines[random.nextInt(sampleLines.size)]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新指定 companionId 对应页面的聊天记录(从服务端重新获取第 1 页)。
|
||||||
|
* 返回被更新的 position 列表,用于通知 UI 刷新。
|
||||||
|
* 注意:此方法包含网络 IO,必须在后台线程调用。
|
||||||
|
*/
|
||||||
|
fun refreshCompanionMessages(companionId: Int): List<Int> {
|
||||||
|
if (companionId <= 0) return emptyList()
|
||||||
|
val updatedPositions = ArrayList<Int>()
|
||||||
|
val matchedPositions = synchronized(lock) {
|
||||||
|
cache.snapshot().filter { it.value.companionId == companionId }.keys.toList()
|
||||||
|
}
|
||||||
|
if (matchedPositions.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val response = fetchChatRecords(companionId, 1, DEFAULT_CHAT_PAGE_SIZE)
|
||||||
|
val freshMessages = mapChatRecords(response.data?.records)
|
||||||
|
|
||||||
|
synchronized(lock) {
|
||||||
|
for (position in matchedPositions) {
|
||||||
|
val page = cache.get(position) ?: continue
|
||||||
|
if (page.companionId != companionId) continue
|
||||||
|
page.messages.clear()
|
||||||
|
page.messages.addAll(freshMessages)
|
||||||
|
page.messageVersion++
|
||||||
|
updatedPositions.add(position)
|
||||||
|
}
|
||||||
|
historyStates.remove(companionId)
|
||||||
|
if (response.data != null) {
|
||||||
|
updateHistoryState(companionId, response.data, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedPositions
|
||||||
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
@@ -332,32 +389,6 @@ class CircleChatRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//主要功能是确保指定位置的聊天页面数据已经存在于缓存中。如果指定位置的页面数据不存在,则生成该页面的数据并将其放入缓存中
|
|
||||||
private fun createPage(position: Int): ChatPageData {
|
|
||||||
val cachedCompanion = synchronized(lock) { companionCache.get(position) }
|
|
||||||
val companionInfo = cachedCompanion ?: run {
|
|
||||||
val pageNum = position / pageFetchSize + 1
|
|
||||||
val records = fetchCompanionPage(pageNum, pageFetchSize)
|
|
||||||
val index = position - (pageNum - 1) * pageFetchSize
|
|
||||||
records.getOrNull(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (companionInfo == null) {
|
|
||||||
return emptyPage(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
val historyResponse = fetchChatRecords(
|
|
||||||
companionInfo.id,
|
|
||||||
1,
|
|
||||||
DEFAULT_CHAT_PAGE_SIZE
|
|
||||||
).data
|
|
||||||
val messages = historyResponse?.records
|
|
||||||
updateHistoryState(companionInfo.id, historyResponse, 1)
|
|
||||||
Log.d("1314520-CircleChatRepository", "createPage: $position")
|
|
||||||
|
|
||||||
return buildPageData(position, companionInfo, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preloadRange(start: Int, end: Int, pageSize: Int, chatPageSize: Int) {
|
private fun preloadRange(start: Int, end: Int, pageSize: Int, chatPageSize: Int) {
|
||||||
val maxPages = availablePages
|
val maxPages = availablePages
|
||||||
if (maxPages <= 0) return
|
if (maxPages <= 0) return
|
||||||
@@ -413,11 +444,20 @@ class CircleChatRepository(
|
|||||||
val messages = historyResponse?.records
|
val messages = historyResponse?.records
|
||||||
updateHistoryState(record.id, historyResponse, 1)
|
updateHistoryState(record.id, historyResponse, 1)
|
||||||
val pageData = buildPageData(position, record, messages)
|
val pageData = buildPageData(position, record, messages)
|
||||||
synchronized(lock) {
|
val wasInserted = synchronized(lock) {
|
||||||
if (cache.get(position) == null) {
|
if (cache.get(position) == null) {
|
||||||
cache.put(position, pageData)
|
cache.put(position, pageData)
|
||||||
}
|
|
||||||
inFlight.remove(position)
|
inFlight.remove(position)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
inFlight.remove(position)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wasInserted) {
|
||||||
|
onPageLoaded?.let { callback ->
|
||||||
|
scope.launch(Dispatchers.Main) { callback(position) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,6 +539,9 @@ class CircleChatRepository(
|
|||||||
updateAvailablePages(available)
|
updateAvailablePages(available)
|
||||||
}
|
}
|
||||||
shouldMarkFetched = data != null
|
shouldMarkFetched = data != null
|
||||||
|
if (!shouldMarkFetched) {
|
||||||
|
hasLoadFailure = true
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
val startPos = (pageNum - 1) * pageSize
|
val startPos = (pageNum - 1) * pageSize
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
@@ -603,7 +646,8 @@ class CircleChatRepository(
|
|||||||
avatarUrl = "",
|
avatarUrl = "",
|
||||||
likeCount = 0,
|
likeCount = 0,
|
||||||
commentCount = 0,
|
commentCount = 0,
|
||||||
liked = false
|
liked = false,
|
||||||
|
messageVersion = -1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class CircleCommentAdapter(
|
|||||||
|
|
||||||
Glide.with(avatarView)
|
Glide.with(avatarView)
|
||||||
.load(comment.userAvatar)
|
.load(comment.userAvatar)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatarView)
|
.into(avatarView)
|
||||||
|
|
||||||
userNameView.text = displayName
|
userNameView.text = displayName
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ class CircleCommentReplyAdapter(
|
|||||||
|
|
||||||
Glide.with(avatarView)
|
Glide.with(avatarView)
|
||||||
.load(comment.userAvatar)
|
.load(comment.userAvatar)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatarView)
|
.into(avatarView)
|
||||||
|
|
||||||
userNameView.text = displayName
|
userNameView.text = displayName
|
||||||
|
|||||||
@@ -158,6 +158,16 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
|
|||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
|
||||||
dialog.window?.setDimAmount(0f)
|
dialog.window?.setDimAmount(0f)
|
||||||
|
|
||||||
|
// 给原始 BlurView 设置固定高度,防止 sheet 缩放时触发 onSizeChanged 导致冻结的内部位图失效
|
||||||
|
commentBlur?.layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
height
|
||||||
|
)
|
||||||
|
// 延迟冻结原始 BlurView:等 sheet 展开动画完成 + 渲染几帧后再冻结
|
||||||
|
commentBlur?.postDelayed({
|
||||||
|
commentBlur?.setBlurAutoUpdate(false)
|
||||||
|
}, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindViews(view: View) {
|
private fun bindViews(view: View) {
|
||||||
@@ -297,6 +307,7 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
|
|||||||
?: return
|
?: return
|
||||||
|
|
||||||
val blurRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 14f else 10f
|
val blurRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 14f else 10f
|
||||||
|
val overlayColor = ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
|
||||||
try {
|
try {
|
||||||
val algorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val algorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
RenderEffectBlur()
|
RenderEffectBlur()
|
||||||
@@ -307,14 +318,10 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
|
|||||||
.setFrameClearDrawable(requireActivity().window.decorView.background)
|
.setFrameClearDrawable(requireActivity().window.decorView.background)
|
||||||
.setBlurRadius(blurRadius)
|
.setBlurRadius(blurRadius)
|
||||||
.setBlurAutoUpdate(true)
|
.setBlurAutoUpdate(true)
|
||||||
.setOverlayColor(
|
.setOverlayColor(overlayColor)
|
||||||
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
|
|
||||||
)
|
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
blurView.visibility = View.GONE
|
blurView.visibility = View.GONE
|
||||||
commentCard.setCardBackgroundColor(
|
commentCard.setCardBackgroundColor(overlayColor)
|
||||||
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,18 +876,6 @@ class CircleCommentSheet : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBlurForIme(imeVisible: Boolean) {
|
private fun updateBlurForIme(imeVisible: Boolean) {
|
||||||
val blurView = commentBlur ?: return
|
// 毛玻璃始终冻结显示,不做切换
|
||||||
|
|
||||||
if (imeVisible) {
|
|
||||||
// 键盘出来:禁用毛玻璃,避免错位
|
|
||||||
blurView.visibility = View.GONE
|
|
||||||
commentCard.setCardBackgroundColor(
|
|
||||||
ContextCompat.getColor(requireContext(), R.color.circle_drawer_blur_overlay)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 键盘收起:恢复毛玻璃
|
|
||||||
blurView.visibility = View.VISIBLE
|
|
||||||
blurView.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ class CircleDrawerMenuAdapter(
|
|||||||
|
|
||||||
Glide.with(itemView.context)
|
Glide.with(itemView.context)
|
||||||
.load(item.avatarUrl)
|
.load(item.avatarUrl)
|
||||||
.placeholder(R.drawable.a123123123)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.a123123123)
|
.error(R.drawable.default_avatar)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
// 选中状态显示不同图标和大小
|
// 选中状态显示不同图标和大小
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.AuthEvent
|
import com.example.myapplication.network.AuthEvent
|
||||||
import com.example.myapplication.network.AuthEventBus
|
import com.example.myapplication.network.AuthEventBus
|
||||||
@@ -41,6 +42,7 @@ import com.example.myapplication.network.NetworkEventBus
|
|||||||
import com.example.myapplication.network.chatMessageRequest
|
import com.example.myapplication.network.chatMessageRequest
|
||||||
import com.example.myapplication.network.aiCompanionLikeRequest
|
import com.example.myapplication.network.aiCompanionLikeRequest
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -56,7 +58,14 @@ import android.os.Build
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.PopupWindow
|
||||||
import com.example.myapplication.network.AiCompanion
|
import com.example.myapplication.network.AiCompanion
|
||||||
|
import com.example.myapplication.network.chatDeleteRequest
|
||||||
import com.example.myapplication.keyboard.AiRolePreferences
|
import com.example.myapplication.keyboard.AiRolePreferences
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -65,6 +74,7 @@ class CircleFragment : Fragment() {
|
|||||||
private lateinit var pageRv: RecyclerView
|
private lateinit var pageRv: RecyclerView
|
||||||
private lateinit var inputOverlay: View
|
private lateinit var inputOverlay: View
|
||||||
private lateinit var noResultOverlay: View
|
private lateinit var noResultOverlay: View
|
||||||
|
private lateinit var noResultSwipeRefresh: SwipeRefreshLayout
|
||||||
private lateinit var imeDismissOverlay: View
|
private lateinit var imeDismissOverlay: View
|
||||||
private lateinit var inputContainerText: View
|
private lateinit var inputContainerText: View
|
||||||
private lateinit var inputContainerVoice: View
|
private lateinit var inputContainerVoice: View
|
||||||
@@ -195,6 +205,13 @@ class CircleFragment : Fragment() {
|
|||||||
pageRv = view.findViewById(R.id.pageRv)
|
pageRv = view.findViewById(R.id.pageRv)
|
||||||
inputOverlay = view.findViewById(R.id.inputOverlay)
|
inputOverlay = view.findViewById(R.id.inputOverlay)
|
||||||
noResultOverlay = view.findViewById(R.id.noResultOverlay)
|
noResultOverlay = view.findViewById(R.id.noResultOverlay)
|
||||||
|
noResultSwipeRefresh = view.findViewById(R.id.noResultSwipeRefresh)
|
||||||
|
noResultSwipeRefresh.setColorSchemeColors(
|
||||||
|
Color.parseColor("#02BEAC"),
|
||||||
|
Color.parseColor("#1B1F1A"),
|
||||||
|
Color.parseColor("#9F9F9F")
|
||||||
|
)
|
||||||
|
noResultSwipeRefresh.setOnRefreshListener { refreshAllCircleData() }
|
||||||
imeDismissOverlay = view.findViewById(R.id.imeDismissOverlay)
|
imeDismissOverlay = view.findViewById(R.id.imeDismissOverlay)
|
||||||
inputContainerText = view.findViewById(R.id.inputContainerText)
|
inputContainerText = view.findViewById(R.id.inputContainerText)
|
||||||
inputContainerVoice = view.findViewById(R.id.inputContainerVoice)
|
inputContainerVoice = view.findViewById(R.id.inputContainerVoice)
|
||||||
@@ -307,8 +324,22 @@ class CircleFragment : Fragment() {
|
|||||||
sharedPool = sharedChatPool,
|
sharedPool = sharedChatPool,
|
||||||
onLikeClick = { position, companionId -> handleLikeClick(position, companionId) },
|
onLikeClick = { position, companionId -> handleLikeClick(position, companionId) },
|
||||||
onCommentClick = { companionId, commentCount -> showCommentSheet(companionId, commentCount) },
|
onCommentClick = { companionId, commentCount -> showCommentSheet(companionId, commentCount) },
|
||||||
onAvatarClick = { companionId -> openCharacterDetails(companionId) }
|
onAvatarClick = { companionId -> openCharacterDetails(companionId) },
|
||||||
|
onMessageLongClick = { message, anchorView, rawX, rawY ->
|
||||||
|
showChatMessagePopup(message, anchorView, rawX, rawY)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
// 后台预加载完成时刷新对应的列表项
|
||||||
|
repository.onPageLoaded = { position ->
|
||||||
|
if (isAdded && view != null) {
|
||||||
|
pageAdapter.notifyItemChanged(position)
|
||||||
|
val page = repository.getPage(position)
|
||||||
|
if (page.companionId > 0) {
|
||||||
|
setNoResultVisible(false)
|
||||||
|
noResultSwipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
parentFragmentManager.setFragmentResultListener(
|
parentFragmentManager.setFragmentResultListener(
|
||||||
RESULT_COMMENT_COUNT_UPDATED,
|
RESULT_COMMENT_COUNT_UPDATED,
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
@@ -418,6 +449,17 @@ class CircleFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听输入法聊天更新事件,刷新对应角色的聊天记录
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
AuthEventBus.events.collect { event ->
|
||||||
|
if (event is AuthEvent.KeyboardChatUpdated) {
|
||||||
|
handleKeyboardChatUpdated(event.companionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -425,6 +467,8 @@ class CircleFragment : Fragment() {
|
|||||||
view?.post { requestOverlayUpdate() }
|
view?.post { requestOverlayUpdate() }
|
||||||
// 检查是否有加载失败的页面,如果有则重新加载
|
// 检查是否有加载失败的页面,如果有则重新加载
|
||||||
retryFailedPages()
|
retryFailedPages()
|
||||||
|
// 消费后台期间积累的脏标记,刷新对应角色聊天记录
|
||||||
|
consumePendingDirtyCompanions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除失败缓存并重新加载
|
// 清除失败缓存并重新加载
|
||||||
@@ -439,8 +483,52 @@ class CircleFragment : Fragment() {
|
|||||||
updateNoResultOverlayFromFirstPage()
|
updateNoResultOverlayFromFirstPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 输入法聊天更新后,刷新对应角色在缓存中的聊天记录
|
||||||
|
private fun handleKeyboardChatUpdated(companionId: Int) {
|
||||||
|
if (!::repository.isInitialized) return
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val updatedPositions = withContext(Dispatchers.IO) {
|
||||||
|
repository.refreshCompanionMessages(companionId)
|
||||||
|
}
|
||||||
|
for (position in updatedPositions) {
|
||||||
|
pageAdapter.notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费后台期间通过 SharedPreferences 持久化的脏标记
|
||||||
|
private fun consumePendingDirtyCompanions() {
|
||||||
|
if (!::repository.isInitialized) return
|
||||||
|
val dirtyIds = AiRolePreferences.consumeDirtyCompanions(requireContext())
|
||||||
|
if (dirtyIds.isEmpty()) return
|
||||||
|
for (companionId in dirtyIds) {
|
||||||
|
handleKeyboardChatUpdated(companionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新:清除全部缓存,重新加载所有接口数据
|
||||||
|
private fun refreshAllCircleData() {
|
||||||
|
repository.invalidateAll()
|
||||||
|
currentPage = RecyclerView.NO_POSITION
|
||||||
|
pageAdapter.notifyDataSetChanged()
|
||||||
|
loadDrawerMenuData()
|
||||||
|
repository.preloadInitialPages()
|
||||||
|
updateNoResultOverlayFromFirstPage()
|
||||||
|
|
||||||
|
// 超时保护:10秒后无论如何停止刷新动画
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
delay(10_000)
|
||||||
|
if (::noResultSwipeRefresh.isInitialized) {
|
||||||
|
noResultSwipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//清理和恢复输入框的高CircleFragment 在生命周期结束时的状态
|
//清理和恢复输入框的高CircleFragment 在生命周期结束时的状态
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
if (::repository.isInitialized) {
|
||||||
|
repository.onPageLoaded = null
|
||||||
|
}
|
||||||
view?.let { root ->
|
view?.let { root ->
|
||||||
ViewCompat.setWindowInsetsAnimationCallback(root, null)
|
ViewCompat.setWindowInsetsAnimationCallback(root, null)
|
||||||
root.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener)
|
root.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener)
|
||||||
@@ -823,7 +911,8 @@ class CircleFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("1314520-Circle", "chatMessage request failed: ${e.message}", e)
|
Log.e("1314520-Circle", "chatMessage request failed: ${e.message}", e)
|
||||||
markBotPlaceholderFailed(page, placeholder, requestFailedText)
|
val removedIndex = repository.removeMessage(page, placeholder.id)
|
||||||
|
notifyMessageRemoved(page, removedIndex)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val data = response.data
|
val data = response.data
|
||||||
@@ -832,7 +921,9 @@ class CircleFragment : Fragment() {
|
|||||||
"1314520-Circle",
|
"1314520-Circle",
|
||||||
"chatMessage failed code=${response.code} message=${response.message}"
|
"chatMessage failed code=${response.code} message=${response.message}"
|
||||||
)
|
)
|
||||||
markBotPlaceholderFailed(page, placeholder, requestFailedText)
|
val removedIndex = repository.removeMessage(page, placeholder.id)
|
||||||
|
notifyMessageRemoved(page, removedIndex)
|
||||||
|
showChatErrorRechargeDialog(response.message)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,6 +953,29 @@ class CircleFragment : Fragment() {
|
|||||||
notifyMessageUpdated(page, placeholder.id)
|
notifyMessageUpdated(page, placeholder.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showChatErrorRechargeDialog(errorMessage: String? = null) {
|
||||||
|
val dialogView = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.dialog_chat_error_recharge, null)
|
||||||
|
val dialog = android.app.AlertDialog.Builder(requireContext())
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
dialog.window?.addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
dialog.window?.setDimAmount(0.5f)
|
||||||
|
if (!errorMessage.isNullOrEmpty()) {
|
||||||
|
dialogView.findViewById<TextView>(R.id.hintText).text = errorMessage
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnClose).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
dialogView.findViewById<View>(R.id.btnRecharge).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
//确定 RecyclerView 中当前选中的页面位置
|
//确定 RecyclerView 中当前选中的页面位置
|
||||||
private fun resolveCurrentPage(): Int {
|
private fun resolveCurrentPage(): Int {
|
||||||
if (currentPage != RecyclerView.NO_POSITION) return currentPage
|
if (currentPage != RecyclerView.NO_POSITION) return currentPage
|
||||||
@@ -897,6 +1011,15 @@ class CircleFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyMessageRemoved(pagePosition: Int, messageIndex: Int) {
|
||||||
|
val holder = pageRv.findViewHolderForAdapterPosition(pagePosition) as? ChatPageViewHolder
|
||||||
|
if (holder != null) {
|
||||||
|
holder.notifyMessageRemoved(messageIndex)
|
||||||
|
} else {
|
||||||
|
pageAdapter.notifyItemChanged(pagePosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleLikeClick(pagePosition: Int, companionId: Int) {
|
private fun handleLikeClick(pagePosition: Int, companionId: Int) {
|
||||||
if (pagePosition == RecyclerView.NO_POSITION || companionId <= 0) return
|
if (pagePosition == RecyclerView.NO_POSITION || companionId <= 0) return
|
||||||
if (!likeInFlight.add(companionId)) return
|
if (!likeInFlight.add(companionId)) return
|
||||||
@@ -979,6 +1102,104 @@ class CircleFragment : Fragment() {
|
|||||||
.show(fm, CircleCommentSheet.TAG)
|
.show(fm, CircleCommentSheet.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showChatMessagePopup(
|
||||||
|
message: ChatMessage,
|
||||||
|
anchorView: View,
|
||||||
|
rawX: Float,
|
||||||
|
rawY: Float
|
||||||
|
) {
|
||||||
|
val ctx = context ?: return
|
||||||
|
val popupView = LayoutInflater.from(ctx)
|
||||||
|
.inflate(R.layout.popup_chat_message_menu, null)
|
||||||
|
|
||||||
|
// AI 消息显示:复制、删除、举报;用户消息显示:复制、删除
|
||||||
|
if (message.isMine) {
|
||||||
|
popupView.findViewById<View>(R.id.menuReport).visibility = View.GONE
|
||||||
|
popupView.findViewById<View>(R.id.divider2).visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
popupView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
val popupW = popupView.measuredWidth
|
||||||
|
val popupH = popupView.measuredHeight
|
||||||
|
|
||||||
|
val popupWindow = PopupWindow(
|
||||||
|
popupView,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
popupWindow.elevation = 8f
|
||||||
|
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
|
||||||
|
// 复制
|
||||||
|
popupView.findViewById<View>(R.id.menuCopy).setOnClickListener {
|
||||||
|
popupWindow.dismiss()
|
||||||
|
val clipboard = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("chat_message", message.text))
|
||||||
|
Toast.makeText(ctx, R.string.chat_copy_success, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
popupView.findViewById<View>(R.id.menuDelete).setOnClickListener {
|
||||||
|
popupWindow.dismiss()
|
||||||
|
handleDeleteMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 举报
|
||||||
|
popupView.findViewById<View>(R.id.menuReport).setOnClickListener {
|
||||||
|
popupWindow.dismiss()
|
||||||
|
val page = resolveCurrentPage()
|
||||||
|
if (page == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||||
|
val companionId = repository.getPage(page).companionId
|
||||||
|
if (companionId <= 0) return@setOnClickListener
|
||||||
|
AuthEventBus.emit(
|
||||||
|
AuthEvent.OpenCirclePage(
|
||||||
|
R.id.circleAiCharacterReportFragment,
|
||||||
|
bundleOf(ARG_COMPANION_ID to companionId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能定位:优先显示在触摸点上方,空间不够则显示在下方
|
||||||
|
val screenWidth = resources.displayMetrics.widthPixels
|
||||||
|
val screenHeight = resources.displayMetrics.heightPixels
|
||||||
|
val touchX = rawX.toInt()
|
||||||
|
val touchY = rawY.toInt()
|
||||||
|
val margin = (8 * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
val x = (touchX - popupW / 2).coerceIn(margin, screenWidth - popupW - margin)
|
||||||
|
val y = if (touchY - popupH - margin > 0) {
|
||||||
|
touchY - popupH - margin
|
||||||
|
} else {
|
||||||
|
touchY + margin
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWindow.showAtLocation(pageRv, Gravity.NO_GRAVITY, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeleteMessage(message: ChatMessage) {
|
||||||
|
val page = resolveCurrentPage()
|
||||||
|
if (page == RecyclerView.NO_POSITION) return
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
RetrofitClient.apiService.chatDelete(chatDeleteRequest(id = message.id.toInt()))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("1314520-Circle", "chatDelete failed: ${e.message}", e)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val removedIndex = repository.removeMessage(page, message.id)
|
||||||
|
if (removedIndex >= 0) {
|
||||||
|
notifyMessageRemoved(page, removedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//同步当前页面的选中状态
|
//同步当前页面的选中状态
|
||||||
private fun syncCurrentPage() {
|
private fun syncCurrentPage() {
|
||||||
val lm = pageRv.layoutManager as? LinearLayoutManager ?: return
|
val lm = pageRv.layoutManager as? LinearLayoutManager ?: return
|
||||||
@@ -1140,6 +1361,9 @@ class CircleFragment : Fragment() {
|
|||||||
blur.visibility = View.GONE
|
blur.visibility = View.GONE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// global 页面可见时不恢复底栏
|
||||||
|
val globalContainer = activity?.findViewById<View>(R.id.global_container)
|
||||||
|
if (globalContainer != null && globalContainer.visibility == View.VISIBLE) return
|
||||||
// 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE)
|
// 恢复时确保底栏可见,不依赖之前保存的状态(可能保存了 GONE)
|
||||||
nav.visibility = View.VISIBLE
|
nav.visibility = View.VISIBLE
|
||||||
prevBottomNavVisibility = null
|
prevBottomNavVisibility = null
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class CirclePageAdapter(
|
|||||||
private val sharedPool: RecyclerView.RecycledViewPool,
|
private val sharedPool: RecyclerView.RecycledViewPool,
|
||||||
private val onLikeClick: ((position: Int, companionId: Int) -> Unit)? = null,
|
private val onLikeClick: ((position: Int, companionId: Int) -> Unit)? = null,
|
||||||
private val onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)? = null,
|
private val onCommentClick: ((companionId: Int, commentCount: Int) -> Unit)? = null,
|
||||||
private val onAvatarClick: ((companionId: Int) -> Unit)? = null
|
private val onAvatarClick: ((companionId: Int) -> Unit)? = null,
|
||||||
|
private val onMessageLongClick: ((message: ChatMessage, anchorView: android.view.View, rawX: Float, rawY: Float) -> Unit)? = null
|
||||||
) : RecyclerView.Adapter<PageViewHolder>() {
|
) : RecyclerView.Adapter<PageViewHolder>() {
|
||||||
|
|
||||||
// 每页固定为屏幕高度,配合 PagerSnapHelper 使用。
|
// 每页固定为屏幕高度,配合 PagerSnapHelper 使用。
|
||||||
@@ -53,7 +54,8 @@ class CirclePageAdapter(
|
|||||||
},
|
},
|
||||||
onLikeClick,
|
onLikeClick,
|
||||||
onCommentClick,
|
onCommentClick,
|
||||||
onAvatarClick
|
onAvatarClick,
|
||||||
|
onMessageLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
|
|||||||
var allowParentInterceptAtTop: (() -> Boolean)? = null
|
var allowParentInterceptAtTop: (() -> Boolean)? = null
|
||||||
var onTopPull: (() -> Unit)? = null
|
var onTopPull: (() -> Unit)? = null
|
||||||
|
|
||||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
|
||||||
when (e.actionMasked) {
|
when (e.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
lastY = e.y
|
lastY = e.y
|
||||||
topPullTriggered = false
|
topPullTriggered = false
|
||||||
|
// 在分发阶段就抢占触摸,防止外层 pageRv 拦截
|
||||||
parent?.requestDisallowInterceptTouchEvent(true)
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
@@ -32,6 +33,14 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
|
|||||||
val canScrollDown = canScrollVertically(1)
|
val canScrollDown = canScrollVertically(1)
|
||||||
val scrollingDown = dy > 0
|
val scrollingDown = dy > 0
|
||||||
|
|
||||||
|
if (!canScrollUp && !canScrollDown) {
|
||||||
|
// 列表内容不足以滚动,放行给父级翻页
|
||||||
|
if (scrollingDown && !topPullTriggered) {
|
||||||
|
topPullTriggered = true
|
||||||
|
onTopPull?.invoke()
|
||||||
|
}
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
} else {
|
||||||
val disallow = if (scrollingDown) {
|
val disallow = if (scrollingDown) {
|
||||||
if (!canScrollUp) {
|
if (!canScrollUp) {
|
||||||
if (!topPullTriggered) {
|
if (!topPullTriggered) {
|
||||||
@@ -42,19 +51,20 @@ class EdgeAwareRecyclerView @JvmOverloads constructor(
|
|||||||
!allowParent
|
!allowParent
|
||||||
} else {
|
} else {
|
||||||
topPullTriggered = false
|
topPullTriggered = false
|
||||||
canScrollUp
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
canScrollDown
|
canScrollDown
|
||||||
}
|
}
|
||||||
parent?.requestDisallowInterceptTouchEvent(disallow)
|
parent?.requestDisallowInterceptTouchEvent(disallow)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
MotionEvent.ACTION_UP,
|
MotionEvent.ACTION_UP,
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
parent?.requestDisallowInterceptTouchEvent(false)
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
topPullTriggered = false
|
topPullTriggered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onTouchEvent(e)
|
return super.dispatchTouchEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ class ThumbsUpAdapter(
|
|||||||
fun bind(item: companionLikedResponse) {
|
fun bind(item: companionLikedResponse) {
|
||||||
Glide.with(ivAvatar)
|
Glide.with(ivAvatar)
|
||||||
.load(item.avatarUrl)
|
.load(item.avatarUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
tvName.text = item.name
|
tvName.text = item.name
|
||||||
@@ -96,8 +96,8 @@ class ChattingAdapter(
|
|||||||
fun bind(item: companionChattedResponse) {
|
fun bind(item: companionChattedResponse) {
|
||||||
Glide.with(ivAvatar)
|
Glide.with(ivAvatar)
|
||||||
.load(item.avatarUrl)
|
.load(item.avatarUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
tvName.text = item.name
|
tvName.text = item.name
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ class CircleCharacterDetailsFragment : Fragment() {
|
|||||||
introTextView.text = data.introText
|
introTextView.text = data.introText
|
||||||
Glide.with(coverImageView)
|
Glide.with(coverImageView)
|
||||||
.load(data.coverImageUrl)
|
.load(data.coverImageUrl)
|
||||||
.placeholder(R.drawable.bg)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.bg)
|
.error(R.drawable.no_search_result)
|
||||||
.transition(DrawableTransitionOptions.withCrossFade(180))
|
.transition(DrawableTransitionOptions.withCrossFade(180))
|
||||||
.listener(object : RequestListener<Drawable> {
|
.listener(object : RequestListener<Drawable> {
|
||||||
override fun onLoadFailed(
|
override fun onLoadFailed(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.myapplication.ui.home
|
package com.example.myapplication.ui.home
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.TransitionDrawable
|
import android.graphics.drawable.TransitionDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -23,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.example.myapplication.ImeGuideActivity
|
import com.example.myapplication.ImeGuideActivity
|
||||||
import com.example.myapplication.ui.common.LoadingOverlay
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
@@ -63,6 +65,7 @@ class HomeFragment : Fragment() {
|
|||||||
private lateinit var tabList2: TextView
|
private lateinit var tabList2: TextView
|
||||||
private lateinit var backgroundImage: ImageView
|
private lateinit var backgroundImage: ImageView
|
||||||
private lateinit var noResultOverlay: View
|
private lateinit var noResultOverlay: View
|
||||||
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
private var lastList1RenderKey: String? = null
|
private var lastList1RenderKey: String? = null
|
||||||
private lateinit var loadingOverlay: LoadingOverlay
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
|
||||||
@@ -265,6 +268,7 @@ class HomeFragment : Fragment() {
|
|||||||
viewPager = view.findViewById(R.id.viewPager)
|
viewPager = view.findViewById(R.id.viewPager)
|
||||||
viewPager.isSaveEnabled = false
|
viewPager.isSaveEnabled = false
|
||||||
viewPager.offscreenPageLimit = 2
|
viewPager.offscreenPageLimit = 2
|
||||||
|
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
|
||||||
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
|
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
|
||||||
noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay)
|
noResultOverlay = bottomSheet.findViewById(R.id.noResultOverlay)
|
||||||
|
|
||||||
@@ -296,6 +300,20 @@ class HomeFragment : Fragment() {
|
|||||||
// ✅ setupViewPager 只初始化一次
|
// ✅ setupViewPager 只初始化一次
|
||||||
setupViewPagerOnce()
|
setupViewPagerOnce()
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
swipeRefreshLayout.setColorSchemeColors(
|
||||||
|
Color.parseColor("#02BEAC"),
|
||||||
|
Color.parseColor("#1B1F1A"),
|
||||||
|
Color.parseColor("#9F9F9F")
|
||||||
|
)
|
||||||
|
swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
refreshAllData { swipeRefreshLayout.isRefreshing = false }
|
||||||
|
}
|
||||||
|
swipeRefreshLayout.setOnChildScrollUpCallback { _, _ ->
|
||||||
|
// BottomSheet 未折叠时,让 BottomSheet 自己处理拖拽,不触发刷新
|
||||||
|
bottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
|
||||||
// 标签 UI 初始为空
|
// 标签 UI 初始为空
|
||||||
setupTags()
|
setupTags()
|
||||||
|
|
||||||
@@ -708,25 +726,29 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshHomeAfterNetwork() {
|
private fun refreshHomeAfterNetwork() {
|
||||||
|
loadingOverlay.show()
|
||||||
|
refreshAllData { loadingOverlay.hide() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshAllData(onFinish: () -> Unit) {
|
||||||
networkRefreshJob?.cancel()
|
networkRefreshJob?.cancel()
|
||||||
networkRefreshJob = viewLifecycleOwner.lifecycleScope.launch {
|
networkRefreshJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||||
preloadJob?.cancel()
|
preloadJob?.cancel()
|
||||||
loadingOverlay.show()
|
|
||||||
try {
|
try {
|
||||||
list1Loaded = false
|
// 先获取所有数据,不动 UI
|
||||||
setNoResultVisible(false)
|
val newPersonaList = fetchAllPersonaList()
|
||||||
val list = fetchAllPersonaList()
|
|
||||||
if (!isAdded) return@launch
|
if (!isAdded) return@launch
|
||||||
allPersonaCache = list
|
val response = RetrofitClient.apiService.tagList()
|
||||||
|
if (!isAdded) return@launch
|
||||||
|
|
||||||
|
// 数据全部返回后,一次性更新 UI
|
||||||
|
allPersonaCache = newPersonaList
|
||||||
list1Loaded = true
|
list1Loaded = true
|
||||||
lastList1RenderKey = null
|
lastList1RenderKey = null
|
||||||
personaCache.clear()
|
personaCache.clear()
|
||||||
notifyPageChangedOnMain(0)
|
notifyPageChangedOnMain(0)
|
||||||
updateNoResultOverlay(0)
|
updateNoResultOverlay(0)
|
||||||
|
|
||||||
val response = RetrofitClient.apiService.tagList()
|
|
||||||
if (!isAdded) return@launch
|
|
||||||
|
|
||||||
tags.clear()
|
tags.clear()
|
||||||
response.data?.let { networkTags ->
|
response.data?.let { networkTags ->
|
||||||
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
|
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
|
||||||
@@ -735,9 +757,9 @@ class HomeFragment : Fragment() {
|
|||||||
setupTags()
|
setupTags()
|
||||||
startPreloadAllTagsFillCacheOnly()
|
startPreloadAllTagsFillCacheOnly()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("HomeFragment", "refresh after network fail", e)
|
Log.e("HomeFragment", "refresh data fail", e)
|
||||||
} finally {
|
} finally {
|
||||||
loadingOverlay.hide()
|
onFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -952,7 +974,11 @@ class HomeFragment : Fragment() {
|
|||||||
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
|
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
|
||||||
|
|
||||||
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
||||||
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
|
com.bumptech.glide.Glide.with(iv)
|
||||||
|
.load(p.avatarUrl)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
|
.into(iv)
|
||||||
|
|
||||||
// ---------------- add 按钮(失败回滚 + 防连点) ----------------
|
// ---------------- add 按钮(失败回滚 + 防连点) ----------------
|
||||||
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
|
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
|
||||||
@@ -1046,7 +1072,11 @@ class HomeFragment : Fragment() {
|
|||||||
addBtn.isVisible = true
|
addBtn.isVisible = true
|
||||||
|
|
||||||
name.text = item.characterName ?: ""
|
name.text = item.characterName ?: ""
|
||||||
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
|
com.bumptech.glide.Glide.with(avatar)
|
||||||
|
.load(item.avatarUrl)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
|
.into(avatar)
|
||||||
|
|
||||||
// ✅ 记录“原始背景/原始icon”,用于 added=false 时恢复
|
// ✅ 记录“原始背景/原始icon”,用于 added=false 时恢复
|
||||||
val originBg = addBtn.background
|
val originBg = addBtn.background
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ class PersonaAdapter(
|
|||||||
|
|
||||||
Glide.with(itemView.context)
|
Glide.with(itemView.context)
|
||||||
.load(item.avatarUrl)
|
.load(item.avatarUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
val isAdded = item.added
|
val isAdded = item.added
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ class PersonaDetailDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.load(data.avatarUrl)
|
.load(data.avatarUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
btnAdd.setOnClickListener {
|
btnAdd.setOnClickListener {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.example.myapplication.network.RetrofitClient
|
|||||||
import com.example.myapplication.network.SubjectTag
|
import com.example.myapplication.network.SubjectTag
|
||||||
import com.example.myapplication.network.themeDetail
|
import com.example.myapplication.network.themeDetail
|
||||||
import com.example.myapplication.network.purchaseThemeRequest
|
import com.example.myapplication.network.purchaseThemeRequest
|
||||||
|
import com.example.myapplication.network.restoreThemeRequest
|
||||||
import com.example.myapplication.ui.shop.ThemeCardAdapter
|
import com.example.myapplication.ui.shop.ThemeCardAdapter
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -38,6 +39,7 @@ import com.example.myapplication.ui.shop.ShopEventBus
|
|||||||
import com.example.myapplication.network.BehaviorReporter
|
import com.example.myapplication.network.BehaviorReporter
|
||||||
import com.example.myapplication.network.AuthEvent
|
import com.example.myapplication.network.AuthEvent
|
||||||
import com.example.myapplication.network.AuthEventBus
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
|
|
||||||
class KeyboardDetailFragment : Fragment() {
|
class KeyboardDetailFragment : Fragment() {
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
private lateinit var enabledButtonText: TextView
|
private lateinit var enabledButtonText: TextView
|
||||||
private lateinit var progressBar: android.widget.ProgressBar
|
private lateinit var progressBar: android.widget.ProgressBar
|
||||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
private var themeDetailResp: themeDetail? = null
|
private var themeDetailResp: themeDetail? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -77,21 +80,22 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
enabledButtonText = view.findViewById<TextView>(R.id.enabledButtonText)
|
enabledButtonText = view.findViewById<TextView>(R.id.enabledButtonText)
|
||||||
progressBar = view.findViewById<android.widget.ProgressBar>(R.id.progressBar)
|
progressBar = view.findViewById<android.widget.ProgressBar>(R.id.progressBar)
|
||||||
swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
|
swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
|
||||||
|
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
|
||||||
|
|
||||||
// 设置按钮始终防止事件穿透的触摸监听器
|
// 设置按钮始终防止事件穿透的触摸监听<EFBFBD>?
|
||||||
enabledButton.setOnTouchListener { _, event ->
|
enabledButton.setOnTouchListener { _, event ->
|
||||||
// 如果按钮被禁用,消耗所有触摸事件防止穿透
|
// 如果按钮被禁用,消耗所有触摸事件防止穿<EFBFBD>?
|
||||||
if (!enabledButton.isEnabled) {
|
if (!enabledButton.isEnabled) {
|
||||||
return@setOnTouchListener true
|
return@setOnTouchListener true
|
||||||
}
|
}
|
||||||
// 如果按钮启用,不消耗事件,让按钮正常处理点击
|
// 如果按钮启用,不消耗事件,让按钮正常处理点<EFBFBD>?
|
||||||
return@setOnTouchListener false
|
return@setOnTouchListener false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化RecyclerView
|
// 初始化RecyclerView
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
|
||||||
// 设置下拉刷新监听器
|
// 设置下拉刷新监听<EFBFBD>?
|
||||||
swipeRefreshLayout.setOnRefreshListener {
|
swipeRefreshLayout.setOnRefreshListener {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
@@ -109,7 +113,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
//充值主题
|
//充值主<EFBFBD>?
|
||||||
rechargeButton.setOnClickListener {
|
rechargeButton.setOnClickListener {
|
||||||
showPurchaseConfirmationDialog(themeId)
|
showPurchaseConfirmationDialog(themeId)
|
||||||
}
|
}
|
||||||
@@ -136,13 +140,18 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
// 下拉刷新时已有自带动画,仅首次加载显<E8BDBD>?overlay
|
||||||
|
if (!swipeRefreshLayout.isRefreshing) {
|
||||||
|
loadingOverlay.show()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
themeDetailResp = getThemeDetail(themeId)?.data
|
themeDetailResp = getThemeDetail(themeId)?.data
|
||||||
val recommendThemeListResp = getrecommendThemeList()?.data
|
val recommendThemeListResp = getrecommendThemeList()?.data
|
||||||
|
|
||||||
Glide.with(requireView().context)
|
Glide.with(requireView().context)
|
||||||
.load(themeDetailResp?.themePreviewImageUrl)
|
.load(themeDetailResp?.themePreviewImageUrl)
|
||||||
.placeholder(R.drawable.bg)
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(shapeableImageView)
|
.into(shapeableImageView)
|
||||||
|
|
||||||
tvKeyboardName.text = themeDetailResp?.themeName
|
tvKeyboardName.text = themeDetailResp?.themeName
|
||||||
@@ -162,7 +171,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
renderTags(tags)
|
renderTags(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染推荐主题列表(剔除当前themeId)
|
// 渲染推荐主题列表(剔除当前themeId<EFBFBD>?
|
||||||
recommendThemeListResp?.let { themes ->
|
recommendThemeListResp?.let { themes ->
|
||||||
val filteredThemes = themes.filter { it.id != themeId }
|
val filteredThemes = themes.filter { it.id != themeId }
|
||||||
themeCardAdapter.submitList(filteredThemes)
|
themeCardAdapter.submitList(filteredThemes)
|
||||||
@@ -173,6 +182,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
} finally {
|
} finally {
|
||||||
// 停止刷新动画
|
// 停止刷新动画
|
||||||
swipeRefreshLayout.isRefreshing = false
|
swipeRefreshLayout.isRefreshing = false
|
||||||
|
loadingOverlay.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,9 +193,9 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
if (tags.isEmpty()) return
|
if (tags.isEmpty()) return
|
||||||
|
|
||||||
val context = layoutTagsContainer.context
|
val context = layoutTagsContainer.context
|
||||||
val tagsPerRow = 5 // 每行固定显示5个标签
|
val tagsPerRow = 5 // 每行固定显示5个标<EFBFBD>?
|
||||||
|
|
||||||
// 将标签分组,每行6个
|
// 将标签分组,每行6<EFBFBD>?
|
||||||
val rows = tags.chunked(tagsPerRow)
|
val rows = tags.chunked(tagsPerRow)
|
||||||
|
|
||||||
rows.forEach { rowTags ->
|
rows.forEach { rowTags ->
|
||||||
@@ -212,7 +222,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
|
|
||||||
// 设置内边距:左右12dp,上下5dp
|
// 设置内边距:左右12dp,上<EFBFBD>?dp
|
||||||
val horizontalPadding = TypedValue.applyDimension(
|
val horizontalPadding = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 12f, resources.displayMetrics
|
TypedValue.COMPLEX_UNIT_DIP, 12f, resources.displayMetrics
|
||||||
).toInt()
|
).toInt()
|
||||||
@@ -221,14 +231,14 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
).toInt()
|
).toInt()
|
||||||
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
|
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
|
||||||
|
|
||||||
// 设置背景(50dp圆角)
|
// 设置背景<EFBFBD>?0dp圆角<EFBFBD>?
|
||||||
background = ContextCompat.getDrawable(context, R.drawable.tag_background)?.mutate()
|
background = ContextCompat.getDrawable(context, R.drawable.tag_background)?.mutate()
|
||||||
background?.setTint(android.graphics.Color.parseColor(tag.color))
|
background?.setTint(android.graphics.Color.parseColor(tag.color))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用权重布局,让标签自适应间距
|
// 使用权重布局,让标签自适应间距
|
||||||
val layoutParams = LinearLayout.LayoutParams(
|
val layoutParams = LinearLayout.LayoutParams(
|
||||||
0, // 宽度设为0,使用权重
|
0, // 宽度设为0,使用权<EFBFBD>?
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
tagWeight
|
tagWeight
|
||||||
).apply {
|
).apply {
|
||||||
@@ -241,13 +251,13 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
rowLayout.addView(tagView, layoutParams)
|
rowLayout.addView(tagView, layoutParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前行标签数量不足6个,添加空View填充剩余空间
|
// 如果当前行标签数量不<EFBFBD>?个,添加空View填充剩余空间
|
||||||
val remainingTags = tagsPerRow - rowTags.size
|
val remainingTags = tagsPerRow - rowTags.size
|
||||||
if (remainingTags > 0) {
|
if (remainingTags > 0) {
|
||||||
repeat(remainingTags) {
|
repeat(remainingTags) {
|
||||||
val emptyView = View(context)
|
val emptyView = View(context)
|
||||||
val layoutParams = LinearLayout.LayoutParams(
|
val layoutParams = LinearLayout.LayoutParams(
|
||||||
0, // 宽度设为0,使用权重
|
0, // 宽度设为0,使用权<EFBFBD>?
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
tagWeight
|
tagWeight
|
||||||
).apply {
|
).apply {
|
||||||
@@ -274,7 +284,8 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
|
|
||||||
private suspend fun setrestoreTheme(themeId: Int): ApiResponse<Unit>? {
|
private suspend fun setrestoreTheme(themeId: Int): ApiResponse<Unit>? {
|
||||||
return try {
|
return try {
|
||||||
RetrofitClient.apiService.restoreTheme(themeId)
|
val restoreThemeRequest = restoreThemeRequest(themeId = themeId)
|
||||||
|
RetrofitClient.apiService.restoreTheme(restoreThemeRequest)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KeyboardDetailFragment", "恢复已删除的主题失败", e)
|
Log.e("KeyboardDetailFragment", "恢复已删除的主题失败", e)
|
||||||
null
|
null
|
||||||
@@ -294,7 +305,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
val purchaseThemeRequest = purchaseThemeRequest(themeId = purchaseId)
|
val purchaseThemeRequest = purchaseThemeRequest(themeId = purchaseId)
|
||||||
val response = RetrofitClient.apiService.purchaseTheme(purchaseThemeRequest)
|
val response = RetrofitClient.apiService.purchaseTheme(purchaseThemeRequest)
|
||||||
|
|
||||||
// 购买成功后触发刷新(成功状态码为0)
|
// 购买成功后触发刷新(成功状态码<EFBFBD>?<3F>?
|
||||||
if (response?.code == 0) {
|
if (response?.code == 0) {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
@@ -308,7 +319,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
|
|
||||||
//=============================RecyclerView===================================
|
//=============================RecyclerView===================================
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
// 设置GridLayoutManager,每行显示2个item
|
// 设置GridLayoutManager,每行显<EFBFBD>?个item
|
||||||
val layoutManager = GridLayoutManager(requireContext(), 2)
|
val layoutManager = GridLayoutManager(requireContext(), 2)
|
||||||
recyclerRecommendList.layoutManager = layoutManager
|
recyclerRecommendList.layoutManager = layoutManager
|
||||||
|
|
||||||
@@ -325,7 +336,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
val dialog = Dialog(requireContext())
|
val dialog = Dialog(requireContext())
|
||||||
dialog.setContentView(R.layout.dialog_purchase_confirmation)
|
dialog.setContentView(R.layout.dialog_purchase_confirmation)
|
||||||
|
|
||||||
// 设置弹窗属性
|
// 设置弹窗属<EFBFBD>?
|
||||||
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
dialog.window?.setLayout(
|
dialog.window?.setLayout(
|
||||||
android.view.WindowManager.LayoutParams.WRAP_CONTENT,
|
android.view.WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
@@ -357,7 +368,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 URL 中提取 zip 包名(去掉路径和查询参数,去掉 .zip 扩展名)
|
* <EFBFBD>?URL 中提<EFBFBD>?zip 包名(去掉路径和查询参数,去<EFBFBD>?.zip 扩展名)
|
||||||
*/
|
*/
|
||||||
private fun extractZipNameFromUrl(url: String): String {
|
private fun extractZipNameFromUrl(url: String): String {
|
||||||
// 提取文件名部分(去掉路径和查询参数)
|
// 提取文件名部分(去掉路径和查询参数)
|
||||||
@@ -367,7 +378,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
url.substring(url.lastIndexOf('/') + 1)
|
url.substring(url.lastIndexOf('/') + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去掉 .zip 扩展名
|
// 去掉 .zip 扩展<EFBFBD>?
|
||||||
return if (fileName.endsWith(".zip")) {
|
return if (fileName.endsWith(".zip")) {
|
||||||
fileName.substring(0, fileName.length - 4)
|
fileName.substring(0, fileName.length - 4)
|
||||||
} else {
|
} else {
|
||||||
@@ -425,7 +436,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
enabledButton.isEnabled = false
|
enabledButton.isEnabled = false
|
||||||
enabledButton.isClickable = false
|
enabledButton.isClickable = false
|
||||||
enabledButton.isFocusable = false
|
enabledButton.isFocusable = false
|
||||||
// 防止点击事件穿透 - 消耗所有触摸事件
|
// 防止点击事件穿<EFBFBD>?- 消耗所有触摸事<EFBFBD>?
|
||||||
enabledButton.setOnTouchListener { _, _ -> true }
|
enabledButton.setOnTouchListener { _, _ -> true }
|
||||||
// 添加视觉上的禁用效果
|
// 添加视觉上的禁用效果
|
||||||
enabledButton.alpha = 0.6f
|
enabledButton.alpha = 0.6f
|
||||||
@@ -446,7 +457,7 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
enabledButton.isFocusable = true
|
enabledButton.isFocusable = true
|
||||||
// 移除触摸监听器,恢复正常触摸事件处理
|
// 移除触摸监听器,恢复正常触摸事件处理
|
||||||
enabledButton.setOnTouchListener(null)
|
enabledButton.setOnTouchListener(null)
|
||||||
// 恢复正常的视觉效果
|
// 恢复正常的视觉效<EFBFBD>?
|
||||||
enabledButton.alpha = 1.0f
|
enabledButton.alpha = 1.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,3 +475,5 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
Log.e("1314520-KeyboardDetailFragment", "Error: $message")
|
Log.e("1314520-KeyboardDetailFragment", "Error: $message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -21,6 +22,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.AuthEvent
|
import com.example.myapplication.network.AuthEvent
|
||||||
@@ -48,6 +50,7 @@ class MineFragment : Fragment() {
|
|||||||
private lateinit var avatar: CircleImageView
|
private lateinit var avatar: CircleImageView
|
||||||
private lateinit var share: LinearLayout
|
private lateinit var share: LinearLayout
|
||||||
private lateinit var loadingOverlay: LoadingOverlay
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
|
|
||||||
private var loadUserJob: Job? = null
|
private var loadUserJob: Job? = null
|
||||||
|
|
||||||
@@ -77,6 +80,15 @@ class MineFragment : Fragment() {
|
|||||||
avatar = view.findViewById(R.id.avatar)
|
avatar = view.findViewById(R.id.avatar)
|
||||||
share = view.findViewById(R.id.click_Share)
|
share = view.findViewById(R.id.click_Share)
|
||||||
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
|
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
|
||||||
|
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
|
||||||
|
swipeRefreshLayout.setColorSchemeColors(
|
||||||
|
Color.parseColor("#02BEAC"),
|
||||||
|
Color.parseColor("#1B1F1A"),
|
||||||
|
Color.parseColor("#9F9F9F")
|
||||||
|
)
|
||||||
|
swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
refreshUser(force = true, showToast = true)
|
||||||
|
}
|
||||||
|
|
||||||
// 1) 先用本地缓存秒出首屏
|
// 1) 先用本地缓存秒出首屏
|
||||||
renderFromCache()
|
renderFromCache()
|
||||||
@@ -178,6 +190,30 @@ class MineFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.findViewById<LinearLayout>(R.id.click_Email).setOnClickListener {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.apiService.delUserCharacter()
|
||||||
|
val email = response.data
|
||||||
|
if (!isAdded) return@launch
|
||||||
|
if (email.isNullOrBlank()) {
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("email", email))
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.email_copy_success), Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is kotlinx.coroutines.CancellationException) return@launch
|
||||||
|
Log.e(TAG, "customerMail failed", e)
|
||||||
|
if (isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
|
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
|
||||||
@@ -231,6 +267,8 @@ class MineFragment : Fragment() {
|
|||||||
cached?.avatarUrl?.let { url ->
|
cached?.avatarUrl?.let { url ->
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatar)
|
.into(avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,6 +280,7 @@ class MineFragment : Fragment() {
|
|||||||
*/
|
*/
|
||||||
private fun refreshUser(force: Boolean, showToast: Boolean = false) {
|
private fun refreshUser(force: Boolean, showToast: Boolean = false) {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
|
swipeRefreshLayout.isRefreshing = false
|
||||||
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.not_logged_in_toast), Toast.LENGTH_SHORT).show()
|
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.not_logged_in_toast), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -264,6 +303,8 @@ class MineFragment : Fragment() {
|
|||||||
u?.avatarUrl?.let { url ->
|
u?.avatarUrl?.let { url ->
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatar)
|
.into(avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +315,8 @@ class MineFragment : Fragment() {
|
|||||||
if (e is kotlinx.coroutines.CancellationException) return@launch
|
if (e is kotlinx.coroutines.CancellationException) return@launch
|
||||||
Log.e(TAG, "getUser failed", e)
|
Log.e(TAG, "getUser failed", e)
|
||||||
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
|
if (showToast && isAdded) Toast.makeText(requireContext(), getString(R.string.refresh_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +337,8 @@ class MineFragment : Fragment() {
|
|||||||
renderVip(false, null)
|
renderVip(false, null)
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.load(R.drawable.default_avatar)
|
.load(R.drawable.default_avatar)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatar)
|
.into(avatar)
|
||||||
|
|
||||||
// 触发登出事件,让MainActivity打开登录页面
|
// 触发登出事件,让MainActivity打开登录页面
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ class PersonalSettings : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.load(u.avatarUrl)
|
.load(u.avatarUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatar)
|
.into(avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +287,8 @@ class PersonalSettings : BottomSheetDialogFragment() {
|
|||||||
private fun handleImageResult(uri: Uri) {
|
private fun handleImageResult(uri: Uri) {
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(avatar)
|
.into(avatar)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
package com.example.myapplication.ui.recharge
|
package com.example.myapplication.ui.recharge
|
||||||
|
|
||||||
import android.graphics.Paint
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.AuthEvent
|
import com.example.myapplication.network.AuthEvent
|
||||||
import com.example.myapplication.network.AuthEventBus
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class RechargeFragment : Fragment() {
|
class RechargeFragment : Fragment() {
|
||||||
|
|
||||||
|
private var currentTab = 0
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -24,15 +35,129 @@ class RechargeFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// 找到旧价格 TextView
|
// 收起输入法,避免从聊天页跳转过来时键盘残留
|
||||||
val tvOldPrice = view.findViewById<TextView>(R.id.tvOldPrice)
|
val imm = requireContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
|
||||||
// 旧价格加删除线
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
tvOldPrice.paintFlags = tvOldPrice.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
|
||||||
|
|
||||||
// 设置关闭按钮点击事件
|
val appBar = view.findViewById<AppBarLayout>(R.id.appBarLayout)
|
||||||
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
|
val headerContainer = view.findViewById<LinearLayout>(R.id.headerContainer)
|
||||||
|
val stickyTitleBar = view.findViewById<FrameLayout>(R.id.stickyTitleBar)
|
||||||
|
val stickyTitle = view.findViewById<TextView>(R.id.stickyTitle)
|
||||||
|
val ivStickyClose = view.findViewById<ImageView>(R.id.iv_sticky_close)
|
||||||
|
val ivClose = view.findViewById<ImageView>(R.id.iv_close)
|
||||||
|
val viewPager = view.findViewById<ViewPager2>(R.id.viewPager)
|
||||||
|
val tabVip = view.findViewById<FrameLayout>(R.id.tabVip)
|
||||||
|
val tabSvip = view.findViewById<FrameLayout>(R.id.tabSvip)
|
||||||
|
val ivVipTab = view.findViewById<ImageView>(R.id.ivVipTab)
|
||||||
|
val ivSvipTab = view.findViewById<ImageView>(R.id.ivSvipTab)
|
||||||
|
|
||||||
|
val titleBarMaxHeight = resources.getDimensionPixelSize(R.dimen.sw_46dp)
|
||||||
|
|
||||||
|
// 显式计算 headerContainer 高度
|
||||||
|
// 原始布局流式位置:bg(224) + close(-198+46) + vip(269) + equity(-198+391) + collapse(-380) + spacing(16) = 170dp
|
||||||
|
// LinearLayout.Math.max 阻止负 margin 缩减测量高度,因此必须在代码中显式设置
|
||||||
|
val headerHeight =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.sw_224dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen._sw_198dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.sw_46dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.sw_269dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen._sw_198dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.sw_391dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen._sw_380dp) +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.sw_16dp)
|
||||||
|
headerContainer.layoutParams.height = headerHeight
|
||||||
|
|
||||||
|
// 关闭按钮(头部原位 + 标题栏共用同一逻辑)
|
||||||
|
val closeAction = View.OnClickListener {
|
||||||
AuthEventBus.emit(AuthEvent.UserUpdated)
|
AuthEventBus.emit(AuthEvent.UserUpdated)
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
ivClose.setOnClickListener(closeAction)
|
||||||
|
ivStickyClose.setOnClickListener(closeAction)
|
||||||
|
|
||||||
|
// ViewPager2 适配器
|
||||||
|
val pageLayouts = intArrayOf(R.layout.page_recharge_vip, R.layout.page_recharge_svip)
|
||||||
|
viewPager.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val itemView = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(pageLayouts[viewType], parent, false)
|
||||||
|
return object : RecyclerView.ViewHolder(itemView) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
|
||||||
|
override fun getItemViewType(position: Int) = position
|
||||||
|
override fun getItemCount() = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面切换回调
|
||||||
|
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
switchTab(position, ivVipTab, ivSvipTab)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tab 点击
|
||||||
|
tabVip.setOnClickListener { viewPager.setCurrentItem(0, true) }
|
||||||
|
tabSvip.setOnClickListener { viewPager.setCurrentItem(1, true) }
|
||||||
|
|
||||||
|
// AppBarLayout 滚动监听:渐进展开标题栏(仿 Shop 页面)
|
||||||
|
appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||||
|
val totalRange = appBarLayout.totalScrollRange
|
||||||
|
if (totalRange == 0) return@OnOffsetChangedListener
|
||||||
|
|
||||||
|
val ratio = abs(verticalOffset).toFloat() / totalRange
|
||||||
|
|
||||||
|
// 最后 20% 渐进展开标题栏
|
||||||
|
val progress = ((ratio - 0.8f) / 0.2f).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
// 标题栏高度 0 → titleBarMaxHeight,防抖优化
|
||||||
|
val newHeight = (progress * titleBarMaxHeight).toInt()
|
||||||
|
val lp = stickyTitleBar.layoutParams
|
||||||
|
if (lp.height != newHeight) {
|
||||||
|
lp.height = newHeight
|
||||||
|
stickyTitleBar.layoutParams = lp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题文字和关闭按钮渐显
|
||||||
|
stickyTitle.alpha = progress
|
||||||
|
ivStickyClose.alpha = progress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchTab(position: Int, ivVipTab: ImageView, ivSvipTab: ImageView) {
|
||||||
|
if (currentTab == position) return
|
||||||
|
currentTab = position
|
||||||
|
|
||||||
|
val selectedView = if (position == 0) ivVipTab else ivSvipTab
|
||||||
|
val unselectedView = if (position == 0) ivSvipTab else ivVipTab
|
||||||
|
|
||||||
|
AnimatorSet().apply {
|
||||||
|
playTogether(
|
||||||
|
ObjectAnimator.ofFloat(selectedView, "scaleX", 0.85f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(selectedView, "scaleY", 0.85f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(selectedView, "alpha", 0.7f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(unselectedView, "scaleX", 1f, 0.85f),
|
||||||
|
ObjectAnimator.ofFloat(unselectedView, "scaleY", 1f, 0.85f),
|
||||||
|
ObjectAnimator.ofFloat(unselectedView, "alpha", 1f, 0.7f)
|
||||||
|
)
|
||||||
|
duration = 250
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position == 0) {
|
||||||
|
ivVipTab.setImageResource(R.drawable.vip_select)
|
||||||
|
ivSvipTab.setImageResource(R.drawable.svip_not_selected)
|
||||||
|
} else {
|
||||||
|
ivVipTab.setImageResource(R.drawable.vip_not_selected)
|
||||||
|
ivSvipTab.setImageResource(R.drawable.svip_select)
|
||||||
|
}
|
||||||
|
|
||||||
|
unselectedView.scaleX = 1f
|
||||||
|
unselectedView.scaleY = 1f
|
||||||
|
unselectedView.alpha = 1f
|
||||||
|
selectedView.scaleX = 1f
|
||||||
|
selectedView.scaleY = 1f
|
||||||
|
selectedView.alpha = 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,10 +189,11 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
|
|||||||
|
|
||||||
themeListLoaded = false
|
themeListLoaded = false
|
||||||
|
|
||||||
setNoResultVisible(false)
|
|
||||||
|
|
||||||
val newThemes = getThemeList()?.data ?: emptyList()
|
val newThemes = getThemeList()?.data ?: emptyList()
|
||||||
if (!isAdded) return@launch
|
if (!isAdded) return@launch
|
||||||
|
|
||||||
|
setNoResultVisible(false)
|
||||||
|
|
||||||
if (newThemes != tabTitles) {
|
if (newThemes != tabTitles) {
|
||||||
tabTitles = newThemes
|
tabTitles = newThemes
|
||||||
styleIds = tabTitles.map { it.id }
|
styleIds = tabTitles.map { it.id }
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class ThemeCardAdapter : ListAdapter<themeStyle, ThemeCardAdapter.ThemeCardViewH
|
|||||||
// 加载主题图片
|
// 加载主题图片
|
||||||
Glide.with(itemView.context)
|
Glide.with(itemView.context)
|
||||||
.load(theme.themePreviewImageUrl)
|
.load(theme.themePreviewImageUrl)
|
||||||
.placeholder(R.drawable.bg)
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(themeImage)
|
.into(themeImage)
|
||||||
|
|
||||||
// 设置主题名称
|
// 设置主题名称
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class MySkinAdapter(
|
|||||||
|
|
||||||
Glide.with(holder.itemView)
|
Glide.with(holder.itemView)
|
||||||
.load(item.themePreviewImageUrl)
|
.load(item.themePreviewImageUrl)
|
||||||
.placeholder(R.drawable.default_avatar)
|
.placeholder(R.drawable.component_loading)
|
||||||
|
.error(R.drawable.no_search_result)
|
||||||
.into(holder.ivPreview)
|
.into(holder.ivPreview)
|
||||||
|
|
||||||
val selected = selectedIds.contains(item.id)
|
val selected = selectedIds.contains(item.id)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.example.myapplication.network.FileDownloader
|
import com.example.myapplication.network.FileDownloader
|
||||||
import com.example.myapplication.network.RetrofitClient
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.network.restoreThemeRequest
|
||||||
import com.example.myapplication.theme.ThemeManager
|
import com.example.myapplication.theme.ThemeManager
|
||||||
import com.example.myapplication.utils.unzipThemeSmart
|
import com.example.myapplication.utils.unzipThemeSmart
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -68,7 +69,8 @@ class ThemeDownloadWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val restoreResp = RetrofitClient.apiService.restoreTheme(themeId)
|
val restoreReq = restoreThemeRequest(themeId = themeId)
|
||||||
|
val restoreResp = RetrofitClient.apiService.restoreTheme(restoreReq)
|
||||||
if (restoreResp.code != 0) {
|
if (restoreResp.code != 0) {
|
||||||
Log.e(TAG, "restoreTheme failed: code=${restoreResp.code} msg=${restoreResp.message}")
|
Log.e(TAG, "restoreTheme failed: code=${restoreResp.code} msg=${restoreResp.message}")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
|
|||||||
|
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:layout_height="match_parent"
|
||||||
android:background="#F6F7FB"
|
android:background="#F6F7FB"
|
||||||
tools:context=".ui.home.HomeFragment">
|
tools:context=".ui.home.HomeFragment">
|
||||||
<!-- 内容 -->
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fillViewport="true"
|
|
||||||
android:overScrollMode="never">
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:background="#F6F7FB"
|
||||||
|
android:elevation="0dp"
|
||||||
|
android:stateListAnimator="@null"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<!-- 可滚动头部区域:随滚动消失 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/headerContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||||
|
|
||||||
<!-- recharge背景 -->
|
<!-- recharge背景 -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -24,8 +35,10 @@
|
|||||||
android:src="@drawable/recharge_bg"
|
android:src="@drawable/recharge_bg"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮 -->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
android:id="@+id/closeContainer"
|
||||||
android:layout_width="@dimen/sw_46dp"
|
android:layout_width="@dimen/sw_46dp"
|
||||||
android:layout_height="@dimen/sw_46dp"
|
android:layout_height="@dimen/sw_46dp"
|
||||||
android:layout_marginTop="@dimen/_sw_198dp">
|
android:layout_marginTop="@dimen/_sw_198dp">
|
||||||
@@ -38,237 +51,156 @@
|
|||||||
android:rotation="180"
|
android:rotation="180"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- vip -->
|
<!-- vip -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_close"
|
|
||||||
android:layout_width="@dimen/sw_321dp"
|
android:layout_width="@dimen/sw_321dp"
|
||||||
android:layout_height="@dimen/sw_269dp"
|
android:layout_height="@dimen/sw_269dp"
|
||||||
android:layout_marginStart="@dimen/sw_24dp"
|
android:layout_marginStart="@dimen/sw_24dp"
|
||||||
android:elevation="@dimen/sw_1dp"
|
android:elevation="@dimen/sw_1dp"
|
||||||
android:src="@drawable/vip_two" />
|
android:src="@drawable/vip_two" />
|
||||||
|
|
||||||
<!-- 权益背景 -->
|
<!-- 权益背景 -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_close"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/sw_391dp"
|
android:layout_height="@dimen/sw_391dp"
|
||||||
android:elevation="@dimen/sw_2dp"
|
android:elevation="@dimen/sw_2dp"
|
||||||
android:layout_marginTop="@dimen/_sw_198dp"
|
android:layout_marginTop="@dimen/_sw_198dp"
|
||||||
android:src="@drawable/recharge_equity_bg" />
|
android:src="@drawable/recharge_equity_bg" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 吸顶区域:不设 scrollFlags → 自动吸顶 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/_sw_380dp"
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:background="#F6F7FB"
|
||||||
android:elevation="@dimen/sw_3dp"
|
android:elevation="@dimen/sw_3dp"
|
||||||
android:padding="@dimen/sw_16dp"
|
android:outlineProvider="none">
|
||||||
android:orientation="vertical">
|
|
||||||
|
<!-- 标题栏:高度由代码控制(0 → sw_46dp 渐进展开) -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/stickyTitleBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipChildren="true"
|
||||||
|
android:background="#F6F7FB">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="@dimen/sw_46dp"
|
||||||
|
android:layout_height="@dimen/sw_46dp"
|
||||||
|
android:layout_gravity="start|center_vertical">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_sticky_close"
|
||||||
|
android:layout_width="@dimen/sw_26dp"
|
||||||
|
android:layout_height="@dimen/sw_26dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/recharge_close"
|
||||||
|
android:rotation="180"
|
||||||
|
android:alpha="0"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/member"
|
android:id="@+id/stickyTitle"
|
||||||
android:layout_marginTop="@dimen/sw_28dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="@dimen/sw_290dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/recharge_title"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
android:textSize="@dimen/sw_18sp"
|
android:textSize="@dimen/sw_18sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="#1B1F1A"
|
android:alpha="0" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- 切换按钮 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/tabSwitchContainer"
|
||||||
|
android:layout_width="@dimen/sw_321dp"
|
||||||
|
android:layout_height="@dimen/sw_39dp"
|
||||||
|
android:layout_marginTop="@dimen/sw_12dp"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:text="Become A Member Of LOVE KEY" />
|
android:background="@drawable/recharge_tab_bg">
|
||||||
|
|
||||||
<TextView
|
<!-- 滑动指示器 -->
|
||||||
android:id="@+id/Unlock"
|
<View
|
||||||
android:layout_marginTop="@dimen/sw_3dp"
|
android:id="@+id/tabIndicator"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="@dimen/sw_14sp"
|
|
||||||
android:textColor="#1B1F1A"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="Unlock all functions" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/sw_16dp">
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imgLeft"
|
|
||||||
android:layout_width="@dimen/sw_150dp"
|
|
||||||
android:layout_height="@dimen/sw_113dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/recharge_wireless_sub_ai_dialogue"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imgRight"
|
|
||||||
android:layout_width="@dimen/sw_150dp"
|
|
||||||
android:layout_height="@dimen/sw_109dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/recharge_personalized_keyboard"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/sw_10dp">
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imgLeft"
|
|
||||||
android:layout_width="@dimen/sw_150dp"
|
|
||||||
android:layout_height="@dimen/sw_122dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/recharge_chat_persona"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imgRight"
|
|
||||||
android:layout_width="@dimen/sw_150dp"
|
|
||||||
android:layout_height="@dimen/sw_115dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/recharge_emotional_counseling"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/sw_16dp"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
<!-- 卡片 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/sw_75dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:background="@drawable/recharge_card_bg"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
<!-- 左侧文字区域 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_gravity="start|center_vertical" />
|
||||||
android:layout_marginStart="@dimen/sw_16dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<!-- 标题:Monthly Subscription -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvTitle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Monthly Subscription"
|
|
||||||
android:textSize="@dimen/sw_14sp"
|
|
||||||
android:textColor="#1B1F1A" />
|
|
||||||
|
|
||||||
<!-- 价格区域:新价格 + 划线旧价格 -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal">
|
||||||
android:layout_marginTop="@dimen/sw_4dp">
|
|
||||||
<!-- 当前价格 -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPrice"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="$4.49"
|
|
||||||
android:textSize="@dimen/sw_20sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="#000000" />
|
|
||||||
|
|
||||||
<!-- 旧价格(划线) -->
|
<!-- VIP 按钮 -->
|
||||||
<TextView
|
<FrameLayout
|
||||||
android:id="@+id/tvOldPrice"
|
android:id="@+id/tabVip"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="@dimen/sw_8dp"
|
android:layout_weight="1">
|
||||||
android:text="$4.49"
|
|
||||||
android:textSize="@dimen/sw_20sp"
|
|
||||||
android:textColor="#b3b3b3" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 右侧选中 -->
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/ivCheck"
|
android:id="@+id/ivVipTab"
|
||||||
android:layout_width="@dimen/sw_24dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/sw_24dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/sw_16dp"
|
android:layout_gravity="center"
|
||||||
android:src="@drawable/unchecked"
|
android:src="@drawable/vip_select"
|
||||||
android:scaleType="centerInside" />
|
android:scaleType="centerInside" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- SVIP 按钮 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/tabSvip"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivSvipTab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/svip_not_selected"
|
||||||
|
android:scaleType="centerInside" />
|
||||||
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<!-- ··························· -->
|
</FrameLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 评论 -->
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<!-- 内容页面 -->
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/viewPager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="@dimen/sw_120dp" />
|
||||||
|
|
||||||
|
<!-- 底部固定区域:按钮 + 协议 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:layout_gravity="bottom"
|
||||||
android:padding="@dimen/sw_16dp"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
<!-- 卡片 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="@dimen/sw_204dp"
|
|
||||||
android:layout_height="@dimen/sw_115dp"
|
|
||||||
android:background="@drawable/settings"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/sw_10dp">
|
android:gravity="center_horizontal"
|
||||||
<!-- 头像昵称 -->
|
android:background="#F6F7FB"
|
||||||
<LinearLayout
|
android:paddingTop="@dimen/sw_12dp"
|
||||||
android:layout_width="wrap_content"
|
android:paddingBottom="@dimen/sw_16dp"
|
||||||
android:layout_height="wrap_content"
|
android:elevation="@dimen/sw_4dp">
|
||||||
android:orientation="horizontal">
|
|
||||||
<de.hdodenhof.circleimageview.CircleImageView
|
|
||||||
android:layout_width="@dimen/sw_24dp"
|
|
||||||
android:layout_height="@dimen/sw_24dp"
|
|
||||||
android:src="@drawable/default_avatar" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvNickname"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/sw_6dp"
|
|
||||||
android:text="Nickname"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textSize="@dimen/sw_14sp"
|
|
||||||
android:textColor="#1B1F1A" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 五星好评 -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/ivStar"
|
|
||||||
android:layout_width="@dimen/sw_77dp"
|
|
||||||
android:layout_height="@dimen/sw_10dp"
|
|
||||||
android:layout_marginTop="@dimen/sw_8dp"
|
|
||||||
android:src="@drawable/five_star_review"
|
|
||||||
android:scaleType="fitXY" />
|
|
||||||
|
|
||||||
<!-- 评论内容 -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvComment"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/sw_6dp"
|
|
||||||
android:text="I highly recommend this APP. It taught me how to chat"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:textSize="@dimen/sw_10sp"
|
|
||||||
android:textColor="#1B1F1A" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 按钮 -->
|
<!-- 按钮 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/rechargeButton"
|
android:id="@+id/rechargeButton"
|
||||||
android:layout_marginTop="@dimen/sw_24dp"
|
|
||||||
android:layout_width="@dimen/sw_343dp"
|
android:layout_width="@dimen/sw_343dp"
|
||||||
android:layout_height="@dimen/sw_51dp"
|
android:layout_height="@dimen/sw_51dp"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:background="@drawable/button_bg"
|
android:background="@drawable/recharge_now_bg"
|
||||||
android:elevation="@dimen/sw_4dp"
|
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -277,8 +209,8 @@
|
|||||||
android:textSize="@dimen/sw_15sp"
|
android:textSize="@dimen/sw_15sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textColor="#1B1F1A"
|
android:textColor="#FFFFFF"
|
||||||
android:text="Recharge now" />
|
android:text="@string/recharge_btn" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 协议 -->
|
<!-- 协议 -->
|
||||||
@@ -287,7 +219,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/sw_17dp"
|
android:layout_marginTop="@dimen/sw_17dp"
|
||||||
android:textSize="@dimen/sw_10sp"
|
android:textSize="@dimen/sw_10sp"
|
||||||
android:text="By clicking "pay", you indicate your agreement to the"
|
android:text="@string/recharge_pay_agreement"
|
||||||
android:textColor="#1B1F1A"
|
android:textColor="#1B1F1A"
|
||||||
android:gravity="center_horizontal"/>
|
android:gravity="center_horizontal"/>
|
||||||
|
|
||||||
@@ -296,10 +228,9 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/sw_4dp"
|
android:layout_marginTop="@dimen/sw_4dp"
|
||||||
android:textSize="@dimen/sw_10sp"
|
android:textSize="@dimen/sw_10sp"
|
||||||
android:text="《Embership Agreement》"
|
android:text="@string/recharge_membership_agreement"
|
||||||
android:textColor="#02BEAC"
|
android:textColor="#02BEAC"
|
||||||
android:gravity="center_horizontal" />
|
android:gravity="center_horizontal" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
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:importantForAccessibility="no"
|
||||||
android:elevation="@dimen/sw_10dp">
|
android:elevation="@dimen/sw_10dp">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/noResultSwipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/noResultImage"
|
android:id="@+id/noResultImage"
|
||||||
@@ -271,6 +275,8 @@
|
|||||||
android:textColor="#1B1F1A"
|
android:textColor="#1B1F1A"
|
||||||
android:includeFontPadding="false" />
|
android:includeFontPadding="false" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
<!-- 抽屉 -->
|
<!-- 抽屉 -->
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/rootCoordinator"
|
android:id="@+id/rootCoordinator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -248,7 +253,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/topTabs"
|
android:id="@+id/topTabs"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/sw_40dp"
|
android:layout_height="@dimen/sw_56dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingStart="@dimen/sw_16dp"
|
android:paddingStart="@dimen/sw_16dp"
|
||||||
@@ -376,3 +381,5 @@
|
|||||||
android:scaleType="centerInside" />
|
android:scaleType="centerInside" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#F6F7FB"/>
|
android:background="#F6F7FB"/>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -144,6 +149,8 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/sw_11dp"
|
||||||
|
android:paddingEnd="@dimen/sw_11dp"
|
||||||
android:layout_marginTop="@dimen/sw_20dp">
|
android:layout_marginTop="@dimen/sw_20dp">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imgLeft"
|
android:id="@+id/imgLeft"
|
||||||
@@ -383,6 +390,7 @@
|
|||||||
|
|
||||||
<!-- 电子邮件 -->
|
<!-- 电子邮件 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/click_Email"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/sw_64dp"
|
android:layout_height="@dimen/sw_64dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
@@ -520,4 +528,6 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|||||||
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
|
<de.hdodenhof.circleimageview.CircleImageView
|
||||||
android:id="@+id/ivMenuAvatar"
|
android:id="@+id/ivMenuAvatar"
|
||||||
android:layout_width="@dimen/sw_40dp"
|
android:layout_width="@dimen/sw_40dp"
|
||||||
android:layout_height="@dimen/sw_40dp"
|
android:layout_height="@dimen/sw_40dp"/>
|
||||||
android:src="@drawable/a123123123" />
|
|
||||||
|
|
||||||
<!-- 昵称和描述 -->
|
<!-- 昵称和描述 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
android:layout_height="@dimen/sw_264dp"
|
android:layout_height="@dimen/sw_264dp"
|
||||||
android:layout_marginTop="@dimen/sw_10dp"
|
android:layout_marginTop="@dimen/sw_10dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:src="@drawable/bg"
|
android:src="@drawable/component_loading"
|
||||||
app:shapeAppearanceOverlay="@style/RoundedImageStyle" />
|
app:shapeAppearanceOverlay="@style/RoundedImageStyle" />
|
||||||
|
|
||||||
<!-- 键盘名称和下载量 -->
|
<!-- 键盘名称和下载量 -->
|
||||||
|
|||||||
@@ -125,9 +125,11 @@
|
|||||||
android:id="@+id/noResultText"
|
android:id="@+id/noResultText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/sw_50dp"
|
||||||
|
android:paddingEnd="@dimen/sw_50dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:text="@string/search_not_data"
|
android:text="@string/skin_select_none"
|
||||||
android:textSize="@dimen/sw_13sp"
|
android:textSize="@dimen/sw_13sp"
|
||||||
android:textColor="#1B1F1A"
|
android:textColor="#1B1F1A"
|
||||||
android:includeFontPadding="false" />
|
android:includeFontPadding="false" />
|
||||||
|
|||||||
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_editor">编辑</string>
|
||||||
<string name="skin_exit_editing">退出编辑</string>
|
<string name="skin_exit_editing">退出编辑</string>
|
||||||
<string name="skin_select_all">个被选中</string>
|
<string name="skin_select_all">个被选中</string>
|
||||||
|
<string name="skin_select_none">您目前还没有任何皮肤,快去下载吧</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- 搜索 -->
|
<!-- 搜索 -->
|
||||||
<string name="search_hint">请输入你要搜索的内容</string>
|
<string name="search_hint">请输入你要搜索的内容</string>
|
||||||
@@ -218,6 +220,10 @@
|
|||||||
<string name="gender_third">第三性别</string>
|
<string name="gender_third">第三性别</string>
|
||||||
<string name="skip">跳过</string>
|
<string name="skip">跳过</string>
|
||||||
<string name="delete">删除</string>
|
<string name="delete">删除</string>
|
||||||
|
<string name="chat_menu_copy">复制</string>
|
||||||
|
<string name="chat_menu_report">举报</string>
|
||||||
|
<string name="chat_copy_success">已复制</string>
|
||||||
|
<string name="email_copy_success">邮箱已复制</string>
|
||||||
<string name="next">下一步</string>
|
<string name="next">下一步</string>
|
||||||
<string name="currently_inputting">对方正在输入...</string>
|
<string name="currently_inputting">对方正在输入...</string>
|
||||||
|
|
||||||
@@ -249,5 +255,19 @@
|
|||||||
<string name="Pop_up_window_SearchFragment_1">请输入搜索关键词。</string><!-- SearchFragment --><!-- SearchResultFragment -->
|
<string name="Pop_up_window_SearchFragment_1">请输入搜索关键词。</string><!-- SearchFragment --><!-- SearchResultFragment -->
|
||||||
<string name="Pop_up_window_ThemeDownloadWorker_1">皮肤应用成功</string><!-- ThemeDownloadWorker -->
|
<string name="Pop_up_window_ThemeDownloadWorker_1">皮肤应用成功</string><!-- ThemeDownloadWorker -->
|
||||||
|
|
||||||
|
<!-- 充值 -->
|
||||||
|
<string name="recharge_title">会员充值</string>
|
||||||
|
<string name="recharge_btn">立即充值</string>
|
||||||
|
<string name="recharge_pay_agreement">点击\"支付\"即表示您同意</string>
|
||||||
|
<string name="recharge_membership_agreement">《会员协议》</string>
|
||||||
|
<string name="recharge_monthly_subscription">月度订阅</string>
|
||||||
|
|
||||||
|
<!-- SVIP充值 -->
|
||||||
|
<string name="recharge_svip_1_week">1周</string>
|
||||||
|
<string name="recharge_svip_membership_benefits">会员权益</string>
|
||||||
|
<string name="recharge_svip_longer_chat_history">更长的聊天记录</string>
|
||||||
|
<string name="recharge_svip_unlimited_chatting">无限畅聊</string>
|
||||||
|
<string name="recharge_svip_no_speed_limits">聊天不限速</string>
|
||||||
|
<string name="recharge_svip_coming_soon">敬请期待</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
<string name="skin_editor">Editor</string>
|
<string name="skin_editor">Editor</string>
|
||||||
<string name="skin_exit_editing">Exit editing</string>
|
<string name="skin_exit_editing">Exit editing</string>
|
||||||
<string name="skin_select_all">items selected</string>
|
<string name="skin_select_all">items selected</string>
|
||||||
|
<string name="skin_select_none">You currently don\'t have any skin. Hurry up and download it!</string>
|
||||||
|
|
||||||
<!-- 搜索 -->
|
<!-- 搜索 -->
|
||||||
<string name="search_hint">Please enter the content you want to search for.</string>
|
<string name="search_hint">Please enter the content you want to search for.</string>
|
||||||
@@ -223,6 +224,10 @@
|
|||||||
<string name="gender_third">The third gender</string>
|
<string name="gender_third">The third gender</string>
|
||||||
<string name="skip">Skip</string>
|
<string name="skip">Skip</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="chat_menu_copy">Copy</string>
|
||||||
|
<string name="chat_menu_report">Report</string>
|
||||||
|
<string name="chat_copy_success">Copied</string>
|
||||||
|
<string name="email_copy_success">Email copied</string>
|
||||||
<string name="next">Next step</string>
|
<string name="next">Next step</string>
|
||||||
<string name="currently_inputting">The other party is currently inputting...</string>
|
<string name="currently_inputting">The other party is currently inputting...</string>
|
||||||
|
|
||||||
@@ -254,4 +259,20 @@
|
|||||||
<string name="Pop_up_window_SearchFragment_1">Please enter the search term.</string><!-- SearchFragment --><!-- SearchResultFragment -->
|
<string name="Pop_up_window_SearchFragment_1">Please enter the search term.</string><!-- SearchFragment --><!-- SearchResultFragment -->
|
||||||
<string name="Pop_up_window_ThemeDownloadWorker_1">Skin application was successful.</string><!-- ThemeDownloadWorker -->
|
<string name="Pop_up_window_ThemeDownloadWorker_1">Skin application was successful.</string><!-- ThemeDownloadWorker -->
|
||||||
|
|
||||||
|
<!-- 充值 -->
|
||||||
|
<string name="recharge_title">Member recharge</string>
|
||||||
|
<string name="recharge_btn">Recharge now</string>
|
||||||
|
<string name="recharge_pay_agreement">By clicking \"pay\", you indicate your agreement to the</string>
|
||||||
|
<string name="recharge_membership_agreement">《Membership Agreement》</string>
|
||||||
|
<string name="recharge_monthly_subscription">Monthly Subscription</string>
|
||||||
|
|
||||||
|
<!-- SVIP充值 -->
|
||||||
|
<string name="recharge_svip_1_week">1 Week</string>
|
||||||
|
<string name="recharge_svip_membership_benefits">Membership Benefits</string>
|
||||||
|
<string name="recharge_svip_longer_chat_history">Longer chat history</string>
|
||||||
|
<string name="recharge_svip_unlimited_chatting">Unlimited chatting</string>
|
||||||
|
<string name="recharge_svip_no_speed_limits">Chat without speed limits</string>
|
||||||
|
<string name="recharge_svip_coming_soon">Coming soon</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||