基本优化完毕

This commit is contained in:
pengxiaolong
2026-01-08 14:56:20 +08:00
parent c1a80dd4cf
commit de48cc7900
70 changed files with 3222 additions and 405 deletions

View File

@@ -3,8 +3,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
@@ -20,6 +22,7 @@
<activity
android:name=".SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.MyApp.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -30,22 +33,26 @@
<!-- 输入法激活页(强烈建议增加) -->
<activity
android:name=".ImeGuideActivity"
android:screenOrientation="portrait"
android:exported="true"/>
<!-- 输入法体验页 -->
<activity
android:name=".GuideActivity"
android:screenOrientation="portrait"
android:exported="true"
android:windowSoftInputMode="stateHidden|adjustNothing" />
<!-- 引导页 -->
<activity
android:name=".OnboardingActivity"
android:screenOrientation="portrait"
android:exported="false"/>
<!-- 主界面 -->
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true">
</activity>
@@ -63,5 +70,16 @@
android:name="android.view.im"
android:resource="@xml/method" />
</service>
<!-- FileProvider for camera image capture -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -105,9 +105,11 @@ class MainActivity : AppCompatActivity() {
openGlobal(R.id.loginFragment)
}
}
is AuthEvent.GenericError -> {
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_SHORT).show()
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_LONG).show()
}
// 登录成功事件处理
is AuthEvent.LoginSuccess -> {
// 关闭 global overlay回到 empty
@@ -123,7 +125,11 @@ class MainActivity : AppCompatActivity() {
}
}
pendingTabAfterLogin = null
// ✅ 登录成功后也刷新一次
bottomNav.post { updateBottomNavVisibility() }
}
// 登出事件处理
is AuthEvent.Logout -> {
pendingTabAfterLogin = event.returnTabTag
@@ -136,11 +142,20 @@ class MainActivity : AppCompatActivity() {
openGlobal(R.id.loginFragment) // ✅ 退出登录后立刻打开登录页
}
}
// 打开全局页面事件处理
is AuthEvent.OpenGlobalPage -> {
// 打开指定的全局页面
openGlobal(event.destinationId, event.bundle)
}
is AuthEvent.UserUpdated -> {
// 不需要处理
}
is AuthEvent.CharacterDeleted -> {
// 不需要处理
}
is AuthEvent.CharacterAdded -> {
// 不需要处理由HomeFragment处理
}
}
}
}
@@ -158,9 +173,16 @@ class MainActivity : AppCompatActivity() {
TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph
}
updateBottomNavVisibility()
}
}
override fun onResume() {
super.onResume()
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener也能恢复底栏
bottomNav.post { updateBottomNavVisibility() }
}
private fun initHosts() {
val fm = supportFragmentManager
@@ -199,6 +221,38 @@ class MainActivity : AppCompatActivity() {
// 绑定底部导航栏可见性监听
bindBottomNavVisibilityForTabs()
bottomNav.post { updateBottomNavVisibility() }
}
/**
* 这些页面需要隐藏底部导航栏:你按需加/减
*/
private fun shouldHideBottomNav(destId: Int): Boolean {
return destId in setOf(
R.id.searchFragment,
R.id.searchResultFragment,
R.id.MySkin,
R.id.notificationFragment,
R.id.feedbackFragment,
R.id.MyKeyboard,
R.id.PersonalSettings,
)
}
/**
* ✅ 统一底栏显隐逻辑:任何地方状态变化都调用它
*/
private fun updateBottomNavVisibility() {
// ✅ 只要 global overlay 不在 empty底栏必须隐藏用 NavController 判断,别用 View.visibility
if (isGlobalVisible()) {
bottomNav.visibility = View.GONE
return
}
// 否则按“当前可见 tab 的当前目的地”判断
val destId = currentTabNavController.currentDestination?.id
bottomNav.visibility = if (destId != null && shouldHideBottomNav(destId)) View.GONE else View.VISIBLE
}
private fun bindGlobalVisibility() {
@@ -207,11 +261,12 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.global_container).visibility =
if (isEmpty) View.GONE else View.VISIBLE
bottomNav.visibility =
if (isEmpty) View.VISIBLE else View.GONE
// ✅ 只在"刚从某个全局页关闭回 empty"时触发回退逻辑
val justClosedOverlay = (dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
// ✅ 底栏统一走 update
updateBottomNavVisibility()
val justClosedOverlay =
(dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
lastGlobalDestId = dest.id
if (justClosedOverlay) {
@@ -220,16 +275,17 @@ class MainActivity : AppCompatActivity() {
TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph
}
// 未登录且当前处在受保护tab强制回首页
if (!isLoggedIn() && currentTabGraphId in protectedTabs) {
switchTab(TAB_HOME, force = true)
bottomNav.selectedItemId = R.id.home_graph
}
// ✅ 只有"没登录就关闭登录页"才清 pending
if (!isLoggedIn()) {
pendingTabAfterLogin = null
}
bottomNav.post { updateBottomNavVisibility() }
}
}
}
@@ -238,7 +294,7 @@ class MainActivity : AppCompatActivity() {
if (!force && targetTag == currentTabTag) return
val fm = supportFragmentManager
if (fm.isStateSaved) return // ✅ 防崩stateSaved 时不做事务
if (fm.isStateSaved) return
currentTabTag = targetTag
@@ -255,40 +311,30 @@ class MainActivity : AppCompatActivity() {
}
}
.commit()
// ✅ 关键hide/show 切 tab 不会触发 destinationChanged所以手动刷新
bottomNav.post { updateBottomNavVisibility() }
}
/** 打开全局页login/recharge等 */
private fun openGlobal(destId: Int, bundle: Bundle? = null) {
val fm = supportFragmentManager
if (fm.isStateSaved) return // ✅ 防崩
if (fm.isStateSaved) return
try {
if (bundle != null) {
globalNavController.navigate(destId, bundle)
} else {
globalNavController.navigate(destId)
}
if (bundle != null) globalNavController.navigate(destId, bundle)
else globalNavController.navigate(destId)
} catch (e: IllegalArgumentException) {
// 可选:防止偶发重复 navigate 崩溃
e.printStackTrace()
}
bottomNav.post { updateBottomNavVisibility() }
}
/** 关闭全局页pop到 empty */
/** Tab 内页面变化时刷新底栏显隐 */
private fun bindBottomNavVisibilityForTabs() {
fun shouldHideBottomNav(destId: Int): Boolean {
return destId in setOf(
R.id.searchFragment,
R.id.searchResultFragment,
R.id.MySkin
// 你还有其他需要全屏的页,也加在这里
)
}
val listener = NavController.OnDestinationChangedListener { _, dest, _ ->
// 只要 global overlay 打开了,仍然以 overlay 为准(你已有逻辑)
if (isGlobalVisible()) return@OnDestinationChangedListener
bottomNav.visibility = if (shouldHideBottomNav(dest.id)) View.GONE else View.VISIBLE
val listener = NavController.OnDestinationChangedListener { _, _, _ ->
updateBottomNavVisibility()
}
homeHost.navController.addOnDestinationChangedListener(listener)
@@ -300,12 +346,20 @@ class MainActivity : AppCompatActivity() {
if (!isGlobalVisible()) return false
val popped = globalNavController.popBackStack()
val stillVisible = globalNavController.currentDestination?.id != R.id.globalEmptyFragment
return popped || stillVisible
// ✅ pop 后刷新一次注意currentDestination 可能要等一帧才更新,所以 post
bottomNav.post { updateBottomNavVisibility() }
// popped = true 表示确实 pop 了;即使 popped=false 也可能已经在 empty 了
return popped || !isGlobalVisible()
}
/**
* ✅ 改这里:不要再用 View.visibility 判断 overlay
* 以 NavController 的目的地为准
*/
private fun isGlobalVisible(): Boolean {
return findViewById<View>(R.id.global_container).visibility == View.VISIBLE
return globalNavController.currentDestination?.id != R.id.globalEmptyFragment
}
private fun setupBackPress() {
@@ -316,13 +370,14 @@ class MainActivity : AppCompatActivity() {
// 2) 再 pop 当前tab
val popped = currentTabNavController.popBackStack()
if (popped) return
if (popped) {
bottomNav.post { updateBottomNavVisibility() }
return
}
// 3) 当前tab到根了如果不是home切回home否则退出
if (currentTabTag != TAB_HOME) {
bottomNav.post {
bottomNav.selectedItemId = R.id.home_graph
}
bottomNav.post { bottomNav.selectedItemId = R.id.home_graph }
switchTab(TAB_HOME)
} else {
finish()

View File

@@ -46,6 +46,8 @@ import android.graphics.drawable.GradientDrawable
import kotlin.math.abs
import java.text.BreakIterator
import android.widget.EditText
import android.content.res.Configuration
import androidx.constraintlayout.widget.ConstraintLayout
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
@@ -264,23 +266,6 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
createNotificationChannelIfNeeded()
tryStartForegroundSafe()
// 监听认证事件
// CoroutineScope(Dispatchers.Main).launch {
// AuthEventBus.events.collectLatest { event ->
// if (event is AuthEvent.TokenExpired) {
// // 启动 MainActivity 并跳转到登录页面
// val intent = Intent(this@MyInputMethodService, MainActivity::class.java).apply {
// flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
// putExtra("navigate_to", "loginFragment")
// }
// startActivity(intent)
// } else if (event is AuthEvent.GenericError) {
// // 显示错误提示
// android.widget.Toast.makeText(this@MyInputMethodService, "请求失败: ${event.message}", android.widget.Toast.LENGTH_SHORT).show()
// }
// }
// }
}
// 输入法状态变化
@@ -319,6 +304,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
val keyboard = ensureMainKeyboard()
currentKeyboardView = keyboard.rootView
mainKeyboardView = keyboard.rootView
(keyboard.rootView.parent as? ViewGroup)?.removeView(keyboard.rootView)
return keyboard.rootView
}
@@ -405,7 +391,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 初始状态:隐藏联想条,显示控制面板
mainKeyboardView
?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
?.findViewById<ConstraintLayout>(R.id.completion_scroll)
?.visibility = View.GONE
mainKeyboardView
@@ -612,7 +598,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
clearEditorState()
val kb = ensureMainKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
@@ -620,7 +606,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
clearEditorState()
val kb = ensureNumberKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
@@ -628,7 +614,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
clearEditorState()
val kb = ensureSymbolKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
@@ -636,7 +622,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
clearEditorState()
val kb = ensureAiKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
@@ -644,10 +630,39 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
clearEditorState()
val kb = ensureEmojiKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
setInputViewSafely(kb.rootView)
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
}
override fun associateClose() {
clearEditorState()
val kb = ensureEmojiKeyboard()
}
override fun onConfigurationChanged(newConfig: Configuration) {
// 先清理缓存,避免复用旧 View
currentKeyboardView = null
mainKeyboardView = null
numberKeyboardView = null
symbolKeyboardView = null
aiKeyboardView = null
emojiKeyboardView = null
mainKeyboard = null
numberKeyboard = null
symbolKeyboard = null
aiKeyboard = null
emojiKeyboard = null
super.onConfigurationChanged(newConfig)
}
private fun setInputViewSafely(v: View) {
(v.parent as? ViewGroup)?.removeView(v)
super.setInputView(v)
}
// Emoji 键盘
private fun ensureEmojiKeyboard(): com.example.myapplication.keyboard.EmojiKeyboard {
if (emojiKeyboard == null) {
@@ -943,7 +958,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 新增:联想滚动条 & 控制面板
val completionScroll =
mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
mainKeyboardView?.findViewById<ConstraintLayout>(R.id.completion_scroll)
val controlLayout =
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)
@@ -1006,7 +1021,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 自动滚回到最左边
private fun scrollSuggestionsToStart() {
val sv = mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
val sv = mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_HorizontalScrollView)
sv?.post { sv.fullScroll(View.FOCUS_LEFT) }
}
@@ -1402,7 +1417,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
// 4. UI联想条隐藏 & 控制面板显示
mainHandler.post {
val completionScroll =
mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
mainKeyboardView?.findViewById<ConstraintLayout>(R.id.completion_scroll)
val controlLayout =
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)

View File

@@ -3,16 +3,64 @@ package com.example.myapplication
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.view.ViewGroup
import android.widget.Toast
import com.example.myapplication.ui.common.LoadingOverlay
import android.os.Handler
import android.os.Looper
class OnboardingActivity : AppCompatActivity() {
private var selectedGender = -1 // 0: male, 1: female, 2: third
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
val btnStart = findViewById<TextView>(R.id.tv_skip)
val maleLayout = findViewById<LinearLayout>(R.id.male_layout)
val femaleLayout = findViewById<LinearLayout>(R.id.female_layout)
val thirdLayout = findViewById<LinearLayout>(R.id.third_layout)
val tvDescription = findViewById<TextView>(R.id.tv_description)
// 设置性别选择点击事件
maleLayout.setOnClickListener {
resetAllLayouts()
maleLayout.setBackgroundResource(R.drawable.gender_background_select)
selectedGender = 0
}
femaleLayout.setOnClickListener {
resetAllLayouts()
femaleLayout.setBackgroundResource(R.drawable.gender_background_select)
selectedGender = 1
}
thirdLayout.setOnClickListener {
resetAllLayouts()
thirdLayout.setBackgroundResource(R.drawable.gender_background_select)
selectedGender = 2
}
tvDescription.setOnClickListener {
if (selectedGender != -1) {
// 这里可以获取selectedGender的值(0,1,2)
// 标记已经不是第一次启动了
val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE)
prefs.edit().putBoolean("is_first_launch", false).apply()
EncryptedSharedPreferencesUtil.save(this, "gender", selectedGender.toString())
// 跳转到主界面
val rootView = window.decorView.findViewById<ViewGroup>(android.R.id.content)
LoadingOverlay.attach(rootView).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}else{
Toast.makeText(this, "Please select your gender.", Toast.LENGTH_SHORT).show()
}
}
btnStart.setOnClickListener {
// 标记已经不是第一次启动了
@@ -20,8 +68,16 @@ class OnboardingActivity : AppCompatActivity() {
prefs.edit().putBoolean("is_first_launch", false).apply()
// 跳转到主界面
val rootView = window.decorView.findViewById<ViewGroup>(android.R.id.content)
LoadingOverlay.attach(rootView).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
private fun resetAllLayouts() {
findViewById<LinearLayout>(R.id.male_layout).setBackgroundResource(R.drawable.gender_background)
findViewById<LinearLayout>(R.id.female_layout).setBackgroundResource(R.drawable.gender_background)
findViewById<LinearLayout>(R.id.third_layout).setBackgroundResource(R.drawable.gender_background)
}
}

View File

@@ -39,6 +39,8 @@ interface KeyboardEnvironment {
fun showAiKeyboard()
//emoji键盘
fun showEmojiKeyboard()
// 关闭联想
fun associateClose()
// 音效
fun playKeyClick()

View File

@@ -15,6 +15,7 @@ import android.view.MotionEvent
import android.view.View
import android.widget.PopupWindow
import android.widget.TextView
import android.widget.LinearLayout
import com.example.myapplication.theme.ThemeManager
class MainKeyboard(
@@ -159,6 +160,10 @@ class MainKeyboard(
updateRevokeButtonVisibility(view, res, pkg)
}
view.findViewById<LinearLayout?>(res.getIdentifier("associate_close", "id", pkg))?.setOnClickListener {
vibrateKey();env.associateClose()
}
view.findViewById<View?>(res.getIdentifier("key_emoji", "id", pkg))?.setOnClickListener {
vibrateKey(); env.showEmojiKeyboard()
}

View File

@@ -1,6 +1,7 @@
// 请求方法
package com.example.myapplication.network
import okhttp3.MultipartBody
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.*
@@ -65,9 +66,41 @@ interface ApiService {
@POST("user/updateInfo")
suspend fun updateUserInfo(
@Body body: updateInfoRequest
): ApiResponse<User>
): ApiResponse<Boolean>
//分页查询钱包交易记录
@POST("wallet/transactions")
suspend fun transactions(
@Body body: transactionsRequest
): ApiResponse<transactionsResponse>
//用户人设列表
@GET("character/listByUser")
suspend fun listByUser(
): ApiResponse<List<ListByUserWithNot>>
//更新用户人设排序
@POST("character/updateUserCharacterSort")
suspend fun updateUserCharacterSort(
@Body body: updateUserCharacterSortRequest
): ApiResponse<Boolean>
// 删除用户人设
@GET("character/delUserCharacter")
suspend fun delUserCharacter(
@Query("id") id: Int
): ApiResponse<Boolean>
//提交反馈
@POST("user/feedback")
suspend fun feedback(
@Body body: feedbackRequest
): ApiResponse<Boolean>
@GET("user/inviteCode")
suspend fun inviteCode(
): ApiResponse<ShareResponse>
//===========================================首页=================================
// 标签列表
@GET("tag/list")
@@ -102,17 +135,11 @@ interface ApiService {
@Query("id") id: Int
): ApiResponse<CharacterDetailResponse>
//删除用户人设
@GET("character/delUserCharacter")
suspend fun delUserCharacter(
@Query("id") id: Int
): ApiResponse<Unit>
//添加用户人设
@POST("character/addUserCharacter")
suspend fun addUserCharacter(
@Body body: AddPersonaClick
): ApiResponse<Unit>
): ApiResponse<Boolean>
//==========================================商城===========================================
@@ -188,4 +215,18 @@ interface ApiService {
suspend fun downloadZipFromUrl(
@Url url: String // 完整的下载 URL
): Response<ResponseBody>
}
/**
* 文件上传服务接口
*/
interface FileUploadService {
@Multipart
@POST("file/upload")
suspend fun uploadFile(
@Query("file") fileQuery: String,
@Part file: MultipartBody.Part
): ApiResponse<String>
}

View File

@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.SharedFlow
object AuthEventBus {
// replay=0不缓存历史事件extraBufferCapacity:避免瞬时事件
// replay=1缓存最近一次事件extraBufferCapacity=64增加缓冲区防止瞬时事件丢失
private val _events = MutableSharedFlow<AuthEvent>(
replay = 0,
extraBufferCapacity = 1
@@ -21,8 +21,11 @@ object AuthEventBus {
sealed class AuthEvent {
data class TokenExpired(val message: String? = null) : AuthEvent()
data class CharacterAdded(val personaId: Int, val newAdded: Boolean = false) : AuthEvent()
data class GenericError(val message: String) : AuthEvent()
object LoginSuccess : AuthEvent()
data class Logout(val returnTabTag: String) : AuthEvent()
data class OpenGlobalPage(val destinationId: Int, val bundle: Bundle? = null) : AuthEvent()
object UserUpdated : AuthEvent()
data class CharacterDeleted(val characterId: Int) : AuthEvent()
}

View File

@@ -9,7 +9,16 @@ import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.content.Context
import com.example.myapplication.network.security.BodyParamsExtractor
import com.example.myapplication.network.security.NonceUtils
import com.example.myapplication.network.security.SignUtils
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import okio.Buffer
import java.net.URLDecoder
import java.net.URLEncoder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
// * 不需要登录的接口路径(相对完整路径)
// * 只写 /api/ 后面的部分
@@ -20,57 +29,92 @@ private val NO_LOGIN_REQUIRED_PATHS = setOf(
"/wallet/balance",
)
private fun noLoginRequired(url: HttpUrl): Boolean {
val path = url.encodedPath // 例:/api/home/banner
private val NO_SIGN_REQUIRED_PATHS = setOf(
"/auth/login",
)
// 统一裁掉 /api 前缀
val apiPath = path.substringAfter("/api", path)
return NO_LOGIN_REQUIRED_PATHS.contains(apiPath)
private fun apiPath(url: HttpUrl): String {
val path = url.encodedPath
return path.substringAfter("/api", path)
}
private fun noLoginRequired(url: HttpUrl): Boolean =
NO_LOGIN_REQUIRED_PATHS.contains(apiPath(url))
private fun noSignRequired(url: HttpUrl): Boolean =
NO_SIGN_REQUIRED_PATHS.contains(apiPath(url))
/**
* 请求拦截器:统一加 Header、token 等
*/
fun requestInterceptor(appContext: Context) = Interceptor { chain ->
val original = chain.request()
val url = original.url
val user = EncryptedSharedPreferencesUtil.get(appContext, "user", LoginResponse::class.java)
val token = user?.token.orEmpty()
val newRequest = original.newBuilder()
val builder = original.newBuilder()
.apply {
if (token.isNotBlank()) {
addHeader("auth-token", "$token")
}
if (token.isNotBlank()) addHeader("auth-token", token)
}
.addHeader("Accept-Language", "lang")
.build()
// ===== 打印请求信息 =====
val request = newRequest
val url = request.url
// ======= ✅ 按你规则加签名header + query + body=======
if (!noSignRequired(url)) {
val appId = "loveKeyboard"
val secret = "kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H" // TODO 正式环境建议下发/混淆/NDK
val timestamp = (System.currentTimeMillis() / 1000).toString()
val nonce = java.util.UUID.randomUUID().toString().replace("-", "").take(16)
// 1) 合并成 Map去掉 sign 本身)
val params = linkedMapOf<String, String>()
params["appId"] = appId
params["timestamp"] = timestamp
params["nonce"] = nonce
// 2) query 参数
for (i in 0 until url.querySize) {
params[url.queryParameterName(i)] = url.queryParameterValue(i).orEmpty()
}
// 3) body 参数json / form
params.putAll(extractBodyParams(original))
// 4) 生成 sign
val sign = calcSign(params, secret)
builder
.addHeader("X-App-Id", appId)
.addHeader("X-Timestamp", timestamp)
.addHeader("X-Nonce", nonce)
.addHeader("X-Sign", sign)
}
val request = builder.build()
// ===== 打印请求信息(保留你原来的)=====
val sb = StringBuilder()
sb.append("\n======== HTTP Request ========\n")
sb.append("Method: ${request.method}\n")
sb.append("URL: $url\n")
sb.append("URL: ${request.url}\n")
sb.append("Headers:\n")
for (name in request.headers.names()) {
sb.append(" $name: ${request.header(name)}\n")
}
if (url.querySize > 0) {
if (request.url.querySize > 0) {
sb.append("Query Params:\n")
for (i in 0 until url.querySize) {
sb.append(" ${url.queryParameterName(i)} = ${url.queryParameterValue(i)}\n")
for (i in 0 until request.url.querySize) {
sb.append(" ${request.url.queryParameterName(i)} = ${request.url.queryParameterValue(i)}\n")
}
}
val requestBody = request.body
if (requestBody != null) {
val buffer = okio.Buffer()
val buffer = Buffer()
requestBody.writeTo(buffer)
sb.append("Body:\n")
sb.append(buffer.readUtf8())
@@ -83,6 +127,132 @@ fun requestInterceptor(appContext: Context) = Interceptor { chain ->
chain.proceed(request)
}
//
// ================== 签名工具(严格按你描述规则) ==================
//
private fun calcSign(params: Map<String, String>, secret: String): String {
// 去空值 + 去 sign
val filtered = params
.filter { (k, v) -> v.isNotBlank() && !k.equals("sign", ignoreCase = true) }
// 按 key 字典序排序
val sorted = filtered.toSortedMap()
// 拼接k=v&...&secret=xxxvalue 统一做 URL encode 防止 & = 破坏结构)
val sb = StringBuilder()
sorted.forEach { (k, v) ->
if (sb.isNotEmpty()) sb.append("&")
sb.append(k).append("=").append(urlEncode(v))
}
sb.append("&secret=").append(urlEncode(secret))
// HMAC-SHA256 -> hex小写
return hmacSha256Hex(sb.toString(), secret)
}
private fun hmacSha256Hex(data: String, secret: String): String {
val mac = Mac.getInstance("HmacSHA256")
val keySpec = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(keySpec)
val bytes = mac.doFinal(data.toByteArray(Charsets.UTF_8))
return bytes.joinToString("") { "%02x".format(it) }
}
private fun urlEncode(v: String): String =
URLEncoder.encode(v, "UTF-8")
//
// ================== Body 参数提取json / form ==================
//
private fun extractBodyParams(request: okhttp3.Request): Map<String, String> {
val body = request.body ?: return emptyMap()
val ct = body.contentType()?.toString()?.lowercase().orEmpty()
return when {
ct.contains("application/json") -> extractJsonBody(body)
ct.contains("application/x-www-form-urlencoded") -> extractFormBody(body)
else -> emptyMap() // multipart / stream 等默认不签 body如需可再扩展
}
}
private fun extractJsonBody(body: okhttp3.RequestBody): Map<String, String> {
val raw = bodyToString(body).trim()
if (raw.isBlank()) return emptyMap()
return try {
val root: JsonElement = JsonParser.parseString(raw)
val out = linkedMapOf<String, String>()
flattenJson(root, "", out)
out
} catch (_: Exception) {
emptyMap()
}
}
private fun extractFormBody(body: okhttp3.RequestBody): Map<String, String> {
val raw = bodyToString(body)
if (raw.isBlank()) return emptyMap()
val map = linkedMapOf<String, String>()
raw.split("&")
.filter { it.isNotBlank() }
.forEach { pair ->
val idx = pair.indexOf("=")
if (idx > 0) {
val k = pair.substring(0, idx)
val v = pair.substring(idx + 1)
// form 这里解码回“原值”,后续签名阶段再统一 encode
map[k] = URLDecoder.decode(v, "UTF-8")
} else {
map[pair] = ""
}
}
return map
}
private fun bodyToString(body: okhttp3.RequestBody): String {
return try {
val buffer = Buffer()
body.writeTo(buffer)
val charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
buffer.readString(charset)
} catch (_: Exception) {
""
}
}
/**
* JSON 扁平化规则:
* object: a.b.c
* array : items[0].id
*/
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
when {
elem.isJsonNull -> {
// null 不参与签名(服务端也要一致)
}
elem.isJsonPrimitive -> {
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
}
elem.isJsonObject -> {
val obj = elem.asJsonObject
for ((k, v) in obj.entrySet()) {
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
flattenJson(v, newKey, out)
}
}
elem.isJsonArray -> {
val arr = elem.asJsonArray
for (i in 0 until arr.size()) {
val newKey = "$prefix[$i]"
flattenJson(arr[i], newKey, out)
}
}
}
}
/**
* 响应拦截器:统一打印日志、做一些简单的错误处理
@@ -121,7 +291,7 @@ val responseInterceptor = Interceptor { chain ->
val gson = Gson()
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
if (errorResponse.code == 40102) {
if (errorResponse.code == 40102|| errorResponse.code == 40103) {
val isNoLoginApi = noLoginRequired(request.url)
Log.w(

View File

@@ -71,16 +71,70 @@ data class User(
val email: String,
val emailVerified: Boolean,
val isVip: Boolean,
val vipExpiry: String,
val token: String
val vipExpiry: String?,
val token: String?,
)
//更新用户
data class updateInfoRequest(
val uid: Long,
val nickName: String,
val gender: Int,
val avatarUrl: String?,
val uid: Long? = null,
val nickName: String? = null,
val gender: Int? = null,
val avatarUrl: String? = null,
)
//分页查询钱包交易记录
data class transactionsRequest(
val pageNum: Int,
val pageSize: Int,
)
//分页查询钱包交易记录响应
data class transactionsResponse(
val records: List<TransactionRecord>,
val total: Int,
val size: Int,
val current: Int,
val pages: Int
)
//分页查询钱包交易记录响应
data class TransactionRecord(
val id: Long,
val type: Int,
val amount: Number,
val beforeBalance: Number,
val afterBalance: Number,
val description: String,
val createdAt: String,
)
//用户人设列表响应
data class ListByUserWithNot(
val id: Int,
val characterName: String,
val emoji: String,
val characterId: Int,
)
//更新用户人设排序
data class updateUserCharacterSortRequest(
val sort: List<Int>
)
//提交反馈
data class feedbackRequest(
val content: String,
)
//分享响应
data class ShareResponse(
val code: String,
val status: Int,
val usedCount: Int,
val maxUses: Int,
val expiresAt: String,
val h5Link: String,
)
// =======================================首页======================================
@@ -115,7 +169,7 @@ data class listByTagWithNotLogin(
// 人设详情响应
data class CharacterDetailResponse(
val id: Long? = null,
val id: Int? = null,
val characterName: String? = null,
val characterBackground: String? = null,
val avatarUrl: String? = null,

View File

@@ -6,6 +6,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import com.example.myapplication.network.FileUploadService
object RetrofitClient {
@@ -50,6 +51,13 @@ object RetrofitClient {
retrofit.create(ApiService::class.java)
}
/**
* 创建文件上传服务
*/
fun createFileUploadService(): FileUploadService {
return retrofit.create(FileUploadService::class.java)
}
/**
* 创建支持完整 URL 下载的 Retrofit 实例
* @param baseUrl 完整的下载 URL

View File

@@ -0,0 +1,137 @@
package com.example.myapplication.network.security
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okio.Buffer
import java.nio.charset.Charset
object BodyParamsExtractor {
/**
* 抽取 Request 的 body 参数到 map
* - JSON: 扁平化a.b[0].c
* - Form: 直接 key=value
* - Multipart: 只签文本字段(文件字段跳过)
*/
fun extractBodyParams(request: Request): Map<String, String> {
val body = request.body ?: return emptyMap()
val contentType = body.contentType()?.toString()?.lowercase().orEmpty()
return when {
contentType.contains("application/json") -> extractJsonBody(body)
contentType.contains("application/x-www-form-urlencoded") -> extractFormBody(body)
contentType.contains("multipart/form-data") -> extractMultipartBody(body)
else -> {
// 其他类型(例如 stream、protobuf、octet-stream
// 建议不签或签一个摘要(需要服务端同样实现)
emptyMap()
}
}
}
private fun extractJsonBody(body: RequestBody): Map<String, String> {
val json = bodyToString(body).trim()
if (json.isBlank()) return emptyMap()
return try {
val root: JsonElement = JsonParser.parseString(json)
val out = linkedMapOf<String, String>()
flattenJson(root, "", out)
out
} catch (_: Exception) {
// JSON 解析失败就不签 body也可以选择直接把原文作为 bodyRaw 参与签名)
emptyMap()
}
}
private fun extractFormBody(body: RequestBody): Map<String, String> {
// x-www-form-urlencoded 本质就是 querystringa=1&b=2
val raw = bodyToString(body)
if (raw.isBlank()) return emptyMap()
val map = linkedMapOf<String, String>()
raw.split("&")
.filter { it.isNotBlank() }
.forEach { pair ->
val idx = pair.indexOf("=")
if (idx > 0) {
val k = pair.substring(0, idx)
val v = pair.substring(idx + 1)
// 注意:这里 raw 是已经 urlencoded 的内容
// 为了与服务端一致,推荐:服务端拿到 form 参数的“解码后值”再参与签名
// 客户端这里可以不 decode改为后续签名阶段统一 encodeSignUtils 做了 encode
map[k] = java.net.URLDecoder.decode(v, "UTF-8")
} else {
map[pair] = ""
}
}
return map
}
private fun extractMultipartBody(body: RequestBody): Map<String, String> {
if (body !is MultipartBody) return emptyMap()
val map = linkedMapOf<String, String>()
for (i in 0 until body.parts.size) {
val part = body.parts[i]
val headers = part.headers
val disp = headers?.get("Content-Disposition").orEmpty()
// 取 name="xxx"
val name = Regex("""name="([^"]+)"""").find(disp)?.groupValues?.getOrNull(1) ?: continue
val filename = Regex("""filename="([^"]+)"""").find(disp)?.groupValues?.getOrNull(1)
// 有 filename 认为是文件字段:默认不签(避免读文件流/超大)
if (!filename.isNullOrBlank()) continue
// 文本字段:读出内容
val value = bodyToString(part.body).trim()
map[name] = value
}
return map
}
private fun bodyToString(body: RequestBody): String {
return try {
val buffer = Buffer()
body.writeTo(buffer)
val charset: Charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
buffer.readString(charset)
} catch (_: Exception) {
""
}
}
/**
* JSON 扁平化规则:
* object: a.b.c
* array: items[0].id
*/
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
when {
elem.isJsonNull -> {
// null 不参与(也可以 out[prefix] = "null" 但需要服务端一致)
}
elem.isJsonPrimitive -> {
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
}
elem.isJsonObject -> {
val obj = elem.asJsonObject
for ((k, v) in obj.entrySet()) {
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
flattenJson(v, newKey, out)
}
}
elem.isJsonArray -> {
val arr = elem.asJsonArray
for (i in 0 until arr.size()) {
val newKey = "$prefix[$i]"
flattenJson(arr[i], newKey, out)
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
package com.example.myapplication.network.security
import java.util.UUID
object NonceUtils {
fun genNonce(): String =
UUID.randomUUID().toString().replace("-", "").take(16)
fun genTimestampSeconds(): String =
(System.currentTimeMillis() / 1000).toString()
}

View File

@@ -0,0 +1,38 @@
package com.example.myapplication.network.security
import java.net.URLEncoder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object SignUtils {
fun calcSign(params: Map<String, String>, secret: String): String {
val signStr = buildSignString(params, secret)
return hmacSha256Hex(signStr, secret)
}
fun buildSignString(params: Map<String, String>, secret: String): String {
val filtered = params
.filter { (k, v) -> v.isNotBlank() && !k.equals("sign", ignoreCase = true) }
.toSortedMap()
val sb = StringBuilder()
filtered.forEach { (k, v) ->
if (sb.isNotEmpty()) sb.append("&")
sb.append(k).append("=").append(urlEncode(v))
}
sb.append("&secret=").append(urlEncode(secret))
return sb.toString()
}
private fun hmacSha256Hex(data: String, secret: String): String {
val mac = Mac.getInstance("HmacSHA256")
val keySpec = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(keySpec)
val bytes = mac.doFinal(data.toByteArray(Charsets.UTF_8))
return bytes.joinToString("") { "%02x".format(it) }
}
private fun urlEncode(v: String): String =
URLEncoder.encode(v, "UTF-8")
}

View File

@@ -1,19 +0,0 @@
package com.example.myapplication.ui.circle
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.myapplication.R
class CircleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_circle, container, false)
}
}

View File

@@ -1,5 +1,7 @@
package com.example.myapplication.ui.common
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -12,20 +14,33 @@ class LoadingOverlay private constructor(
companion object {
fun attach(parent: ViewGroup): LoadingOverlay {
val overlay = LayoutInflater.from(parent.context)
.inflate(R.layout.view_fullscreen_loading, parent, false)
.inflate(R.layout.view_fullscreen_loading, parent, false).apply {
visibility = View.GONE
bringToFront()
elevation = 100f // 确保在其它视图之上
}
overlay.visibility = View.GONE
parent.addView(overlay) // 加到最上层(最后添加的在最上面)
parent.addView(overlay)
return LoadingOverlay(parent, overlay)
}
}
fun show() {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
overlay.visibility = View.VISIBLE
} else {
overlay.post { overlay.visibility = View.VISIBLE }
}
Log.d("LoadingOverlay", "Show loading")
}
fun hide() {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
overlay.visibility = View.GONE
} else {
overlay.post { overlay.visibility = View.GONE }
}
Log.d("LoadingOverlay", "Hide loading")
}
fun remove() {

View File

@@ -25,13 +25,20 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.ImeGuideActivity
import com.example.myapplication.ui.common.LoadingOverlay
import com.example.myapplication.R
import com.example.myapplication.network.*
import com.example.myapplication.network.AddPersonaClick
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.listByTagWithNotLogin
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import com.example.myapplication.network.PersonaClick
import com.example.myapplication.ui.home.PersonaAdapter
import kotlin.math.abs
class HomeFragment : Fragment() {
@@ -47,6 +54,7 @@ class HomeFragment : Fragment() {
private lateinit var tabList2: TextView
private lateinit var backgroundImage: ImageView
private var lastList1RenderKey: String? = null
private lateinit var loadingOverlay: LoadingOverlay
private var preloadJob: Job? = null
private var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
@@ -114,13 +122,82 @@ class HomeFragment : Fragment() {
}
}
}
is AuthEvent.CharacterAdded -> {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
// 1) 列表一:重新拉
allPersonaCache = fetchAllPersonaList()
lastList1RenderKey = null
notifyPageChangedOnMain(0)
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
personaCache.clear()
// 3) 如果当前就在某个 tag 页:让它立刻更新(显示 loading -> 拉取 -> 刷新)
val pos = viewPager.currentItem
if (pos > 0) {
val tagId = tags.getOrNull(pos - 1)?.id
if (tagId != null) {
// 先刷新一次,让页面进入 loading因为缓存被清了
notifyPageChangedOnMain(pos)
// 再拉当前 tag 的新数据
val list = fetchPersonaByTag(tagId)
personaCache[tagId] = list
notifyPageChangedOnMain(pos)
}
}
// 4) 可选:你如果希望“删除后列表二也立刻全量更新”,就顺手再预加载一遍
startPreloadAllTagsFillCacheOnly()
} finally {
loadingOverlay.hide()
}
}
}
is AuthEvent.CharacterDeleted -> {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
// 1) 列表一:重新拉
allPersonaCache = fetchAllPersonaList()
lastList1RenderKey = null
notifyPageChangedOnMain(0)
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
personaCache.clear()
// 3) 如果当前就在某个 tag 页:让它立刻更新(显示 loading -> 拉取 -> 刷新)
val pos = viewPager.currentItem
if (pos > 0) {
val tagId = tags.getOrNull(pos - 1)?.id
if (tagId != null) {
// 先刷新一次,让页面进入 loading因为缓存被清了
notifyPageChangedOnMain(pos)
// 再拉当前 tag 的新数据
val list = fetchPersonaByTag(tagId)
personaCache[tagId] = list
notifyPageChangedOnMain(pos)
}
}
// 4) 可选:你如果希望“删除后列表二也立刻全量更新”,就顺手再预加载一遍
startPreloadAllTagsFillCacheOnly()
} finally {
loadingOverlay.hide()
}
}
}
else -> Unit
}
}
}
}
// 充值按钮点击 - 使用事件总线打开全局页面
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
@@ -146,6 +223,8 @@ class HomeFragment : Fragment() {
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
loadingOverlay = LoadingOverlay.attach(root)
Log.d("HomeFragment", "LoadingOverlay initialized")
root.post {
if (!isAdded) return@post
@@ -169,6 +248,7 @@ class HomeFragment : Fragment() {
// 加载列表一
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val list = fetchAllPersonaList()
if (!isAdded) return@launch
@@ -180,11 +260,14 @@ class HomeFragment : Fragment() {
notifyPageChangedOnMain(0)
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "获取列表一失败", e)
} finally {
loadingOverlay.hide()
}
}
// 拉标签 + 预加载
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
@@ -204,11 +287,13 @@ class HomeFragment : Fragment() {
startPreloadAllTagsFillCacheOnly()
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "获取标签失败", e)
} finally {
loadingOverlay.hide()
}
}
}
// ================== 你要求的核心优化setupViewPager 只初始化一次 ==================
// ================== 核心setupViewPager 只初始化一次 ==================
private fun setupViewPagerOnce() {
if (sheetAdapter != null) return
@@ -245,6 +330,52 @@ class HomeFragment : Fragment() {
}
}
// ---------------- 方案A成功后“造新数据(copy)替换缓存”并刷新 ----------------
private fun applyAddedToggle(personaId: Int, newAdded: Boolean) {
// 1) 更新列表一缓存
run {
val oldAll = allPersonaCache
val idxAll = oldAll.indexOfFirst { it.id == personaId }
if (idxAll >= 0) {
val newList = oldAll.toMutableList()
val oldItem = newList[idxAll]
newList[idxAll] = oldItem.copy(added = newAdded)
allPersonaCache = newList
// renderList1 有 renderKey必须清一下
lastList1RenderKey = null
notifyPageChangedOnMain(0)
}
}
// 2) 更新所有 tag 缓存personaCache
val keys = personaCache.keys.toList()
var changedCurrentTagPage = false
for (tagId in keys) {
val old = personaCache[tagId] ?: continue
val idx = old.indexOfFirst { it.id == personaId }
if (idx >= 0) {
val newList = old.toMutableList()
val oldItem = newList[idx]
newList[idx] = oldItem.copy(added = newAdded)
personaCache[tagId] = newList
// 如果当前就在这个 tag 页,标记需要刷新
val pos = viewPager.currentItem
val currentTagId = tags.getOrNull(pos - 1)?.id
if (pos > 0 && currentTagId == tagId) {
changedCurrentTagPage = true
}
}
}
if (changedCurrentTagPage) {
notifyPageChangedOnMain(viewPager.currentItem)
}
}
// ---------------- 拖拽效果 ----------------
private fun initDrag(target: View, parent: ViewGroup) {
@@ -520,7 +651,6 @@ class HomeFragment : Fragment() {
}
}
// ---------------- ViewPager Adapter ----------------
inner class SheetPagerAdapter(
@@ -564,10 +694,7 @@ class HomeFragment : Fragment() {
val loadingView = root.findViewById<View>(R.id.loadingView)
rv2.setHasFixedSize(true)
// ✅ 禁止 itemAnimator减少 layout 抖动)
rv2.itemAnimator = null
rv2.isNestedScrollingEnabled = false
var adapter = rv2.adapter as? PersonaAdapter
@@ -584,22 +711,31 @@ class HomeFragment : Fragment() {
is PersonaClick.Add -> {
viewLifecycleOwner.lifecycleScope.launch {
val personaId = click.persona.id?: return@launch
val oldAdded = click.persona.added
val newAdded = !oldAdded
try {
if (click.persona.added == true) {
click.persona.id?.let { id ->
RetrofitClient.apiService.delUserCharacter(id.toInt())
if (oldAdded) {
// RetrofitClient.apiService.delUserCharacter(personaId)
// // ✅ 成功后替换缓存并刷新
// applyAddedToggle(personaId, newAdded)
}
} else {
else {
Log.d("1314520-HomeFragment", "add persona id=${click.persona.id}")
val req = AddPersonaClick(
characterId = click.persona.id?.toInt() ?: 0,
characterId = personaId,
emoji = click.persona.emoji ?: ""
)
RetrofitClient.apiService.addUserCharacter(req)
// ✅ 成功后替换缓存并刷新
applyAddedToggle(personaId, newAdded)
}
} catch (_: Exception) {
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "grid toggle add failed id=$personaId", e)
}
}
}
else -> Unit
}
}
rv2.layoutManager = GridLayoutManager(root.context, 2)
@@ -629,7 +765,7 @@ class HomeFragment : Fragment() {
override fun getItemCount(): Int = pageCount
}
// ---------------- 列表一渲染(原逻辑不动) ----------------
// ---------------- 列表一渲染 ----------------
private fun renderList1(root: View, list: List<listByTagWithNotLogin>) {
val key = buildString {
@@ -641,6 +777,7 @@ class HomeFragment : Fragment() {
}
if (key == lastList1RenderKey) return
lastList1RenderKey = key
val sorted = list.sortedBy { it.rank ?: Int.MAX_VALUE }
val top3 = sorted.take(3)
val others = if (sorted.size > 3) sorted.drop(3) else emptyList()
@@ -650,6 +787,7 @@ class HomeFragment : Fragment() {
avatarId = R.id.avatar_first,
nameId = R.id.name_first,
addBtnId = R.id.btn_add_first,
addBtnIcon = R.id.add_first_icon,
containerId = R.id.container_first,
item = top3.getOrNull(0)
)
@@ -659,6 +797,7 @@ class HomeFragment : Fragment() {
avatarId = R.id.avatar_second,
nameId = R.id.name_second,
addBtnId = R.id.btn_add_second,
addBtnIcon = R.id.add_second_icon,
containerId = R.id.container_second,
item = top3.getOrNull(1)
)
@@ -668,6 +807,7 @@ class HomeFragment : Fragment() {
avatarId = R.id.avatar_third,
nameId = R.id.name_third,
addBtnId = R.id.btn_add_third,
addBtnIcon = R.id.add_third_icon,
containerId = R.id.container_third,
item = top3.getOrNull(2)
)
@@ -686,6 +826,62 @@ class HomeFragment : Fragment() {
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
// ---------------- add 按钮(失败回滚 + 防连点) ----------------
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
val addIcon = itemView.findViewById<ImageView>(R.id.add_icon)
val originBg = addBtn.background
val originIcon = addIcon.drawable
fun renderAddState(added: Boolean) {
if (added) {
addBtn.setBackgroundResource(R.drawable.round_bg_others_already)
addIcon.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
} else {
addBtn.background = originBg
addIcon.setImageDrawable(originIcon)
}
}
// ✅ 首次渲染
renderAddState(p.added == true)
addBtn.setOnClickListener {
if (!addBtn.isEnabled) return@setOnClickListener
viewLifecycleOwner.lifecycleScope.launch {
val personaId = p.id?: return@launch
val oldAdded = p.added
val newAdded = !oldAdded
addBtn.isEnabled = false
renderAddState(newAdded)
try {
if (oldAdded) {
// RetrofitClient.apiService.delUserCharacter(personaId)
// // ✅ 只有成功才更新缓存 + 更新UI失败则保持原样
// applyAddedToggle(personaId, newAdded)
}
else {
Log.d("1314520-HomeFragment", "add persona id=${p.id}")
val req = AddPersonaClick(
characterId = personaId,
emoji = p.emoji ?: ""
)
RetrofitClient.apiService.addUserCharacter(req)
// ✅ 只有成功才更新缓存 + 更新UI失败则保持原样
applyAddedToggle(personaId, newAdded)
}
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "others toggle add failed id=$personaId", e)
renderAddState(oldAdded)
} finally {
addBtn.isEnabled = true
}
}
}
// ---------------- item 点击 ----------------
itemView.setOnClickListener {
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
PersonaDetailDialogFragment
@@ -693,23 +889,6 @@ class HomeFragment : Fragment() {
.show(childFragmentManager, "persona_detail")
}
itemView.findViewById<View>(R.id.btn_add).setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
try {
if (p.added == true) {
p.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
} else {
val req = AddPersonaClick(
characterId = p.id?.toInt() ?: 0,
emoji = p.emoji ?: ""
)
RetrofitClient.apiService.addUserCharacter(req)
}
} catch (_: Exception) {
}
}
}
container.addView(itemView)
}
}
@@ -719,12 +898,14 @@ class HomeFragment : Fragment() {
avatarId: Int,
nameId: Int,
addBtnId: Int,
addBtnIcon: Int,
containerId: Int,
item: listByTagWithNotLogin?
) {
val avatar = root.findViewById<de.hdodenhof.circleimageview.CircleImageView>(avatarId)
val name = root.findViewById<TextView>(nameId)
val addBtn = root.findViewById<View>(addBtnId)
val addBtn = root.findViewById<LinearLayout>(addBtnId)
val addIcon = root.findViewById<ImageView>(addBtnIcon)
val container = root.findViewById<LinearLayout>(containerId)
if (item == null) {
@@ -739,19 +920,60 @@ class HomeFragment : Fragment() {
name.text = item.characterName ?: ""
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
addBtn.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
try {
if (item.added == true) {
item.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
// ✅ 记录“原始背景/原始icon”用于 added=false 时恢复
val originBg = addBtn.background
val originIconRes = when (addBtnId) {
R.id.btn_add_first -> R.drawable.first_add
R.id.btn_add_second -> R.drawable.second_add
R.id.btn_add_third -> R.drawable.third_add
else -> 0
}
fun renderAddState(added: Boolean) {
if (added) {
addBtn.setBackgroundResource(R.drawable.round_bg_others_already)
addIcon.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
} else {
addBtn.background = originBg
if (originIconRes != 0) addIcon.setImageResource(originIconRes)
}
}
// ✅ 首次渲染
renderAddState(item.added == true)
// ✅ 点击:失败回滚 + 防连点(请求中禁用按钮)
addBtn.setOnClickListener {
if (!addBtn.isEnabled) return@setOnClickListener
viewLifecycleOwner.lifecycleScope.launch {
val personaId = item.id?: return@launch
val oldAdded = item.added
val newAdded = !oldAdded
addBtn.isEnabled = false
renderAddState(newAdded)
try {
if (oldAdded) {
// RetrofitClient.apiService.delUserCharacter(personaId)
// // ✅ 只有成功才更新缓存 + 更新UI
// applyAddedToggle(personaId, newAdded)
}
else {
Log.d("1314520-HomeFragment", "add persona id=${item.id}")
val req = AddPersonaClick(
characterId = item.id?.toInt() ?: 0,
characterId = personaId,
emoji = item.emoji ?: ""
)
RetrofitClient.apiService.addUserCharacter(req)
// ✅ 只有成功才更新缓存 + 更新UI
applyAddedToggle(personaId, newAdded)
}
} catch (_: Exception) {
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "others toggle add failed id=$personaId", e)
renderAddState(oldAdded)
} finally {
addBtn.isEnabled = true
}
}
}

View File

@@ -3,14 +3,15 @@ package com.example.myapplication.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.myapplication.R
import com.example.myapplication.network.listByTagWithNotLogin
import com.example.myapplication.network.PersonaClick
import com.example.myapplication.network.listByTagWithNotLogin
import de.hdodenhof.circleimageview.CircleImageView
import android.util.Log
class PersonaAdapter(
private val onClick: (PersonaClick) -> Unit
@@ -26,16 +27,14 @@ class PersonaAdapter(
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivAvatar: CircleImageView = itemView.findViewById(R.id.ivAvatar)
val tvName: TextView = itemView.findViewById(R.id.tvName)
val characterBackground: TextView =
itemView.findViewById(R.id.characterBackground)
val download: TextView = itemView.findViewById(R.id.download)
val operation: TextView = itemView.findViewById(R.id.operation)
private val ivAvatar: CircleImageView = itemView.findViewById(R.id.ivAvatar)
private val tvName: TextView = itemView.findViewById(R.id.tvName)
private val characterBackground: TextView = itemView.findViewById(R.id.characterBackground)
private val download: TextView = itemView.findViewById(R.id.download)
private val operation: LinearLayout = itemView.findViewById(R.id.operation)
private val operationIcon: ImageView = itemView.findViewById(R.id.operation_add_icon)
/** ✅ 统一绑定 + 点击逻辑 */
fun bind(item: listByTagWithNotLogin) {
tvName.text = item.characterName
characterBackground.text = item.characterBackground
download.text = item.download
@@ -46,15 +45,19 @@ class PersonaAdapter(
.error(R.drawable.default_avatar)
.into(ivAvatar)
// ✅ 整个 item跳详情
itemView.setOnClickListener {
onClick(PersonaClick.Item(item))
}
val isAdded = item.added
// ✅ 添加 / 下载按钮
operation.setOnClickListener {
onClick(PersonaClick.Add(item))
}
// ✅ 背景改 operation外层容器
operation.setBackgroundResource(
if (isAdded) R.drawable.list_two_bg_already else R.drawable.list_two_bg
)
// ✅ 图标改 operationIcon中间图
operationIcon.setImageResource(
if (isAdded) R.drawable.ime_guide_activity_btn_completed_img else R.drawable.operation_add
)
itemView.setOnClickListener { onClick(PersonaClick.Item(item)) }
operation.setOnClickListener { onClick(PersonaClick.Add(item)) }
}
}

View File

@@ -15,6 +15,9 @@ import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.CharacterDetailResponse
import kotlinx.coroutines.launch
import com.example.myapplication.network.AddPersonaClick
import android.util.Log
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
class PersonaDetailDialogFragment : DialogFragment() {
@@ -61,6 +64,7 @@ class PersonaDetailDialogFragment : DialogFragment() {
tvBackground.text = data.characterBackground ?: ""
btnAdd.text = data.added?.let { "Added" } ?: "Add"
btnAdd.setBackgroundResource(data.added?.let { R.drawable.ic_added } ?: R.drawable.keyboard_ettings)
val newAdded = !(data.added ?: false)
Glide.with(requireContext())
.load(data.avatarUrl)
@@ -72,13 +76,13 @@ class PersonaDetailDialogFragment : DialogFragment() {
lifecycleScope.launch {
if(data.added == true){
//取消收藏
data.id?.let { id ->
try {
RetrofitClient.apiService.delUserCharacter(id.toInt())
} catch (e: Exception) {
// 处理错误
}
}
// data.id?.let { id ->
// try {
// RetrofitClient.apiService.delUserCharacter(id.toInt())
// } catch (e: Exception) {
// // 处理错误
// }
// }
}else{
val addPersonaRequest = AddPersonaClick(
characterId = data.id?.toInt() ?: 0,
@@ -86,8 +90,11 @@ class PersonaDetailDialogFragment : DialogFragment() {
)
try {
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
data.id?.let { personaId ->
AuthEventBus.emit(AuthEvent.CharacterAdded(personaId,newAdded))
}
} catch (e: Exception) {
// 处理错误
Log.e("1314520-PersonaDetailDialogFragment", "addUserCharacter error", e)
}
}
dismissAllowingStateLoss()

View File

@@ -0,0 +1,72 @@
package com.example.myapplication.ui.keyboard
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Window
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import com.example.myapplication.R
class ConfirmDeleteDialogFragment : DialogFragment() {
companion object {
private const val ARG_NAME = "arg_name"
private const val ARG_EMOJI = "arg_emoji"
fun newInstance(
characterName: String?,
emoji: String?,
onConfirm: () -> Unit
): ConfirmDeleteDialogFragment {
return ConfirmDeleteDialogFragment().apply {
this.onConfirm = onConfirm
arguments = Bundle().apply {
putString(ARG_NAME, characterName ?: "")
putString(ARG_EMOJI, emoji ?: "🙂")
}
}
}
}
private var onConfirm: (() -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val v = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_confirm_delete_character, null, false)
dialog.setContentView(v)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
val name = arguments?.getString(ARG_NAME).orEmpty()
val emoji = arguments?.getString(ARG_EMOJI) ?: "🙂"
v.findViewById<TextView>(R.id.tv_name).text = name
v.findViewById<TextView>(R.id.tv_emoji).text = emoji
v.findViewById<TextView>(R.id.btn_cancel).setOnClickListener {
dismissAllowingStateLoss()
}
v.findViewById<TextView>(R.id.btn_confirm).setOnClickListener {
dismissAllowingStateLoss()
onConfirm?.invoke()
}
// 宽度更贴合弹窗
dialog.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.86f).toInt(),
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
)
return dialog
}
override fun onDestroyView() {
super.onDestroyView()
onConfirm = null
}
}

View File

@@ -0,0 +1,68 @@
package com.example.myapplication.ui.keyboard
import android.view.HapticFeedbackConstants
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class DragSortCallback(
private val onMove: (from: Int, to: Int) -> Unit
) : ItemTouchHelper.Callback() {
private var didHaptic = false
override fun isLongPressDragEnabled(): Boolean = true
override fun isItemViewSwipeEnabled(): Boolean = false
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags(dragFlags, 0)
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
// ✅ 触觉反馈(只触发一次)
if (!didHaptic) {
viewHolder.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
didHaptic = true
}
// ✅ 视觉反馈:放大 + 半透明
viewHolder.itemView.animate()
.scaleX(1.03f).scaleY(1.03f)
.alpha(0.85f)
.setDuration(120)
.start()
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
didHaptic = false
// ✅ 结束拖动,恢复视觉
viewHolder.itemView.animate()
.scaleX(1f).scaleY(1f)
.alpha(1f)
.setDuration(120)
.start()
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.adapterPosition
val to = target.adapterPosition
if (from == RecyclerView.NO_POSITION || to == RecyclerView.NO_POSITION) return false
onMove(from, to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
}

View File

@@ -0,0 +1,57 @@
package com.example.myapplication.ui.keyboard
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
import com.example.myapplication.network.ListByUserWithNot
class KeyboardAdapter(
private val onItemClick: (ListByUserWithNot) -> Unit
) : RecyclerView.Adapter<KeyboardAdapter.VH>() {
private val items = mutableListOf<ListByUserWithNot>()
fun submitList(newList: List<ListByUserWithNot>) {
items.clear()
items.addAll(newList)
notifyDataSetChanged()
}
fun getCurrentIdsInOrder(): List<Int> = items.map { it.id }
fun moveItem(from: Int, to: Int) {
if (from == to) return
val item = items.removeAt(from)
items.add(to, item)
notifyItemMoved(from, to)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.item_keyboard_character, parent, false)
return VH(v)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val root: View = itemView.findViewById(R.id.item_root)
private val tvEmoji: TextView = itemView.findViewById(R.id.tv_emoji)
private val tvName: TextView = itemView.findViewById(R.id.tv_name)
fun bind(item: ListByUserWithNot) {
tvEmoji.text = item.emoji ?: "🙂"
tvName.text = item.characterName ?: ""
// ✅ 点击整卡(不直接删,交给外层弹窗确认)
root.setOnClickListener { onItemClick(item) }
}
}
}

View File

@@ -1,28 +1,144 @@
package com.example.myapplication.ui.keyboard
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.ui.common.LoadingOverlay
import com.example.myapplication.network.ListByUserWithNot
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.updateUserCharacterSortRequest
import kotlinx.coroutines.launch
class MyKeyboard : Fragment() {
private lateinit var rv: RecyclerView
private lateinit var btnSave: TextView
private lateinit var adapter: KeyboardAdapter
private lateinit var loadingOverlay: LoadingOverlay
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_keyboard, container, false)
}
): View = inflater.inflate(R.layout.my_keyboard, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
rv = view.findViewById(R.id.rv_keyboard)
btnSave = view.findViewById(R.id.btn_keyboard)
adapter = KeyboardAdapter(
onItemClick = { item ->
// ✅ 点击卡片:弹自定义确认弹窗
ConfirmDeleteDialogFragment
.newInstance(item.characterName, item.emoji) {
// ✅ 用户确认后才删除
viewLifecycleOwner.lifecycleScope.launch {
val resp = setdelUserCharacter(item.id)
if (resp?.code == 0 && resp.data == true) {
Toast.makeText(requireContext(),"Deleted successfully", Toast.LENGTH_SHORT).show()
AuthEventBus.emit(AuthEvent.CharacterDeleted(item.id))
loadList()
} else {
Toast.makeText(requireContext(), resp?.message ?: "Delete failed", Toast.LENGTH_SHORT).show()
}
}
}
.show(parentFragmentManager, "confirm_delete")
}
)
rv.layoutManager = GridLayoutManager(requireContext(), 2)
rv.adapter = adapter
// ✅ 长按拖动排序 + 反馈
ItemTouchHelper(
DragSortCallback { from, to ->
adapter.moveItem(from, to)
}
).attachToRecyclerView(rv)
loadingOverlay = LoadingOverlay.attach(view as ViewGroup)
loadList()
// ✅ Save上传当前排序id数组
btnSave.setOnClickListener {
val sortIds = adapter.getCurrentIdsInOrder()
Log.d("MyKeyboard-sort", sortIds.toString())
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val body = updateUserCharacterSortRequest(sort = sortIds)
val resp = setupdateUserCharacterSort(body)
if (resp?.code == 0 && resp.data == true) {
requireActivity().onBackPressedDispatcher.onBackPressed()
Toast.makeText(requireContext(), "Sorting has been successfully modified.", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), resp?.message ?: "Save failed", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(requireContext(), "Network error: ${e.message}", Toast.LENGTH_SHORT).show()
} finally {
loadingOverlay.hide()
}
}
}
}
private fun loadList() {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val resp = getlistByUser()
if (resp?.code == 0 && resp.data != null) {
adapter.submitList(resp.data)
Log.d("1314520-list", resp.data.toString())
} else {
Toast.makeText(requireContext(), resp?.message ?: "Load failed", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(requireContext(), "Load failed: ${e.message}", Toast.LENGTH_SHORT).show()
} finally {
loadingOverlay.hide()
}
}
}
// 获取用户人设列表
private suspend fun getlistByUser(): ApiResponse<List<ListByUserWithNot>>? =
runCatching { RetrofitClient.apiService.listByUser() }.getOrNull()
// 更新用户人设排序
private suspend fun setupdateUserCharacterSort(body: updateUserCharacterSortRequest): ApiResponse<Boolean>? =
runCatching { RetrofitClient.apiService.updateUserCharacterSort(body) }.getOrNull()
// 删除用户人设
private suspend fun setdelUserCharacter(id: Int): ApiResponse<Boolean>? {
loadingOverlay.show()
return try {
runCatching { RetrofitClient.apiService.delUserCharacter(id) }.getOrNull()
} finally {
loadingOverlay.hide()
}
}
}

View File

@@ -1,5 +1,8 @@
package com.example.myapplication.ui.mine
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@@ -17,11 +20,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.example.myapplication.R
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.LoginResponse
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.ShareResponse
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.ui.common.LoadingOverlay
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import de.hdodenhof.circleimageview.CircleImageView
import kotlinx.coroutines.Job
@@ -33,6 +40,9 @@ class MineFragment : Fragment() {
private lateinit var nickname: TextView
private lateinit var time: TextView
private lateinit var logout: TextView
private lateinit var avatar: CircleImageView
private lateinit var share: LinearLayout
private lateinit var loadingOverlay: LoadingOverlay
private var loadUserJob: Job? = null
@@ -48,6 +58,7 @@ class MineFragment : Fragment() {
override fun onDestroyView() {
loadUserJob?.cancel()
loadingOverlay.remove()
super.onDestroyView()
}
@@ -57,20 +68,29 @@ class MineFragment : Fragment() {
nickname = view.findViewById(R.id.nickname)
time = view.findViewById(R.id.time)
logout = view.findViewById(R.id.logout)
avatar = view.findViewById(R.id.avatar)
share = view.findViewById(R.id.click_Share)
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
// 1) 先用本地缓存秒出首屏
renderFromCache()
// 2) 首次进入不刷新由onResume处理
// // ✅ 手动刷新:不改布局也能用
// // - 点昵称刷新
// nickname.setOnClickListener { refreshUser(force = true, showToast = true) }
// // - 长按 time 刷新
// time.setOnLongClickListener {
// refreshUser(force = true, showToast = true)
// true
// }
share.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val response = getinviteCode()
response?.data?.h5Link?.let { link ->
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("h5Link", link)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, "The sharing link has been copied to the clipboard.", Toast.LENGTH_LONG).show()
}
} finally {
loadingOverlay.hide()
}
}
}
logout.setOnClickListener {
LogoutDialogFragment { doLogout() }
@@ -85,17 +105,20 @@ class MineFragment : Fragment() {
// 使用事件总线打开金币充值页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
view.findViewById<CircleImageView>(R.id.avatar).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_personalSettings)
avatar.setOnClickListener {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.PersonalSettings))
}
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_mykeyboard)
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.MyKeyboard))
}
view.findViewById<LinearLayout>(R.id.click_record).setOnClickListener {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.consumptionRecordFragment))
}
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_feedbackFragment)
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.feedbackFragment))
}
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_notificationFragment)
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.notificationFragment))
}
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
@@ -108,6 +131,10 @@ class MineFragment : Fragment() {
renderFromCache()
refreshUser(force = true, showToast = false)
}
AuthEvent.UserUpdated -> {
renderFromCache()
refreshUser(force = true, showToast = false)
}
else -> Unit
}
}
@@ -131,6 +158,11 @@ class MineFragment : Fragment() {
)
nickname.text = cached?.nickName ?: ""
time.text = cached?.vipExpiry?.let { "Due on November $it" } ?: ""
cached?.avatarUrl?.let { url ->
Glide.with(requireContext())
.load(url)
.into(avatar)
}
}
/**
@@ -154,15 +186,22 @@ class MineFragment : Fragment() {
Log.d(TAG, "getUser ok: nick=${u?.nickName} vip=${u?.vipExpiry}")
nickname.text = u?.nickName ?: ""
time.text = u?.vipExpiry?.let { "Due on November $it" } ?: ""
u?.avatarUrl?.let { url ->
Glide.with(requireContext())
.load(url)
.into(avatar)
}
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", u)
if (showToast) Toast.makeText(requireContext(), "已刷新", Toast.LENGTH_SHORT).show()
if (showToast) Toast.makeText(requireContext(), "Refreshed", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) return@launch
Log.e(TAG, "getUser failed", e)
if (showToast && isAdded) Toast.makeText(requireContext(), "刷新失败", Toast.LENGTH_SHORT).show()
if (showToast && isAdded) Toast.makeText(requireContext(), "Refresh failed", Toast.LENGTH_SHORT).show()
}
}
}
@@ -180,6 +219,9 @@ class MineFragment : Fragment() {
// 清空 UI
nickname.text = ""
time.text = ""
Glide.with(requireContext())
.load(R.drawable.default_avatar)
.into(avatar)
// 触发登出事件让MainActivity打开登录页面
AuthEventBus.emit(AuthEvent.Logout(returnTabTag = "tab_mine"))
@@ -209,4 +251,7 @@ class MineFragment : Fragment() {
companion object {
private const val TAG = "1314520-MineFragment"
}
private suspend fun getinviteCode(): ApiResponse<ShareResponse>? =
runCatching<ApiResponse<ShareResponse>> { RetrofitClient.apiService.inviteCode() }.getOrNull()
}

View File

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

View File

@@ -0,0 +1,181 @@
package com.example.myapplication.ui.mine.consumptionRecord
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.example.myapplication.R
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.TransactionRecord
import com.example.myapplication.network.Wallet
import com.example.myapplication.network.transactionsRequest
import com.example.myapplication.network.transactionsResponse
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import kotlinx.coroutines.launch
class ConsumptionRecordFragment : BottomSheetDialogFragment() {
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var rv: RecyclerView
private lateinit var adapter: TransactionAdapter
private val listData = arrayListOf<TransactionRecord>()
private var pageNum = 1
private val pageSize = 10
private var totalPages = Int.MAX_VALUE
private var isLoading = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_consumption_record, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeRefresh = view.findViewById(R.id.swipeRefresh)
rv = view.findViewById(R.id.rvTransactions)
setupRecycler()
setupRefresh()
refreshAll()
}
/**
* ✅ 重点:关闭必须走 NavController popBackStack
* 不要 dismiss(),否则 global_nav 栈不会变,底部导航就会一直被隐藏
*/
private fun closeByNav() {
runCatching {
findNavController().popBackStack()
}.onFailure {
// 万一不是走 nav 打开的(极少情况),再兜底 dismiss
dismissAllowingStateLoss()
}
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
// ✅ 用户手势下拉/点外部取消,也要 pop 返回栈
closeByNav()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
// ✅ 有些机型/场景只走 onDismiss不走 onCancel双保险
closeByNav()
}
private fun setupRecycler() {
adapter = TransactionAdapter(
data = listData,
onCloseClick = { closeByNav() }, // ✅ 改这里:不要 dismiss()
onRechargeClick = {
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
)
rv.layoutManager = LinearLayoutManager(requireContext())
rv.adapter = adapter
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState != RecyclerView.SCROLL_STATE_IDLE) return
val reachedBottom = !recyclerView.canScrollVertically(1)
if (!reachedBottom) return
if (!isLoading && pageNum < totalPages) {
loadMore()
} else if (!isLoading && pageNum >= totalPages) {
adapter.setFooterNoMore()
}
}
})
}
private fun setupRefresh() {
swipeRefresh.setOnRefreshListener { refreshAll() }
}
private fun refreshAll() {
lifecycleScope.launch {
swipeRefresh.isRefreshing = true
pageNum = 1
totalPages = Int.MAX_VALUE
isLoading = false
adapter.hideFooter()
adapter.replaceAll(emptyList())
val walletResp = getwalletBalance()
val balanceText = walletResp?.data?.balanceDisplay ?: "0.00"
adapter.updateHeaderBalance(balanceText)
loadPage(targetPage = 1, isRefresh = true)
swipeRefresh.isRefreshing = false
}
}
private fun loadMore() {
lifecycleScope.launch { loadPage(targetPage = pageNum + 1, isRefresh = false) }
}
private suspend fun loadPage(targetPage: Int, isRefresh: Boolean) {
if (isLoading) return
isLoading = true
if (!isRefresh) adapter.setFooterLoading()
val body = transactionsRequest(pageNum = targetPage, pageSize = pageSize)
val resp = gettransactions(body)
val data = resp?.data
if (data != null) {
totalPages = data.pages
pageNum = data.current
val records = data.records
if (isRefresh) adapter.replaceAll(records) else adapter.append(records)
if (pageNum >= totalPages) adapter.setFooterNoMore() else adapter.hideFooter()
rv.post {
val notScrollableYet = !rv.canScrollVertically(1)
if (!isLoading && notScrollableYet && pageNum < totalPages) {
loadMore()
}
}
} else {
adapter.hideFooter()
}
isLoading = false
}
// ========================网络请求===========================================
private suspend fun getwalletBalance(): ApiResponse<Wallet>? =
runCatching { RetrofitClient.apiService.walletBalance() }.getOrNull()
private suspend fun gettransactions(body: transactionsRequest): ApiResponse<transactionsResponse>? =
runCatching { RetrofitClient.apiService.transactions(body) }.getOrNull()
}

View File

@@ -2,16 +2,20 @@ package com.example.myapplication.ui.mine.myotherpages
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import android.widget.FrameLayout
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.example.myapplication.R
import android.widget.FrameLayout
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import java.util.*
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.feedbackRequest
import com.google.android.material.textfield.TextInputEditText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FeedbackFragment : Fragment() {
@@ -26,9 +30,51 @@ class FeedbackFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置关闭按钮点击事件
// 关闭按钮
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
// 让多行输入框:不聚焦也能上下滑动内容
val etFeedback = view.findViewById<TextInputEditText>(R.id.et_feedback)
etFeedback.apply {
// 可选:让它本身可滚动(你 XML 已经写了也没问题)
isVerticalScrollBarEnabled = true
setOnTouchListener { v, event ->
// 告诉父布局NestedScrollView先别抢这个触摸事件
v.parent?.requestDisallowInterceptTouchEvent(true)
// 手指抬起/取消时,把控制权还给父布局(页面还能继续滚)
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
v.parent?.requestDisallowInterceptTouchEvent(false)
}
// false不吞事件让 EditText 自己处理滚动/光标
false
}
}
// 提交反馈按钮点击事件
view.findViewById<View>(R.id.btn_keyboard).setOnClickListener {
val feedbackText = etFeedback.text.toString().trim()
if (feedbackText.isEmpty()) {
Toast.makeText(context, "Please enter your feedback", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
CoroutineScope(Dispatchers.Main).launch {
val response = submitFeedback(feedbackRequest(content = feedbackText))
if (response?.code == 0) {
Toast.makeText(context, "Feedback submitted successfully", Toast.LENGTH_SHORT).show()
parentFragmentManager.popBackStack()
} else {
Toast.makeText(context, "Failed to submit feedback", Toast.LENGTH_SHORT).show()
}
}
}
}
//提交反馈
private suspend fun submitFeedback(body:feedbackRequest): ApiResponse<Boolean>? =
runCatching<ApiResponse<Boolean>> { RetrofitClient.apiService.feedback(body) }.getOrNull()
}

View File

@@ -0,0 +1,165 @@
package com.example.myapplication.ui.mine.myotherpages
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlin.math.abs
class GenderSelectSheet : BottomSheetDialogFragment() {
private val values = listOf("Male", "Female", "The third gender")
private var selectedIndex = 0
private val itemHeightDp = 48f // 每行高度(和 Adapter 里一致)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.sheet_select_gender, container, false)
selectedIndex = (arguments?.getInt(ARG_INITIAL) ?: 0).coerceIn(0, values.lastIndex)
val rv = view.findViewById<RecyclerView>(R.id.gender_wheel)
val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
rv.layoutManager = layoutManager
rv.adapter = WheelAdapter(values, dpToPx(itemHeightDp))
rv.overScrollMode = View.OVER_SCROLL_NEVER
rv.isNestedScrollingEnabled = true
rv.clipToPadding = false
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(rv)
// ✅ 关键:触摸滚轮时不让 BottomSheet 抢手势(否则会拖动弹窗)
rv.setOnTouchListener { v, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
setSheetDraggable(false)
v.parent?.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> v.parent?.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
setSheetDraggable(true)
v.parent?.requestDisallowInterceptTouchEvent(false)
}
}
false
}
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updateChildColors(recyclerView)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val snapView = snapHelper.findSnapView(layoutManager) ?: return
val pos = layoutManager.getPosition(snapView).coerceIn(0, values.lastIndex)
selectedIndex = pos
updateChildColors(recyclerView)
}
}
})
// ✅ 关键:给上下 padding让 3 条内容也能滚动/居中吸附
rv.post {
val itemPx = dpToPx(itemHeightDp)
val pad = (rv.height / 2 - itemPx / 2).coerceAtLeast(0)
rv.setPadding(rv.paddingLeft, pad, rv.paddingRight, pad)
layoutManager.scrollToPositionWithOffset(selectedIndex, pad)
rv.post { updateChildColors(rv) }
}
view.findViewById<View>(R.id.btn_close).setOnClickListener { dismiss() }
view.findViewById<View>(R.id.btn_save).setOnClickListener {
parentFragmentManager.setFragmentResult(
REQ_KEY,
Bundle().apply { putInt(BUNDLE_KEY_GENDER, selectedIndex) }
)
dismiss()
}
return view
}
private fun setSheetDraggable(draggable: Boolean) {
val d = dialog as? BottomSheetDialog ?: return
val sheet = d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) ?: return
BottomSheetBehavior.from(sheet).isDraggable = draggable
}
/** 根据距离中心点决定文字颜色 */
private fun updateChildColors(rv: RecyclerView) {
val centerY = rv.height / 2f
val selectedColor = Color.parseColor("#02BEAC")
val normalColor = Color.parseColor("#B5B5B5")
for (i in 0 until rv.childCount) {
val child = rv.getChildAt(i)
val tv = child as? TextView ?: continue
val childCenterY = (child.top + child.bottom) / 2f
val distance = abs(childCenterY - centerY)
val isSelected = distance < dpToPx(8f)
tv.setTextColor(if (isSelected) selectedColor else normalColor)
}
}
companion object {
const val REQ_KEY = "req_select_gender"
const val BUNDLE_KEY_GENDER = "bundle_gender"
private const val ARG_INITIAL = "arg_initial_gender"
fun newInstance(initialGender: Int) = GenderSelectSheet().apply {
arguments = Bundle().apply { putInt(ARG_INITIAL, initialGender) }
}
}
private fun dpToPx(dp: Float): Int =
(dp * resources.displayMetrics.density + 0.5f).toInt()
private class WheelAdapter(
private val items: List<String>,
private val itemHeightPx: Int
) : RecyclerView.Adapter<WheelAdapter.VH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val tv = TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
itemHeightPx
)
gravity = Gravity.CENTER
textSize = 18f
setTextColor(Color.parseColor("#B5B5B5"))
}
return VH(tv)
}
override fun onBindViewHolder(holder: VH, position: Int) {
(holder.itemView as TextView).text = items[position]
}
override fun getItemCount(): Int = items.size
class VH(itemView: View) : RecyclerView.ViewHolder(itemView)
}
}

View File

@@ -0,0 +1,64 @@
package com.example.myapplication.ui.mine.myotherpages
import android.os.Bundle
import android.text.InputType
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.setPadding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.example.myapplication.R
class NicknameEditSheet : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
dialog?.window?.setSoftInputMode(
android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or
android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.sheet_edit_nickname, container, false)
val initial = arguments?.getString(ARG_INITIAL).orEmpty()
val et = view.findViewById<TextInputEditText>(R.id.et_nickname)
et.setText(initial)
view.findViewById<View>(R.id.btn_close).setOnClickListener { dismiss() }
view.findViewById<View>(R.id.btn_save).setOnClickListener {
val nickname = et.text?.toString()?.trim().orEmpty()
if (nickname.isBlank()) return@setOnClickListener
parentFragmentManager.setFragmentResult(
REQ_KEY,
Bundle().apply { putString(BUNDLE_KEY_NICKNAME, nickname) }
)
dismiss()
}
return view
}
companion object {
const val REQ_KEY = "req_edit_nickname"
const val BUNDLE_KEY_NICKNAME = "bundle_nickname"
private const val ARG_INITIAL = "arg_initial_nickname"
fun newInstance(initial: String) = NicknameEditSheet().apply {
arguments = Bundle().apply { putString(ARG_INITIAL, initial) }
}
}
}

View File

@@ -1,34 +1,386 @@
package com.example.myapplication.ui.mine.myotherpages
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.example.myapplication.R
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.example.myapplication.R
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.User
import com.example.myapplication.ui.common.LoadingOverlay
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import java.util.*
import de.hdodenhof.circleimageview.CircleImageView
import kotlinx.coroutines.launch
import com.example.myapplication.network.updateInfoRequest
import android.util.Log
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class PersonalSettings : BottomSheetDialogFragment() {
private var user: User? = null
private lateinit var avatar: CircleImageView
private lateinit var tvNickname: TextView
private lateinit var tvGender: TextView
private lateinit var tvUserId: TextView
private lateinit var loadingOverlay: LoadingOverlay
// ActivityResultLauncher for image selection - restrict to image types
private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let { handleImageResult(it) }
}
private val cameraLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success: Boolean ->
if (success) {
cameraImageUri?.let { handleImageResult(it) }
}
}
private var cameraImageUri: Uri? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.personal_settings, container, false)
}
): View = inflater.inflate(R.layout.personal_settings, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置关闭按钮点击事件
// 初始化loadingOverlay
loadingOverlay = LoadingOverlay.attach(requireView() as ViewGroup)
// 关闭
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
AuthEventBus.emit(AuthEvent.UserUpdated)
requireActivity().onBackPressedDispatcher.onBackPressed()
}
// bind
avatar = view.findViewById(R.id.avatar)
tvNickname = view.findViewById(R.id.tv_nickname_value)
tvGender = view.findViewById(R.id.tv_gender_value)
tvUserId = view.findViewById(R.id.tv_userid_value)
// Avatar click listener
avatar.setOnClickListener {
showImagePickerDialog()
}
// ===================== FragmentResult listeners =====================
// 昵称保存回传
parentFragmentManager.setFragmentResultListener(
NicknameEditSheet.REQ_KEY,
viewLifecycleOwner
) { _, bundle ->
val newName = bundle.getString(NicknameEditSheet.BUNDLE_KEY_NICKNAME).orEmpty()
if (newName.isBlank()) return@setFragmentResultListener
lifecycleScope.launch {
loadingOverlay.show()
try {
val ReturnValue = setupdateUserInfo(updateInfoRequest(nickName = newName))
Log.d("1314520-PersonalSettings", "setupdateUserInfo: $ReturnValue")
if(ReturnValue?.code == 0){
tvNickname.text = newName
}
user = user?.copy(nickName = newName)
} catch (e: Exception) {
Log.e("PersonalSettings", "Failed to update nickname", e)
} finally {
loadingOverlay.hide()
}
}
}
// 性别保存回传
parentFragmentManager.setFragmentResultListener(
GenderSelectSheet.REQ_KEY,
viewLifecycleOwner
) { _, bundle ->
val newGender = bundle.getInt(GenderSelectSheet.BUNDLE_KEY_GENDER, 0)
lifecycleScope.launch {
loadingOverlay.show()
try {
val ReturnValue = setupdateUserInfo(updateInfoRequest(gender = newGender))
if(ReturnValue?.code == 0){
tvGender.text = genderText(newGender)
}
user = user?.copy(gender = newGender)
} catch (e: Exception) {
Log.e("PersonalSettings", "Failed to update gender", e)
} finally {
loadingOverlay.hide()
}
}
}
// ===================== row click =====================
// Nickname打开编辑 BottomSheetarguments 传初始值)
view.findViewById<View>(R.id.row_nickname).setOnClickListener {
NicknameEditSheet.newInstance(user?.nickName.orEmpty())
.show(parentFragmentManager, "NicknameEditSheet")
}
// Gender打开选择 BottomSheet
view.findViewById<View>(R.id.row_gender).setOnClickListener {
GenderSelectSheet.newInstance(user?.gender ?: 0)
.show(parentFragmentManager, "GenderSelectSheet")
}
// UserID点击复制
view.findViewById<View>(R.id.row_userid).setOnClickListener {
val uid = user?.uid?.toString() ?: tvUserId.text?.toString().orEmpty()
if (uid.isBlank()) return@setOnClickListener
copyToClipboard(uid)
Toast.makeText(requireContext(), "Copy successfully", Toast.LENGTH_SHORT).show()
}
// ===================== load & render =====================
viewLifecycleOwner.lifecycleScope.launch {
loadingOverlay.show()
try {
val resp = getUserdata()
val u = resp?.data // 如果你的 ApiResponse 字段不是 data这里改成你的字段名
if (u == null) {
Toast.makeText(requireContext(), "Load failed", Toast.LENGTH_SHORT).show()
return@launch
}
user = u
renderUser(u)
} finally {
loadingOverlay.hide()
}
}
}
private fun renderUser(u: User) {
tvNickname.text = u.nickName
tvGender.text = genderText(u.gender)
tvUserId.text = u.uid.toString()
Glide.with(this)
.load(u.avatarUrl)
.placeholder(R.drawable.default_avatar)
.error(R.drawable.default_avatar)
.into(avatar)
}
private fun genderText(gender: Int): String = when (gender) {
1 -> "Female"
2 -> "The third gender"
0 -> "Male"
else -> ""
}
private fun copyToClipboard(text: String) {
val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(ClipData.newPlainText("user_id", text))
}
private suspend fun getUserdata(): ApiResponse<User>? =
runCatching { RetrofitClient.apiService.getUser() }.getOrNull()
private suspend fun setupdateUserInfo(body: updateInfoRequest): ApiResponse<Boolean>? =
runCatching { RetrofitClient.apiService.updateUserInfo(body) }.getOrNull()
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
cameraImageUri = createImageFile()
cameraImageUri?.let { cameraLauncher.launch(it) }
} else {
Toast.makeText(
requireContext(),
"Camera permission is required to take photos",
Toast.LENGTH_SHORT
).show()
}
}
private fun showImagePickerDialog() {
val options = arrayOf(
getString(R.string.choose_from_gallery),
getString(R.string.take_photo)
)
androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.change_avatar)
.setItems(options) { _, which ->
when (which) {
0 -> galleryLauncher.launch("image/png,image/jpeg")
1 -> {
if (ContextCompat.checkSelfPermission(
requireContext(),
android.Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
cameraImageUri = createImageFile()
cameraImageUri?.let { cameraLauncher.launch(it) }
} else {
cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA)
}
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun handleImageResult(uri: Uri) {
Glide.with(this)
.load(uri)
.into(avatar)
lifecycleScope.launch {
uploadAvatar(uri)
}
}
private suspend fun uploadAvatar(uri: Uri) {
loadingOverlay.show()
try {
// Get MIME type to determine image format
val mimeType = requireContext().contentResolver.getType(uri)
val isPng = mimeType?.equals("image/png", ignoreCase = true) == true
// Determine file extension and compression format based on MIME type
val fileExtension = if (isPng) ".png" else ".jpg"
val compressFormat = if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
val mediaType = if (isPng) "image/png" else "image/jpeg"
val inputStream = requireContext().contentResolver.openInputStream(uri) ?: return
val storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val tempFile = File.createTempFile(
"UPLOAD_${timeStamp}_",
fileExtension,
storageDir
)
// Read and compress image if needed
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, options)
inputStream.close()
// Calculate inSampleSize
var inSampleSize = 1
val maxSize = 5 * 1024 * 1024 // 5MB
if (options.outHeight * options.outWidth * 4 > maxSize) {
val halfHeight = options.outHeight / 2
val halfWidth = options.outWidth / 2
while (halfHeight / inSampleSize >= 1024 && halfWidth / inSampleSize >= 1024) {
inSampleSize *= 2
}
}
// Decode with inSampleSize
options.inJustDecodeBounds = false
options.inSampleSize = inSampleSize
val inputStream2 = requireContext().contentResolver.openInputStream(uri) ?: return
val bitmap = BitmapFactory.decodeStream(inputStream2, null, options)
inputStream2.close()
// Compress to file
tempFile.outputStream().use { output ->
bitmap?.let { bmp ->
if (isPng) {
// PNG compression (quality parameter is ignored for PNG)
bmp.compress(Bitmap.CompressFormat.PNG, 100, output)
} else {
// JPEG compression with quality adjustment
var quality = 90
do {
output.channel.truncate(0)
bmp.compress(Bitmap.CompressFormat.JPEG, quality, output)
quality -= 10
} while (tempFile.length() > maxSize && quality > 10)
}
} ?: run {
Toast.makeText(requireContext(), "Failed to decode image", Toast.LENGTH_SHORT).show()
return
}
}
if (tempFile.length() > maxSize) {
Toast.makeText(requireContext(), "Image is too large after compression", Toast.LENGTH_SHORT).show()
return
}
val requestFile = RequestBody.create(mediaType.toMediaTypeOrNull(), tempFile)
val body = MultipartBody.Part.createFormData("file", tempFile.name, requestFile)
val response = RetrofitClient.createFileUploadService()
.uploadFile("avatar", body)
// Clean up
bitmap?.recycle()
tempFile.delete()
if (response?.code == 0) {
val ReturnValue = setupdateUserInfo(updateInfoRequest(avatarUrl = response.data))
Toast.makeText(requireContext(), R.string.avatar_updated, Toast.LENGTH_SHORT).show()
user = user?.copy(avatarUrl = response.data)
} else {
Toast.makeText(requireContext(), R.string.upload_failed, Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(requireContext(), R.string.upload_error, Toast.LENGTH_SHORT).show()
Log.e("PersonalSettings", "Upload avatar error", e)
} finally {
loadingOverlay.hide()
}
}
private fun createImageFile(): Uri? {
val storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val imageFile = File.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
storageDir
)
return FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.fileprovider",
imageFile
)
}
override fun onDestroyView() {
loadingOverlay.remove()
super.onDestroyView()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- 40% 透明白 -->
<corners
android:topLeftRadius="0dp"
android:topRightRadius="4dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="4dp" />
</shape>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 透明背景:保证不会出现“有背景颜色” -->
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
<!-- 底部边框:固定在底部,只画一条线 -->
<item android:gravity="bottom">
<shape android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="#F3F3F3" />
</shape>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -2,4 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#FAFAFA"/>
<corners android:radius="11dp"/>
<stroke android:width="2dp" android:color="#FAFAFA"/>
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#FAFAFA"/>
<corners android:radius="11dp"/>
<stroke android:width="2dp" android:color="#02BEAC"/>
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#70C5FFF6"/>
<corners android:radius="20dp"/>
<!-- 白色边框 -->
<stroke android:width="2dp" android:color="#FFFFFF"/>
</shape>

View File

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

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F3F4F6"/> <!-- 灰色背景 -->
<corners android:radius="50dp"/> <!-- 大圆角 -->
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

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

View File

@@ -2,6 +2,6 @@
android:shape="rectangle">
<solid android:color=" #FFEEE6" /> <!-- 背景色 -->
<corners android:radius="14dp" /> <!-- 圆角半径,越大越圆 -->
<corners android:radius="12dp" /> <!-- 圆角半径,越大越圆 -->
</shape>

View File

@@ -2,6 +2,6 @@
android:shape="rectangle">
<solid android:color=" #D6E7FF" /> <!-- 背景色 -->
<corners android:radius="14dp" /> <!-- 圆角半径,越大越圆 -->
<corners android:radius="12dp" /> <!-- 圆角半径,越大越圆 -->
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -80,9 +80,10 @@
android:src="@drawable/male" />
<LinearLayout
android:id="@+id/male_layout"
android:layout_width="match_parent"
android:layout_height="87dp"
android:layout_marginTop="-87dp"
android:layout_marginTop="-85dp"
android:background="@drawable/gender_background"
android:gravity="end"
android:orientation="horizontal">
@@ -117,9 +118,10 @@
android:src="@drawable/female" />
<LinearLayout
android:id="@+id/female_layout"
android:layout_width="match_parent"
android:layout_height="87dp"
android:layout_marginTop="-87dp"
android:layout_marginTop="-95dp"
android:background="@drawable/gender_background"
android:orientation="horizontal">
@@ -161,6 +163,7 @@
android:src="@drawable/question_mark_one" />
<LinearLayout
android:id="@+id/third_layout"
android:layout_width="match_parent"
android:layout_height="87dp"
android:layout_marginTop="-87dp"

View File

@@ -63,17 +63,19 @@
android:textSize="10sp"
android:textColor="#1B1F1A" />
<TextView
<LinearLayout
android:id="@+id/btn_add_second"
android:layout_width="60dp"
android:layout_height="28dp"
android:background="@drawable/round_bg_two"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="+"
android:textColor="#6EA0EB"
android:textSize="20dp"
android:textStyle="bold" />
android:layout_width="60dp"
android:layout_marginTop="50dp"
android:layout_height="28dp"
android:background="@drawable/round_bg_two">
<ImageView
android:id="@+id/add_second_icon"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/second_add"/>
</LinearLayout>
</LinearLayout>
<!-- 第一名 -->
@@ -117,17 +119,19 @@
android:textSize="10sp"
android:textColor="#1B1F1A" />
<TextView
<LinearLayout
android:id="@+id/btn_add_first"
android:layout_width="60dp"
android:layout_height="28dp"
android:gravity="center"
android:layout_width="60dp"
android:layout_marginTop="50dp"
android:text="+"
android:textSize="20dp"
android:textStyle="bold"
android:textColor="#F0C729"
android:background="@drawable/round_bg_one" />
android:layout_height="28dp"
android:background="@drawable/round_bg_one">
<ImageView
android:id="@+id/add_first_icon"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/first_add"/>
</LinearLayout>
</LinearLayout>
<!-- 第三名 -->
@@ -172,17 +176,20 @@
android:textSize="10sp"
android:textColor="#1B1F1A" />
<TextView
<LinearLayout
android:id="@+id/btn_add_third"
android:layout_width="60dp"
android:layout_height="28dp"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="+"
android:textSize="20dp"
android:textStyle="bold"
android:textColor="#FFD9C4"
android:background="@drawable/round_bg_three" />
android:layout_width="60dp"
android:layout_marginTop="50dp"
android:layout_height="28dp"
android:background="@drawable/round_bg_three">
<ImageView
android:id="@+id/add_third_icon"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/third_add"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="320dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp"
android:background="@drawable/dialog_background">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete character design?"
android:textStyle="bold"
android:textColor="#1B1F1A"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Are you sure you want to delete this character profile?"
android:textColor="#6B7280"
android:textSize="13sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_emoji"
android:layout_width="28dp"
android:layout_height="28dp"
android:gravity="center"
android:text="🙂"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="Prof."
android:textColor="#1B1F1A"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:orientation="horizontal">
<TextView
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="42dp"
android:layout_weight="1"
android:gravity="center"
android:text="Cancel"
android:textColor="#1B1F1A"
android:textSize="14sp"
android:background="@drawable/my_keyboard_cancel" />
<TextView
android:id="@+id/btn_confirm"
android:layout_width="0dp"
android:layout_height="42dp"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:gravity="center"
android:text="Delete"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:background="@drawable/my_keyboard_delete" />
</LinearLayout>
</LinearLayout>

View File

@@ -69,14 +69,17 @@
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_feedback"
android:layout_width="match_parent"
android:layout_height="259dp"
android:layout_height="200dp"
android:gravity="top|start"
android:hint="Please enter your feedback..."
android:padding="12dp"
android:inputType="textMultiLine"
android:overScrollMode="never"
android:maxLines="6"
android:minLines="4" />
android:minLines="4"
android:maxLines="10"
android:scrollbars="vertical"
android:isScrollContainer="true"
android:nestedScrollingEnabled="true" />
</com.google.android.material.textfield.TextInputLayout>
<TextView

View File

@@ -1,13 +0,0 @@
<?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="match_parent"
android:gravity="center">
<TextView
android:id="@+id/tv_circle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Circle 页面"
android:textSize="24sp" />
</FrameLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootCoordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".ui.mine.consumptionRecord.ConsumptionRecordFragment">
<!-- 整页下拉刷新:包住唯一可滚动容器 RecyclerView -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/login_bg">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTransactions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:clipToPadding="false"
android:paddingBottom="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -154,6 +154,48 @@
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 消费记录 -->
<LinearLayout
android:id="@+id/click_record"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="20dp"
android:background="@drawable/settings"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="20dp"
android:layout_height="24dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:src="@drawable/record" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="Consumption Record"
android:textColor="#1B1F1A"
android:textStyle="bold"
android:layout_weight="1"
android:textSize="20sp" />
</LinearLayout>
<ImageView
android:layout_width="9dp"
android:layout_height="13dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:src="@drawable/more_icons" />
</LinearLayout>
<!-- 注意 -->
<LinearLayout
android:id="@+id/click_Notice"
@@ -198,6 +240,7 @@
<!-- 分享应用 -->
<LinearLayout
android:id="@+id/click_Share"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="20dp"

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_root"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_margin="6dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="#FFFFFF"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<TextView
android:id="@+id/tv_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
android:gravity="center"
android:text="😄"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="Prof."
android:textColor="#1B1F1A"
android:textSize="13sp" />
</LinearLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="14dp"
android:paddingBottom="14dp">
<ProgressBar
android:id="@+id/progress"
android:layout_width="20dp"
android:layout_height="20dp" />
<TextView
android:id="@+id/tvLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="Loading..."
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>

View File

@@ -70,20 +70,23 @@
android:textColor="#02BEAC"
android:textSize="10sp" />
<TextView
<LinearLayout
android:id="@+id/operation"
android:gravity="center"
android:layout_width="100dp"
android:layout_height="32dp"
android:text="+"
android:gravity="center"
android:layout_marginTop="5dp"
android:background="@drawable/list_two_bg"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:textSize="20sp" />
android:background="@drawable/list_two_bg">
<ImageView
android:id="@+id/operation_add_icon"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/operation_add"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@@ -57,16 +57,17 @@
android:textColor="#9A9A9A" />
</LinearLayout>
<TextView
<LinearLayout
android:id="@+id/btn_add"
android:gravity="center"
android:layout_width="56dp"
android:layout_height="38dp"
android:gravity="center"
android:layout_weight="1"
android:text="+"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#02BEAC"
android:background="@drawable/round_bg_others" />
android:background="@drawable/round_bg_others">
<ImageView
android:id="@+id/add_icon"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/round_bg_others_add"/>
</LinearLayout>
</LinearLayout>
<!-- 内容卡片结束 -->

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="20dp"
android:background="@drawable/consumption_details_bg"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Date"
android:textColor="#020202"
android:textStyle="bold"
android:textSize="14sp" />
<TextView
android:id="@+id/tvTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="0000-00-00 00:00:00"
android:textColor="#9E9E9E"
android:textStyle="bold"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:id="@+id/tvAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:text="00.00"
android:textStyle="bold"
android:textSize="18sp" />
</LinearLayout>

View File

@@ -1,5 +1,6 @@
<?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:id="@+id/background"
@@ -60,14 +61,22 @@
<!-- 补全建议区域(可横向滑动) -->
<HorizontalScrollView
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/completion_scroll"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="3dp"
android:background="@drawable/complete_bg">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="50dp"
android:scrollbars="none"
android:overScrollMode="never"
android:background="@drawable/complete_bg"
android:id="@+id/completion_scroll">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/completion_HorizontalScrollView">
<LinearLayout
android:id="@+id/completion_suggestions"
@@ -307,10 +316,24 @@
android:clickable="true"
android:background="@drawable/btn_keyboard"
android:textColor="#FFFFFF"/>
</LinearLayout>
</HorizontalScrollView>
<LinearLayout
android:id="@+id/associate_close"
android:layout_width="40dp"
android:layout_height="50dp"
android:background="@drawable/complete_close_bg"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="10dp"
android:layout_height="10dp"
android:src="@drawable/associate_close"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 第一行字母键 -->
<LinearLayout

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 标题和返回 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<!-- 返回按钮 -->
<FrameLayout
android:id="@+id/iv_close"
android:layout_width="46dp"
android:layout_height="46dp">
<ImageView
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_gravity="center"
android:src="@drawable/more_icons"
android:rotation="180"
android:scaleType="fitCenter" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="49dp"
android:gravity="center"
android:textStyle="bold"
android:text="Consumption record"
android:textColor="#1B1F1A"
android:textSize="16sp" />
</LinearLayout>
<!-- 金币显示卡片 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="126dp"
android:elevation="10dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/gold_coin_bg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="My points"
android:textColor="#1B1F1A"
android:textSize="14sp"
android:padding="20dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_marginStart="20dp"
android:src="@drawable/gold_coin" />
<TextView
android:id="@+id/balance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="0.00"
android:textColor="#02BEAC"
android:textSize="40sp" />
<!-- 充值按钮 -->
<LinearLayout
android:id="@+id/rechargeButton"
android:layout_width="114dp"
android:layout_height="42dp"
android:layout_marginEnd="10dp"
android:gravity="center"
android:background="@drawable/gold_coin_button"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textStyle="bold"
android:gravity="center"
android:textColor="#1B1F1A"
android:text="Recharge" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
<!-- 列表标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:src="@drawable/gold_coin" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Consumption Details"
android:textStyle="bold"
android:textColor="#1B1F1A"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -56,7 +56,6 @@
android:textSize="16sp" />
</LinearLayout>
<!-- 内容-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -64,55 +63,25 @@
android:padding="15dp"
android:layout_marginTop="90dp"
android:background="@drawable/mykeyboard_bg"
android:orientation="horizontal">
android:orientation="vertical">
<!-- 卡片 -->
<LinearLayout
android:layout_width="115dp"
android:layout_height="42dp"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="109dp"
android:layout_height="36dp"
android:gravity="center"
android:orientation="horizontal"
android:background="#FFFFFF">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/default_avatar"
android:clickable="true"
android:focusable="true"/>
<TextView
android:layout_width="wrap_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:text="Humor"
android:textColor="#1B1F1A"
android:textSize="13sp"/>
android:overScrollMode="never"
tools:listitem="@layout/item_keyboard_character" />
</LinearLayout>
<TextView
android:layout_width="11dp"
android:layout_height="11dp"
android:layout_marginStart="-5dp"
android:layout_marginTop="-15dp"
android:text="-"
android:textSize="5sp"
android:textColor="#ffffff"
android:background="@drawable/my_keyboard_delete"
android:gravity="center"/>
</LinearLayout>
<!-- `````````````````` -->
</LinearLayout>
<!-- 按钮 -->
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/btn_keyboard"
android:layout_width="343dp"
android:layout_height="45dp"
android:layout_marginBottom="16dp"
android:layout_gravity="bottom|center_horizontal"
android:gravity="center"
android:text="Save"
android:textColor="#FFFFFF"
@@ -120,6 +89,4 @@
android:background="@drawable/my_keyboard_delete"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -96,6 +96,7 @@
android:orientation="vertical">
<!-- Nickname -->
<LinearLayout
android:id="@+id/row_nickname"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
@@ -117,6 +118,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_nickname_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Nickname"
@@ -138,6 +140,7 @@
<!-- Gender -->
<LinearLayout
android:id="@+id/row_gender"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
@@ -159,6 +162,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_gender_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Gender"
@@ -180,6 +184,7 @@
<!-- User ID -->
<LinearLayout
android:id="@+id/row_userid"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
@@ -201,6 +206,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_userid_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="88888888"

View File

@@ -0,0 +1,61 @@
<?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:orientation="vertical"
android:padding="16dp"
android:background="#F8F8F8"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Change the nickname"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#1B1F1A"/>
<ImageView
android:id="@+id/btn_close"
android:layout_width="26dp"
android:layout_height="26dp"
android:src="@drawable/pop_collapse"/>
</LinearLayout>
<!-- Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:boxBackgroundMode="none">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="#FFFFFF"/>
</com.google.android.material.textfield.TextInputLayout>
<View
android:layout_width="match_parent"
android:layout_height="43dp"/>
<!-- Save -->
<TextView
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:text="Save"
android:textColor="#FFFFFF"
android:background="@drawable/keyboard_ettings"/>
</LinearLayout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Modify gender"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#1B1F1A"/>
<ImageView
android:id="@+id/btn_close"
android:layout_width="26dp"
android:layout_height="26dp"
android:src="@drawable/pop_collapse"/>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_marginTop="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gender_wheel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:nestedScrollingEnabled="true"
android:clipToPadding="false"/>
<View
android:id="@+id/line_top"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_gravity="center"
android:translationY="-24dp"
android:background="#D8D8D8"
android:clickable="false"
android:focusable="false"/>
<View
android:id="@+id/line_bottom"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_gravity="center"
android:translationY="24dp"
android:background="#D8D8D8"
android:clickable="false"
android:focusable="false"/>
</FrameLayout>
<!-- Save -->
<TextView
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_marginTop="43dp"
android:gravity="center"
android:text="Save"
android:textColor="#FFFFFF"
android:background="@drawable/keyboard_ettings"/>
</LinearLayout>

View File

@@ -11,13 +11,6 @@
android:name="com.example.myapplication.ui.EmptyFragment"
android:label="empty" />
<!-- 圈子 -->
<fragment
android:id="@+id/circleFragment"
android:name="com.example.myapplication.ui.circle.CircleFragment"
android:label="Circle"
tools:layout="@layout/fragment_circle" />
<!-- 充值 -->
<fragment
android:id="@+id/rechargeFragment"
@@ -86,6 +79,50 @@
android:label="Forget Password Reset"
tools:layout="@layout/fragment_forget_password_reset" />
<!-- 消费记录 -->
<fragment
android:id="@+id/consumptionRecordFragment"
android:name="com.example.myapplication.ui.mine.consumptionRecord.ConsumptionRecordFragment"
android:label="Consumption Record"
tools:layout="@layout/fragment_consumption_record" />
<!-- 个人设置 -->
<fragment
android:id="@+id/PersonalSettings"
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
android:label="Personal Settings"
tools:layout="@layout/personal_settings" />
<!-- 键盘设置 -->
<fragment
android:id="@+id/MyKeyboard"
android:name="com.example.myapplication.ui.keyboard.MyKeyboard"
android:label="My Keyboard"
tools:layout="@layout/my_keyboard" />
<!-- 反馈页面 -->
<fragment
android:id="@+id/feedbackFragment"
android:name="com.example.myapplication.ui.mine.myotherpages.FeedbackFragment"
android:label="Feedback"
tools:layout="@layout/feedback_fragment" />
<!-- 通知页面 -->
<fragment
android:id="@+id/notificationFragment"
android:name="com.example.myapplication.ui.mine.myotherpages.NotificationFragment"
android:label="Notification"
tools:layout="@layout/notification_fragment" />
<!-- 消费记录跳转 -->
<action
android:id="@+id/action_global_consumptionRecordFragment"
app:destination="@id/consumptionRecordFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast" />
<!-- 充值跳转 -->
<action
android:id="@+id/action_global_rechargeFragment"

View File

@@ -44,6 +44,7 @@
app:popExitAnim="@anim/fade_out_fast" />
</fragment>
<!-- 个人设置页面 -->
<fragment
android:id="@+id/PersonalSettings"
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"

View File

@@ -17,4 +17,11 @@
<string name="key_comma">,</string>
<string name="key_period">.</string>
<string name="key_enter">Enter</string>
<string name="choose_from_gallery">从相册选择</string>
<string name="take_photo">拍照</string>
<string name="change_avatar">更换头像</string>
<string name="cancel">取消</string>
<string name="avatar_updated">头像更新成功</string>
<string name="upload_failed">上传失败</string>
<string name="upload_error">上传出错</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="my_images"
path="Pictures" />
</paths>