This commit is contained in:
pengxiaolong
2025-12-31 18:36:55 +08:00
parent 70e727fdb7
commit c1a80dd4cf
51 changed files with 2560 additions and 1159 deletions

View File

@@ -2,92 +2,354 @@ package com.example.myapplication
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import com.example.myapplication.network.AuthEventBus
import androidx.navigation.fragment.NavHostFragment
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var bottomNav: BottomNavigationView
private lateinit var navController: NavController
private val TAB_HOME = "tab_home"
private val TAB_SHOP = "tab_shop"
private val TAB_MINE = "tab_mine"
private val GLOBAL_HOST = "global_host"
private var currentTabTag = TAB_HOME
private var pendingTabAfterLogin: String? = null
private val protectedTabs = setOf(
R.id.shop_graph,
R.id.mine_graph
)
private val tabMap by lazy {
mapOf(
R.id.home_graph to TAB_HOME,
R.id.shop_graph to TAB_SHOP,
R.id.mine_graph to TAB_MINE
)
}
private lateinit var homeHost: NavHostFragment
private lateinit var shopHost: NavHostFragment
private lateinit var mineHost: NavHostFragment
private lateinit var globalHost: NavHostFragment
private var lastGlobalDestId: Int = R.id.globalEmptyFragment
private val currentTabHost: NavHostFragment
get() = when (currentTabTag) {
TAB_SHOP -> shopHost
TAB_MINE -> mineHost
else -> homeHost
}
private val currentTabNavController: NavController
get() = currentTabHost.navController
private val globalNavController: NavController
get() = globalHost.navController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomNav = findViewById(R.id.bottom_nav)
bottomNav.itemIconTintList = null
bottomNav.setOnItemReselectedListener { /* ignore */ }
// 1) 恢复当前tab
currentTabTag = savedInstanceState?.getString("current_tab_tag") ?: TAB_HOME
// 2) 初始化/找回 3个Tab host + global host
initHosts()
// 3) 底栏点击show/hide 切 tab带登录拦截 + 防 stateSaved
bottomNav.setOnItemSelectedListener { item ->
val tabTag = tabMap[item.itemId] ?: return@setOnItemSelectedListener false
// 登录拦截未登录点受保护tab => 打开全局login
if (!isLoggedIn() && item.itemId in protectedTabs) {
// 强制回到当前tab的选中状态避免底栏短暂闪一下
bottomNav.selectedItemId = when (currentTabTag) {
TAB_SHOP -> R.id.shop_graph
TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph
}
pendingTabAfterLogin = tabTag // 记录目标tab
openGlobal(R.id.loginFragment)
return@setOnItemSelectedListener false
}
switchTab(tabTag)
true
}
// 4) token过期走全局 overlay且避免重复打开
lifecycleScope.launch {
AuthEventBus.events.collectLatest { event ->
if (event is AuthEvent.TokenExpired) {
val navController = (supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment)
.navController
// 避免重复跳转(比如已经在登录页)
if (navController.currentDestination?.id != R.id.loginFragment) {
navController.navigate(R.id.action_global_loginFragment)
when (event) {
is AuthEvent.TokenExpired -> {
pendingTabAfterLogin = null
// 已经在login就别重复
if (!isGlobalVisible() || globalNavController.currentDestination?.id != R.id.loginFragment) {
openGlobal(R.id.loginFragment)
}
}
is AuthEvent.GenericError -> {
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_SHORT).show()
}
// 登录成功事件处理
is AuthEvent.LoginSuccess -> {
// 关闭 global overlay回到 empty
globalNavController.popBackStack(R.id.globalEmptyFragment, false)
// 如果之前想去商城/我的,登录成功后自动切过去
pendingTabAfterLogin?.let { tag ->
switchTab(tag)
bottomNav.selectedItemId = when (tag) {
TAB_SHOP -> R.id.shop_graph
TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph
}
}
pendingTabAfterLogin = null
}
// 登出事件处理
is AuthEvent.Logout -> {
pendingTabAfterLogin = event.returnTabTag
// ✅ 用户没登录按返回,应回首页,所以先切到首页
switchTab(TAB_HOME, force = true)
bottomNav.post {
bottomNav.selectedItemId = R.id.home_graph
openGlobal(R.id.loginFragment) // ✅ 退出登录后立刻打开登录页
}
}
// 打开全局页面事件处理
is AuthEvent.OpenGlobalPage -> {
// 打开指定的全局页面
openGlobal(event.destinationId, event.bundle)
}
} else if (event is AuthEvent.GenericError) {
android.widget.Toast.makeText(this@MainActivity, "${event.message}", android.widget.Toast.LENGTH_SHORT).show()
}
}
}
// 1. 找到 NavHostFragment
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
// 2. 找到 BottomNavigationView
bottomNav = findViewById(R.id.bottom_nav)
// 3. 绑定导航控制器(负责切换 Fragment、保持选中状态
bottomNav.setupWithNavController(navController)
// 4. 取消图标颜色 tint —— 使用原图标颜色
bottomNav.itemIconTintList = null
// 5. 添加导航监听(用于某些 Fragment 隐藏底部导航栏)
navController.addOnDestinationChangedListener { _, destination, _ ->
// 只有这些页面显示 BottomNav
val pagesWithBottomNav = setOf(
R.id.mineFragment,
R.id.homeFragment,
R.id.shopFragment
)
if (destination.id in pagesWithBottomNav) {
bottomNav.visibility = View.VISIBLE
} else {
bottomNav.visibility = View.GONE
}
}
// 6. 检查是否有导航参数,处理从键盘跳转过来的请求
// 5) intent跳转充值等统一走全局 overlay
handleNavigationFromIntent()
// 6) 返回键规则优先关闭global其次pop当前tab
setupBackPress()
// 7) 初始选中正确tab不会触发二次创建
bottomNav.post {
bottomNav.selectedItemId = when (currentTabTag) {
TAB_SHOP -> R.id.shop_graph
TAB_MINE -> R.id.mine_graph
else -> R.id.home_graph
}
}
}
private fun initHosts() {
val fm = supportFragmentManager
homeHost = fm.findFragmentByTag(TAB_HOME) as? NavHostFragment
?: NavHostFragment.create(R.navigation.home_graph)
shopHost = fm.findFragmentByTag(TAB_SHOP) as? NavHostFragment
?: NavHostFragment.create(R.navigation.shop_graph)
mineHost = fm.findFragmentByTag(TAB_MINE) as? NavHostFragment
?: NavHostFragment.create(R.navigation.mine_graph)
globalHost = fm.findFragmentByTag(GLOBAL_HOST) as? NavHostFragment
?: NavHostFragment.create(R.navigation.global_graph)
// 第一次创建时 add后续进程重建会自动恢复无需重复 add
if (fm.findFragmentByTag(TAB_HOME) == null) {
fm.beginTransaction()
.setReorderingAllowed(true)
.add(R.id.tab_container, homeHost, TAB_HOME)
.add(R.id.tab_container, shopHost, TAB_SHOP).hide(shopHost)
.add(R.id.tab_container, mineHost, TAB_MINE).hide(mineHost)
.commitNow()
}
if (fm.findFragmentByTag(GLOBAL_HOST) == null) {
fm.beginTransaction()
.setReorderingAllowed(true)
.add(R.id.global_container, globalHost, GLOBAL_HOST)
.commitNow()
}
// 确保当前tab可见
switchTab(currentTabTag, force = true)
// 绑定全局导航可见性监听
bindGlobalVisibility()
// 绑定底部导航栏可见性监听
bindBottomNavVisibilityForTabs()
}
private fun bindGlobalVisibility() {
globalNavController.addOnDestinationChangedListener { _, dest, _ ->
val isEmpty = dest.id == R.id.globalEmptyFragment
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)
lastGlobalDestId = dest.id
if (justClosedOverlay) {
val currentTabGraphId = when (currentTabTag) {
TAB_SHOP -> R.id.shop_graph
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
}
}
}
}
private fun switchTab(targetTag: String, force: Boolean = false) {
if (!force && targetTag == currentTabTag) return
val fm = supportFragmentManager
if (fm.isStateSaved) return // ✅ 防崩stateSaved 时不做事务
currentTabTag = targetTag
fm.beginTransaction()
.setReorderingAllowed(true)
.hide(homeHost)
.hide(shopHost)
.hide(mineHost)
.also { ft ->
when (targetTag) {
TAB_SHOP -> ft.show(shopHost)
TAB_MINE -> ft.show(mineHost)
else -> ft.show(homeHost)
}
}
.commit()
}
/** 打开全局页login/recharge等 */
private fun openGlobal(destId: Int, bundle: Bundle? = null) {
val fm = supportFragmentManager
if (fm.isStateSaved) return // ✅ 防崩
try {
if (bundle != null) {
globalNavController.navigate(destId, bundle)
} else {
globalNavController.navigate(destId)
}
} catch (e: IllegalArgumentException) {
// 可选:防止偶发重复 navigate 崩溃
e.printStackTrace()
}
}
/** 关闭全局页pop到 empty */
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
}
homeHost.navController.addOnDestinationChangedListener(listener)
shopHost.navController.addOnDestinationChangedListener(listener)
mineHost.navController.addOnDestinationChangedListener(listener)
}
private fun closeGlobalIfPossible(): Boolean {
if (!isGlobalVisible()) return false
val popped = globalNavController.popBackStack()
val stillVisible = globalNavController.currentDestination?.id != R.id.globalEmptyFragment
return popped || stillVisible
}
private fun isGlobalVisible(): Boolean {
return findViewById<View>(R.id.global_container).visibility == View.VISIBLE
}
private fun setupBackPress() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// 1) 优先关 global
if (closeGlobalIfPossible()) return
// 2) 再 pop 当前tab
val popped = currentTabNavController.popBackStack()
if (popped) return
// 3) 当前tab到根了如果不是home切回home否则退出
if (currentTabTag != TAB_HOME) {
bottomNav.post {
bottomNav.selectedItemId = R.id.home_graph
}
switchTab(TAB_HOME)
} else {
finish()
}
}
})
}
private fun handleNavigationFromIntent() {
val navigateTo = intent.getStringExtra("navigate_to")
if (navigateTo == "recharge_fragment") {
// 延迟执行导航,确保导航控制器已经准备好
bottomNav.post {
try {
navController.navigate(R.id.action_global_rechargeFragment)
} catch (e: Exception) {
// 如果导航失败,记录错误日志
android.util.Log.e("MainActivity", "Failed to navigate to recharge fragment", e)
if (!isLoggedIn()) {
openGlobal(R.id.loginFragment)
return@post
}
openGlobal(R.id.rechargeFragment)
}
}
}
private fun isLoggedIn(): Boolean {
return EncryptedSharedPreferencesUtil.contains(this, "user")
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("current_tab_tag", currentTabTag)
super.onSaveInstanceState(outState)
}
}

View File

@@ -44,7 +44,8 @@ import android.content.Intent
import android.view.inputmethod.ExtractedTextRequest
import android.graphics.drawable.GradientDrawable
import kotlin.math.abs
import java.text.BreakIterator
import android.widget.EditText
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
@@ -85,6 +86,10 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
private const val TAG = "MyIME"
private const val NOTIFICATION_CHANNEL_ID = "input_method_channel"
private const val NOTIFICATION_ID = 1
private const val ZWJ = 0x200D // ZERO WIDTH JOINER
private const val VS16 = 0xFE0F // VARIATION SELECTOR-16 (emoji)
private const val VS15 = 0xFE0E // VARIATION SELECTOR-15 (text)
}
// ================= 表情 =================
private var emojiKeyboardView: View? = null
@@ -604,6 +609,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
override fun showMainKeyboard() {
clearEditorState()
val kb = ensureMainKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
@@ -611,6 +617,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
override fun showNumberKeyboard() {
clearEditorState()
val kb = ensureNumberKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
@@ -618,6 +625,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
override fun showSymbolKeyboard() {
clearEditorState()
val kb = ensureSymbolKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
@@ -625,6 +633,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
override fun showAiKeyboard() {
clearEditorState()
val kb = ensureAiKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
@@ -632,6 +641,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
override fun showEmojiKeyboard() {
clearEditorState()
val kb = ensureEmojiKeyboard()
currentKeyboardView = kb.rootView
setInputView(kb.rootView)
@@ -704,25 +714,95 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
playKeyClick()
}
// 删除一个字符(原 handleBackspace
// 删除
override fun deleteOne() {
val ic = currentInputConnection ?: return
// 删除时少做 IPCselectedText 也可能慢,所以只在需要时取
// 记录删除前的极小上下文,用来判断“是否真的删掉了东西”
// 不要取多,避免开销
val before1_pre = ic.getTextBeforeCursor(1, 0)?.toString().orEmpty()
val after1_pre = ic.getTextAfterCursor(1, 0)?.toString().orEmpty()
// 1) 先处理选区:和你原来一致
val selected = ic.getSelectedText(0)
if (!selected.isNullOrEmpty()) {
// 删选区
ic.commitText("", 1)
} else {
// 删光标前一个字符(更同步)
ic.deleteSurroundingText(1, 0)
scheduleRefreshSuggestions()
playKeyClick()
return
}
// 2) 无选区:优先按 code point 删除解决“Emoji 要删两下”)
val deletedByCodePoint = try {
ic.deleteSurroundingTextInCodePoints(1, 0)
true
} catch (_: Throwable) {
false
}
if (!deletedByCodePoint) {
// 兼容兜底(老实现)
ic.deleteSurroundingText(1, 0)
} else {
// 3) 兜底清理:处理 ZWJ/变体选择符/组合符,保证“一个可见 Emoji 一次删干净”
val before = ic.getTextBeforeCursor(32, 0)?.toString().orEmpty()
if (before.isNotEmpty()) {
var s = before
var extra = 0
fun endsWithJoinerOrVariationOrMark(str: String): Boolean {
if (str.isEmpty()) return false
val cp = Character.codePointBefore(str, str.length)
return cp == ZWJ ||
cp == VS16 ||
cp == VS15 ||
Character.getType(cp) == Character.NON_SPACING_MARK.toInt() ||
Character.getType(cp) == Character.COMBINING_SPACING_MARK.toInt() ||
Character.getType(cp) == Character.ENCLOSING_MARK.toInt()
}
while (s.isNotEmpty() && extra < 8 && endsWithJoinerOrVariationOrMark(s)) {
try {
ic.deleteSurroundingTextInCodePoints(1, 0)
} catch (_: Throwable) {
ic.deleteSurroundingText(1, 0)
break
}
extra++
s = ic.getTextBeforeCursor(32, 0)?.toString().orEmpty()
}
if (s.isNotEmpty() && Character.codePointBefore(s, s.length) == ZWJ) {
try {
ic.deleteSurroundingTextInCodePoints(1, 0)
} catch (_: Throwable) {
ic.deleteSurroundingText(1, 0)
}
}
}
}
// ==== 关键:判断“删完后是否真的发生变化” ====
val before1_post = ic.getTextBeforeCursor(1, 0)?.toString().orEmpty()
val after1_post = ic.getTextAfterCursor(1, 0)?.toString().orEmpty()
val changed = (before1_pre != before1_post) || (after1_pre != after1_post)
if (!changed) {
// ✅ 删不动(常见于验证码空格子继续按删除)
// 补发一个 KEYCODE_DEL让宿主 EditText 的 OnKeyListener 能收到,执行“跳到前一格并删”
ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL))
} else {
// 只有真的改动了文本,才刷新联想(避免空删触发刷新抖动)
scheduleRefreshSuggestions()
}
playKeyClick()
}
private fun refreshSuggestionsAfterEdit() {
val ic = currentInputConnection ?: return
@@ -1061,6 +1141,35 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
}
}
private fun deleteOneGrapheme(editText: EditText) {
val text = editText.text ?: return
// 优先删选中内容(和系统行为一致)
val startSel = editText.selectionStart
val endSel = editText.selectionEnd
if (startSel != -1 && endSel != -1 && startSel != endSel) {
val s = minOf(startSel, endSel)
val e = maxOf(startSel, endSel)
text.delete(s, e)
editText.setSelection(s)
return
}
val cursor = startSel
if (cursor <= 0) return
val s = text.toString()
// 找到 cursor 前一个“字符边界”
val it = BreakIterator.getCharacterInstance()
it.setText(s)
val prev = it.preceding(cursor)
if (prev != BreakIterator.DONE && prev >= 0 && prev < cursor) {
text.delete(prev, cursor)
editText.setSelection(prev)
}
}
private fun stopRepeatDelete() {

View File

@@ -26,6 +26,35 @@ interface ApiService {
@Body body: LoginRequest
): ApiResponse<LoginResponse>
//退出登录
@GET("user/logout")
suspend fun logout(
): ApiResponse<Boolean>
//发送验证嘛
@POST("user/sendVerifyMail")
suspend fun sendVerifyCode(
@Body body: SendVerifyCodeRequest
): ApiResponse<Boolean>
//注册
@POST("user/register")
suspend fun register(
@Body body: RegisterRequest
): ApiResponse<Boolean>
//验证验证码
@POST("user/verifyMailCode")
suspend fun verifyCode(
@Body body: VerifyCodeRequest
): ApiResponse<Boolean>
//重置密码
@POST("user/resetPassWord")
suspend fun resetPassword(
@Body body: ResetPasswordRequest
): ApiResponse<Boolean>
// =========================================用户=================================
//获取用户详情
@GET("user/detail")

View File

@@ -1,5 +1,6 @@
package com.example.myapplication.network
import android.os.Bundle
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -21,4 +22,7 @@ object AuthEventBus {
sealed class AuthEvent {
data class TokenExpired(val message: String? = null) : 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()
}

View File

@@ -3,12 +3,32 @@ package com.example.myapplication.network
import android.util.Log
import com.google.gson.Gson
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.content.Context
// * 不需要登录的接口路径(相对完整路径)
// * 只写 /api/ 后面的部分
// * 例如真实 URL: https://xxx.com/api/home/banner
// * 这里写: /home/banner
private val NO_LOGIN_REQUIRED_PATHS = setOf(
"/themes/listByStyle",
"/wallet/balance",
)
private fun noLoginRequired(url: HttpUrl): Boolean {
val path = url.encodedPath // 例:/api/home/banner
// 统一裁掉 /api 前缀
val apiPath = path.substringAfter("/api", path)
return NO_LOGIN_REQUIRED_PATHS.contains(apiPath)
}
/**
* 请求拦截器:统一加 Header、token 等
*/
@@ -102,14 +122,24 @@ val responseInterceptor = Interceptor { chain ->
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
if (errorResponse.code == 40102) {
Log.w("1314520-HTTP", "token 过期: ${errorResponse.message}")
val isNoLoginApi = noLoginRequired(request.url)
// 只发事件UI 层去跳转
Log.w(
"1314520-HTTP",
"40102 path=${request.url.encodedPath}, noLogin=$isNoLoginApi"
)
// ✅ 只有"需要登录"的接口,才触发全局跳转
if (!isNoLoginApi) {
AuthEventBus.emit(AuthEvent.TokenExpired(errorResponse.message))
}
return@Interceptor response.newBuilder()
.code(401)
.message("Login expired: ${errorResponse.message}")
.message(
if (isNoLoginApi) response.message
else "Login required: ${errorResponse.message}"
)
.body(bodyString.toResponseBody(mediaType))
.build()
}

View File

@@ -33,6 +33,34 @@ data class LoginResponse(
val token: String
)
//验证码发送邮箱
data class SendVerifyCodeRequest(
val mailAddress: String
)
//注册
data class RegisterRequest(
val mailAddress: String,
val password: String,
val passwordConfirm: String,
val gender: Int?,
val verifyCode: String,
val inviteCode: String?
)
//验证验证码
data class VerifyCodeRequest(
val mailAddress: String,
val verifyCode: String,
)
//重置密码
data class ResetPasswordRequest(
val mailAddress: String,
val password: String,
val confirmPassword: String
)
// ======================================用户===================================
//获取用户详情
data class User(

View File

@@ -0,0 +1,16 @@
package com.example.myapplication.ui
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.myapplication.R
class EmptyFragment : Fragment(R.layout.fragment_empty) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置透明背景,确保不遮挡底层内容
view.setBackgroundResource(android.R.color.transparent)
}
}

View File

@@ -15,27 +15,24 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.ImeGuideActivity
import com.example.myapplication.R
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.listByTagWithNotLogin
import com.example.myapplication.network.PersonaClick
import com.example.myapplication.network.*
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 kotlin.math.abs
import com.example.myapplication.network.AddPersonaClick
class HomeFragment : Fragment() {
@@ -49,21 +46,17 @@ class HomeFragment : Fragment() {
private lateinit var tabList1: TextView
private lateinit var tabList2: TextView
private lateinit var backgroundImage: ImageView
private var preloadJob: kotlinx.coroutines.Job? = null
private var lastList1RenderKey: String? = null
private var preloadJob: Job? = null
private var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
private val sharedPool = RecyclerView.RecycledViewPool()
private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>()
private var parentWidth = 0
private var parentHeight = 0
// 你点了哪个 tag列表二
private var clickedTagId: Int? = null
// ✅ 列表二:每个 tagId 对应一份 persona 数据,避免串页
private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>()
data class Tag(val id: Int, val tagName: String)
private val tags = mutableListOf<Tag>()
private val dragToCloseThreshold by lazy {
@@ -71,11 +64,9 @@ class HomeFragment : Fragment() {
(dp * resources.displayMetrics.density)
}
private val list1Adapter: List1Adapter by lazy {
List1Adapter { item: String ->
Log.d("HomeFragment", "list1 click: $item")
}
}
// ---------------- ViewPager2 + Tabs ----------------
private var sheetAdapter: SheetPagerAdapter? = null
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
override fun onDestroyView() {
preloadJob?.cancel()
@@ -89,24 +80,57 @@ class HomeFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
): View? = inflater.inflate(R.layout.fragment_home, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 充值按钮点击
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
AuthEventBus.events.collect { event ->
when (event) {
AuthEvent.LoginSuccess -> {
// 1) 清掉未登录缓存
preloadJob?.cancel()
personaCache.clear()
allPersonaCache = emptyList()
lastList1RenderKey = null
// 2) 重新拉列表1登录态接口会变
viewLifecycleOwner.lifecycleScope.launch {
allPersonaCache = fetchAllPersonaList()
notifyPageChangedOnMain(0)
}
// 3) 如果当前在某个 tag 页,也建议重新拉当前页数据
val pos = viewPager.currentItem
if (pos > 0) {
val tagId = tags.getOrNull(pos - 1)?.id
if (tagId != null) {
viewLifecycleOwner.lifecycleScope.launch {
personaCache[tagId] = fetchPersonaByTag(tagId)
notifyPageChangedOnMain(pos)
}
}
}
}
else -> Unit
}
}
}
}
// 充值按钮点击 - 使用事件总线打开全局页面
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
findNavController().navigate(R.id.action_global_rechargeFragment)
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
}
// 输入法激活跳转
view.findViewById<ImageView>(R.id.floatingImage).setOnClickListener {
if (isAdded) {
if (!isAdded) return@setOnClickListener
startActivity(Intent(requireActivity(), ImeGuideActivity::class.java))
}
}
scrim = view.findViewById(R.id.view_scrim)
bottomSheet = view.findViewById(R.id.bottomSheet)
@@ -117,11 +141,14 @@ class HomeFragment : Fragment() {
tabList2 = view.findViewById(R.id.tab_list2)
viewPager = view.findViewById(R.id.viewPager)
viewPager.isSaveEnabled = false
viewPager.offscreenPageLimit = 2
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
root.post {
if (!isAdded) return@post
parentWidth = root.width
parentHeight = root.height
}
@@ -130,43 +157,96 @@ class HomeFragment : Fragment() {
setupBottomSheet(view)
setupTopTabs()
// 先把 ViewPager / Tags 初始化为空(避免你下面网络回来前被调用多次)
setupViewPager()
// ✅ setupViewPager 只初始化一次
setupViewPagerOnce()
// 标签 UI 初始为空
setupTags()
//刚进来强制显示列表1
// 默认显示列表1
viewPager.setCurrentItem(0, false)
updateTabsAndTags(0)
// 加载标签列表(列表一
// 加载列表一
viewLifecycleOwner.lifecycleScope.launch {
try {
val list = fetchAllPersonaList()
if (!isAdded) return@launch
allPersonaCache = list
viewPager.adapter?.notifyItemChanged(0) // 只刷新第一页
// ✅ 关键:数据变了就清 renderKey允许重建一次 UI
lastList1RenderKey = null
notifyPageChangedOnMain(0)
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "获取列表一失败", e)
}
}
// 拉标签 + 默认加载第一个 tag 的 persona列表二第一个页
// 拉标签 + 预加载
viewLifecycleOwner.lifecycleScope.launch {
try {
val response = RetrofitClient.apiService.tagList()
if (!isAdded) return@launch
tags.clear()
response.data?.let { networkTags ->
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
}
// 刷新:页数和标签栏
setupViewPager()
// ✅ 只更新页数(不重建 adapter/callback
sheetAdapter?.updatePageCount(1 + tags.size)
// 重新画 tags
setupTags()
startPreloadAllTags()
// ✅ 预加载只填缓存,不刷 UI
startPreloadAllTagsFillCacheOnly()
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "获取标签失败", e)
}
}
}
// ================== 你要求的核心优化setupViewPager 只初始化一次 ==================
private fun setupViewPagerOnce() {
if (sheetAdapter != null) return
sheetAdapter = SheetPagerAdapter(1 + tags.size)
viewPager.adapter = sheetAdapter
// ✅ 禁止 itemAnimator减少 layout 抖动)
(viewPager.getChildAt(0) as? RecyclerView)?.itemAnimator = null
pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (!isAdded) return
updateTabsAndTags(position)
// ✅ 修复当切换到标签页且缓存已有数据时强制刷新UI
if (position > 0) {
val tagIndex = position - 1
val tagId = tags.getOrNull(tagIndex)?.id
if (tagId != null && personaCache.containsKey(tagId)) {
notifyPageChangedOnMain(position)
}
}
}
}
viewPager.registerOnPageChangeCallback(pageChangeCallback!!)
}
// ✅ 统一:确保 notifyItemChanged 只在主线程
private fun notifyPageChangedOnMain(position: Int) {
viewPager.post {
if (!isAdded) return@post
viewPager.adapter?.notifyItemChanged(position)
}
}
// ---------------- 拖拽效果 ----------------
private fun initDrag(target: View, parent: ViewGroup) {
var dX = 0f
var dY = 0f
@@ -245,6 +325,7 @@ class HomeFragment : Fragment() {
}
// ---------------- BottomSheet 行为 ----------------
private fun setupBottomSheet(root: View) {
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
@@ -254,6 +335,7 @@ class HomeFragment : Fragment() {
bottomSheetBehavior.halfExpandedRatio = 0.7f
root.post {
if (!isAdded) return@post
val coordinatorHeight = root.height - 40
val button = root.findViewById<View>(R.id.rechargeButton)
val peek = (coordinatorHeight - button.bottom).coerceAtLeast(200)
@@ -296,69 +378,17 @@ class HomeFragment : Fragment() {
}
}
// ---------------- ViewPager2 + Tabs ----------------
private var sheetAdapter: SheetPagerAdapter? = null
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
private fun setupViewPager() {
if (sheetAdapter == null) {
sheetAdapter = SheetPagerAdapter(1 + tags.size)
viewPager.adapter = sheetAdapter
pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateTabsAndTags(position)
}
}
viewPager.registerOnPageChangeCallback(pageChangeCallback!!)
} else {
// tags 数量变了,只更新 pageCount 并刷新一次即可
sheetAdapter!!.updatePageCount(1 + tags.size)
}
}
private fun startPreloadAllTags() {
preloadJob?.cancel()
// 限制并发,避免一下子打爆网络/主线程调度抖动
val semaphore = kotlinx.coroutines.sync.Semaphore(permits = 2)
preloadJob = viewLifecycleOwner.lifecycleScope.launch {
// tags 还没拿到就别跑
if (tags.isEmpty()) return@launch
// 逐个 tag 预拉取(并发=2
tags.forEachIndexed { index, tag ->
// 已经有缓存就跳过
if (personaCache.containsKey(tag.id)) return@forEachIndexed
launch {
semaphore.acquire()
try {
val list = fetchPersonaByTag(tag.id)
personaCache[tag.id] = list
val pagePos = 1 + index
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
viewPager.adapter?.notifyItemChanged(pagePos)
}
} finally {
semaphore.release()
}
}
}
}
}
// ---------------- Tabs ----------------
private fun setupTopTabs() {
tabList1.setOnClickListener { viewPager.currentItem = 0 }
tabList2.setOnClickListener {
// 没有标签就别切
if (tags.isNotEmpty()) viewPager.currentItem = 1
}
}
// ---------------- Tags ----------------
private fun setupTags() {
tagContainer.removeAllViews()
@@ -370,28 +400,26 @@ class HomeFragment : Fragment() {
clickedTagId = tag.id
val pagePos = 1 + index
// 先切页:用户体感立刻响应
// 先切页:响应
viewPager.setCurrentItem(pagePos, true)
// ✅ 有缓存就不阻塞(可选:同时后台刷新)
// ✅ Tag 切换不触发多余 notify
// 1) 有缓存:不 notifyonBind 会直接显示缓存)
val cached = personaCache[tag.id]
if (cached != null) {
viewPager.adapter?.notifyItemChanged(pagePos)
return@setOnClickListener
}
if (cached != null) return@setOnClickListener
// 没缓存:页内显示 loading你 onBind 已经处理 cached==null 的 loading
viewPager.adapter?.notifyItemChanged(pagePos)
// 2) 没缓存:只 notify 一次,让页显示 loading
notifyPageChangedOnMain(pagePos)
// 后台拉取,回来只刷新这一页
// 后台拉取,回来再 notify 一次
viewLifecycleOwner.lifecycleScope.launch {
val list = fetchPersonaByTag(tag.id)
if (!isAdded) return@launch
personaCache[tag.id] = list
viewPager.adapter?.notifyItemChanged(pagePos) // ✅ 只刷新这一页
notifyPageChangedOnMain(pagePos)
}
}
tagContainer.addView(tv)
}
@@ -399,14 +427,16 @@ class HomeFragment : Fragment() {
}
private fun updateTabsAndTags(position: Int) {
val ctx = context ?: return
if (position == 0) {
tabList1.setTextColor(requireContext().getColor(R.color.black))
tabList2.setTextColor(requireContext().getColor(R.color.light_black))
tabList1.setTextColor(ctx.getColor(R.color.black))
tabList2.setTextColor(ctx.getColor(R.color.light_black))
tagScroll.isVisible = false
fadeImage(backgroundImage, R.drawable.option_background)
} else {
tabList1.setTextColor(requireContext().getColor(R.color.light_black))
tabList2.setTextColor(requireContext().getColor(R.color.black))
tabList1.setTextColor(ctx.getColor(R.color.light_black))
tabList2.setTextColor(ctx.getColor(R.color.black))
tagScroll.isVisible = true
fadeImage(backgroundImage, R.drawable.option_background_two)
@@ -416,8 +446,9 @@ class HomeFragment : Fragment() {
}
private fun fadeImage(imageView: ImageView, newImageRes: Int) {
val ctx = context ?: return
val oldDrawable = imageView.drawable
val newDrawable = ContextCompat.getDrawable(requireContext(), newImageRes) ?: return
val newDrawable = ContextCompat.getDrawable(ctx, newImageRes) ?: return
if (oldDrawable == null) {
imageView.setImageDrawable(newDrawable)
@@ -432,12 +463,15 @@ class HomeFragment : Fragment() {
}
private fun highlightTag(index: Int) {
val ctx = context ?: return
for (i in 0 until tagContainer.childCount) {
val child = tagContainer.getChildAt(i) as TextView
if (i == index) {
child.setBackgroundResource(R.drawable.tag_selected_bg)
child.setTextColor(requireContext().getColor(android.R.color.white))
child.setTextColor(ctx.getColor(android.R.color.white))
tagScroll.post {
if (!isAdded) return@post
val scrollViewWidth = tagScroll.width
val childCenter = child.left + child.width / 2
val targetScrollX = childCenter - scrollViewWidth / 2
@@ -445,12 +479,50 @@ class HomeFragment : Fragment() {
}
} else {
child.setBackgroundResource(R.drawable.tag_unselected_bg)
child.setTextColor(requireContext().getColor(R.color.light_black))
child.setTextColor(ctx.getColor(R.color.light_black))
}
}
}
// ---------------- 预加载:只填缓存,不刷新 UI ----------------
private fun startPreloadAllTagsFillCacheOnly() {
preloadJob?.cancel()
val semaphore = kotlinx.coroutines.sync.Semaphore(permits = 2)
preloadJob = viewLifecycleOwner.lifecycleScope.launch {
if (tags.isEmpty()) return@launch
tags.forEach { tag ->
if (personaCache.containsKey(tag.id)) return@forEach
launch {
semaphore.acquire()
try {
val list = fetchPersonaByTag(tag.id)
personaCache[tag.id] = list
// ✅ 只在用户正在看的页时刷新一次(不算乱刷 UI
val idx = tags.indexOfFirst { it.id == tag.id }
val thisPos = 1 + idx
if (idx >= 0 && viewPager.currentItem == thisPos) {
notifyPageChangedOnMain(thisPos)
}
} catch (e: Exception) {
Log.e("HomeFragment", "preload tag=${tag.id} fail", e)
} finally {
semaphore.release()
}
}
}
}
}
// ---------------- ViewPager Adapter ----------------
inner class SheetPagerAdapter(
private var pageCount: Int
) : RecyclerView.Adapter<SheetPagerAdapter.PageViewHolder>() {
@@ -458,8 +530,16 @@ class HomeFragment : Fragment() {
inner class PageViewHolder(val root: View) : RecyclerView.ViewHolder(root)
fun updatePageCount(newCount: Int) {
if (newCount == pageCount) return
val old = pageCount
pageCount = newCount
notifyDataSetChanged()
if (newCount > old) {
notifyItemRangeInserted(old, newCount - old)
} else {
notifyItemRangeRemoved(newCount, old - newCount)
}
}
override fun getItemViewType(position: Int): Int = if (position == 0) 0 else 1
@@ -484,7 +564,10 @@ 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
@@ -493,12 +576,15 @@ class HomeFragment : Fragment() {
when (click) {
is PersonaClick.Item -> {
val id = click.persona.id
if (!isAdded || childFragmentManager.isStateSaved) return@PersonaAdapter
PersonaDetailDialogFragment
.newInstance(id)
.show(childFragmentManager, "persona_detail")
}
is PersonaClick.Add -> {
lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch {
try {
if (click.persona.added == true) {
click.persona.id?.let { id ->
RetrofitClient.apiService.delUserCharacter(id.toInt())
@@ -510,6 +596,8 @@ class HomeFragment : Fragment() {
)
RetrofitClient.apiService.addUserCharacter(req)
}
} catch (_: Exception) {
}
}
}
}
@@ -541,47 +629,49 @@ class HomeFragment : Fragment() {
override fun getItemCount(): Int = pageCount
}
// 通过 tagIndex 取出该页要显示的数据
private fun getPersonaListByTagIndex(tagIndex: Int): List<listByTagWithNotLogin> {
if (tagIndex !in tags.indices) return emptyList()
val tagId = tags[tagIndex].id
return personaCache[tagId] ?: emptyList()
}
// ---------------- 列表一渲染(原逻辑不动) ----------------
private fun renderList1(root: View, list: List<listByTagWithNotLogin>) {
// 1) 排序rank 小的排前面
val key = buildString {
list.forEach { p ->
append(p.id).append('_')
append(p.added).append('_')
append(p.rank).append('|')
}
}
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()
// 2) 绑定前三名(注意:你的 UI 排列是:第二/第一/第三)
bindTopItem(root,
bindTopItem(
root,
avatarId = R.id.avatar_first,
nameId = R.id.name_first,
addBtnId = R.id.btn_add_first,
container = R.id.container_first,
item = top3.getOrNull(0) // rank 最小 = 第一名
containerId = R.id.container_first,
item = top3.getOrNull(0)
)
bindTopItem(root,
bindTopItem(
root,
avatarId = R.id.avatar_second,
nameId = R.id.name_second,
addBtnId = R.id.btn_add_second,
container = R.id.container_second,
item = top3.getOrNull(1) // 第二名
containerId = R.id.container_second,
item = top3.getOrNull(1)
)
bindTopItem(root,
bindTopItem(
root,
avatarId = R.id.avatar_third,
nameId = R.id.name_third,
addBtnId = R.id.btn_add_third,
container = R.id.container_third,
item = top3.getOrNull(2) // 第三名
containerId = R.id.container_third,
item = top3.getOrNull(2)
)
// 3) 渲染后面的内容卡片
val container = root.findViewById<LinearLayout>(R.id.container_others)
container.removeAllViews()
@@ -589,48 +679,33 @@ class HomeFragment : Fragment() {
others.forEach { p ->
val itemView = inflater.inflate(R.layout.item_rank_other, container, false)
itemView.findViewById<TextView>(R.id.tv_rank).text = (p.rank ?: "--").toString()
itemView.findViewById<TextView>(R.id.tv_name).text = p.characterName ?: ""
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
// 头像
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
// Glide 示例
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
itemView.setOnClickListener {
val id = p.id
Log.d("HomeFragment", "list1 others click id=$id")
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
PersonaDetailDialogFragment
.newInstance(id)
.newInstance(p.id)
.show(childFragmentManager, "persona_detail")
}
// 只点“添加”按钮
itemView.findViewById<View>(R.id.btn_add).setOnClickListener {
val id = p.id
lifecycleScope.launch {
if(p.added == true){
//取消收藏
p.id?.let { id ->
viewLifecycleOwner.lifecycleScope.launch {
try {
RetrofitClient.apiService.delUserCharacter(id.toInt())
} catch (e: Exception) {
// 处理错误
}
}
if (p.added == true) {
p.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
} else {
val addPersonaRequest = AddPersonaClick(
val req = AddPersonaClick(
characterId = p.id?.toInt() ?: 0,
emoji = p.emoji ?: ""
)
try {
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
} catch (e: Exception) {
// 处理错误
RetrofitClient.apiService.addUserCharacter(req)
}
} catch (_: Exception) {
}
}
}
@@ -644,68 +719,53 @@ class HomeFragment : Fragment() {
avatarId: Int,
nameId: Int,
addBtnId: Int,
container: 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 container = root.findViewById<LinearLayout>(container)
val container = root.findViewById<LinearLayout>(containerId)
if (item == null) {
// 没数据就隐藏(或者显示占位)
// avatar.isVisible = false
name.isVisible = false
addBtn.isVisible = false
return
}
avatar.isVisible = true
name.isVisible = true
addBtn.isVisible = true
name.text = item.characterName ?: ""
// 头像
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
addBtn.setOnClickListener {
val id = item.id
lifecycleScope.launch {
if(item.added == true){
//取消收藏
item.id?.let { id ->
viewLifecycleOwner.lifecycleScope.launch {
try {
RetrofitClient.apiService.delUserCharacter(id.toInt())
} catch (e: Exception) {
// 处理错误
}
}
if (item.added == true) {
item.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
} else {
val addPersonaRequest = AddPersonaClick(
val req = AddPersonaClick(
characterId = item.id?.toInt() ?: 0,
emoji = item.emoji ?: ""
)
try {
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
} catch (e: Exception) {
// 处理错误
RetrofitClient.apiService.addUserCharacter(req)
}
} catch (_: Exception) {
}
}
}
container.setOnClickListener {
val id = item.id
Log.d("HomeFragment", "list1 top click id=$id rank=${item.rank}")
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
PersonaDetailDialogFragment
.newInstance(id)
.newInstance(item.id)
.show(childFragmentManager, "persona_detail")
}
}
// ---------------- 网络请求 ----------------
private suspend fun fetchPersonaByTag(tagId: Int): List<listByTagWithNotLogin> {
return try {
val resp = if (!isLoggedIn()) {
@@ -715,54 +775,36 @@ class HomeFragment : Fragment() {
}
resp.data ?: emptyList()
} catch (e: Exception) {
if(!isLoggedIn()){
//未登录用户获取人设列表
Log.e("1314520-HomeFragment", "未登录根据标签获取人设列表", e)
}else{
Log.e("1314520-HomeFragment", "登录根据标签获取人设列表", e)
}
Log.e("1314520-HomeFragment", "按标签获取人设列表失败 tagId=$tagId", e)
emptyList()
}
}
private suspend fun fetchAllPersonaList(): List<listByTagWithNotLogin> {
return try {
val personaData = if (!isLoggedIn()) {
val resp = if (!isLoggedIn()) {
RetrofitClient.apiService.personaListWithNotLogin()
} else {
RetrofitClient.apiService.personaByTag()
}
personaData.data ?: emptyList()
resp.data ?: emptyList()
} catch (e: Exception) {
if(!isLoggedIn()){
//未登录用户获取人设列表
Log.e("1314520-HomeFragment", "未登录用户人设列表", e)
}else{
Log.e("1314520-HomeFragment", "登录用户人设列表", e)
}
Log.e("1314520-HomeFragment", "获取列表一失败", e)
emptyList()
}
}
suspend fun getpersonaLis(id: Int): ApiResponse<List<listByTagWithNotLogin>>? {
return try {
RetrofitClient.apiService.personaListByTag(id)
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "未登录用户按标签查询人设列表", e)
null
}
}
suspend fun loggedInGetpersonaLis(id: Int): ApiResponse<List<listByTagWithNotLogin>>? {
return try {
RetrofitClient.apiService.loggedInPersonaListByTag(id)
} catch (e: Exception) {
Log.e("1314520-HomeFragment", "登录用户按标签查询人设列表", e)
null
}
}
// ✅ 不再 requireContext(),避免 detach 直接崩
private fun isLoggedIn(): Boolean {
return EncryptedSharedPreferencesUtil.contains(requireContext(), "user")
val ctx = context ?: return false
return EncryptedSharedPreferencesUtil.contains(ctx, "user")
}
// ✅ 统一安全导航stateSaved 防护(切很快/后台回来时很重要)
private fun safeNavigate(actionId: Int) {
if (!isAdded) return
if (parentFragmentManager.isStateSaved) return
runCatching { findNavController().navigate(actionId) }
.onFailure { Log.e("HomeFragment", "navigate fail: $actionId", it) }
}
}

View File

@@ -40,6 +40,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedInputStream
import java.io.FileInputStream
import com.example.myapplication.ui.shop.ShopEvent
import com.example.myapplication.ui.shop.ShopEventBus
class KeyboardDetailFragment : Fragment() {
@@ -291,6 +293,8 @@ class KeyboardDetailFragment : Fragment() {
if (response?.code == 0) {
loadData()
}
ShopEventBus.post(ShopEvent.ThemePurchased)
} catch (e: Exception) {
Log.e("KeyboardDetailFragment", "购买主题失败", e)
}

View File

@@ -0,0 +1,43 @@
package com.example.myapplication.ui.common
import android.content.Context
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputConnectionWrapper
import androidx.appcompat.widget.AppCompatEditText
class CodeEditText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {
var onDelPressed: (() -> Unit)? = null
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
val ic = super.onCreateInputConnection(outAttrs)
return object : InputConnectionWrapper(ic, true) {
// 软键盘删除通常走这个
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
if (beforeLength == 1 && afterLength == 0) {
onDelPressed?.invoke()
return true
}
return super.deleteSurroundingText(beforeLength, afterLength)
}
// 一些键盘会直接发 KeyEvent DEL
override fun sendKeyEvent(event: android.view.KeyEvent): Boolean {
if (event.action == android.view.KeyEvent.ACTION_DOWN &&
event.keyCode == android.view.KeyEvent.KEYCODE_DEL
) {
onDelPressed?.invoke()
return true
}
return super.sendKeyEvent(event)
}
}
}
}

View File

@@ -5,14 +5,25 @@ 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
import com.google.android.material.textfield.TextInputLayout
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.example.myapplication.R
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.example.myapplication.network.SendVerifyCodeRequest
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.util.Log
class ForgetPasswordEmailFragment : Fragment() {
private lateinit var emailEditText: EditText // 邮箱输入框
private var loadingOverlay: com.example.myapplication.ui.common.LoadingOverlay? = null // 加载遮罩层
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -20,12 +31,63 @@ class ForgetPasswordEmailFragment : Fragment() {
): View? {
return inflater.inflate(R.layout.fragment_forget_password_email, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//验证码页面
// 初始化加载遮罩层
loadingOverlay = com.example.myapplication.ui.common.LoadingOverlay.attach(view as ViewGroup)
emailEditText = view.findViewById<EditText>(R.id.et_email)
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
// 下一步按钮点击事件
view.findViewById<TextView>(R.id.nextstep).setOnClickListener {
// 对输入框去除首尾空格
val email = emailEditText.text.toString().trim()
// 判断邮箱是否为空
if (email.isEmpty()) {
Toast.makeText(activity, "Please enter your email address", Toast.LENGTH_SHORT).show()
} else if (!isValidEmail(email)) {
Toast.makeText(activity, "The email address format is incorrect", Toast.LENGTH_SHORT).show()
} else {
loadingOverlay?.show()
viewLifecycleOwner.lifecycleScope.launch {
try {
val body = SendVerifyCodeRequest(email)
val response = RetrofitClient.apiService.sendVerifyCode(body)
if (response.code == 0) {
EncryptedSharedPreferencesUtil.save(requireContext(), "forget_email", email)
findNavController().navigate(R.id.action_forgetPasswordEmailFragment_to_forgetPasswordVerifyFragment)
Toast.makeText(activity, "A verification email has been sent to ${email}. Please check your inbox to complete the verification.", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Log.e("1314520-MineFragment", "发送验证码失败", e)
} finally {
loadingOverlay?.hide()
}
}
}
}
}
override fun onDestroyView() {
loadingOverlay?.remove()
loadingOverlay = null
super.onDestroyView()
}
/**
* 验证邮箱格式是否有效
* @param email 要验证的邮箱地址
* @return 如果邮箱格式有效返回true否则返回false
*/
private fun isValidEmail(email: String): Boolean {
val emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
return email.matches(emailRegex.toRegex())
}
}

View File

@@ -2,13 +2,36 @@
package com.example.myapplication.ui.login
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.example.myapplication.R
import com.example.myapplication.network.ResetPasswordRequest
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.ui.common.LoadingOverlay
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import kotlinx.coroutines.launch
class ForgetPasswordResetFragment : Fragment() {
private lateinit var passwordEditText: EditText // 密码输入框
private lateinit var confirmPasswordEditText: EditText // 确认密码输入框
private lateinit var toggleImageView: android.widget.ImageView // 密码显示/隐藏按钮
private lateinit var confirmtoggleImageView: android.widget.ImageView // 确认密码显示/隐藏按钮
private var loadingOverlay: LoadingOverlay? = null // 加载遮罩层
private var isPasswordVisible = false // 密码可见性状态
private var isConfirmPasswordVisible = false // 确认密码可见性状态
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -17,5 +40,98 @@ class ForgetPasswordResetFragment : Fragment() {
return inflater.inflate(R.layout.fragment_forget_password_reset, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
passwordEditText = view.findViewById<EditText>(R.id.et_password)
confirmPasswordEditText = view.findViewById<EditText>(R.id.et_confirm_password)
toggleImageView = view.findViewById(R.id.iv_toggle)
confirmtoggleImageView = view.findViewById(R.id.iv_confirm_toggle)
// 初始化加载遮罩层
loadingOverlay = LoadingOverlay.attach(view as ViewGroup)
// 密码显示/隐藏按钮点击事件
passwordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
toggleImageView.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// 显示密码
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
toggleImageView.setImageResource(R.drawable.display)
} else {
// 隐藏密码
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
toggleImageView.setImageResource(R.drawable.hide)
}
// 保持光标在末尾
passwordEditText.setSelection(passwordEditText.text?.length ?: 0)
}
// 确认密码显示/隐藏按钮点击事件
confirmPasswordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
confirmtoggleImageView.setOnClickListener {
isConfirmPasswordVisible = !isConfirmPasswordVisible
if (isConfirmPasswordVisible) {
// 显示密码
confirmPasswordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
confirmtoggleImageView.setImageResource(R.drawable.display)
} else {
// 隐藏密码
confirmPasswordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
confirmtoggleImageView.setImageResource(R.drawable.hide)
}
// 保持光标在末尾
confirmPasswordEditText.setSelection(confirmPasswordEditText.text?.length ?: 0)
}
val savedEmail = EncryptedSharedPreferencesUtil.get(requireContext(), "forget_email", String::class.java)
view.findViewById<TextView>(R.id.nextstep).setOnClickListener {
val password = passwordEditText.text.toString().trim()
val confirmPassword = confirmPasswordEditText.text.toString().trim()
//判断密码,邮箱,确认密码是否为空
if (password.isEmpty() || confirmPassword.isEmpty()) {
Toast.makeText(activity, "Please fill in the complete information", Toast.LENGTH_SHORT).show()
} else if (password != confirmPassword) {
Toast.makeText(activity, "The two password entries are inconsistent", Toast.LENGTH_SHORT).show()
} else {
loadingOverlay?.show()
viewLifecycleOwner.lifecycleScope.launch {
try {
val body = ResetPasswordRequest(
mailAddress = savedEmail!!,
password = password,
confirmPassword = confirmPassword
)
val response = RetrofitClient.apiService.resetPassword(body)
if (response.code == 0) {
EncryptedSharedPreferencesUtil.remove(requireContext(), "forget_email")
Toast.makeText(activity, "Password reset successful. Please log in again.", Toast.LENGTH_LONG).show()
// 使用忘记密码专用的action跳转到登录页面
findNavController().navigate(R.id.action_global_loginFragment_from_forget_password)
}
} catch (e: Exception) {
Log.e("1314520-MineFragment", "重置密码失败", e)
} finally {
loadingOverlay?.hide()
}
}
}
}
}
}

View File

@@ -9,15 +9,23 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.example.myapplication.R
import android.widget.TextView
import androidx.navigation.fragment.findNavController
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.VerifyCodeRequest
import android.util.Log
class ForgetPasswordVerifyFragment : Fragment() {
private lateinit var codeInputs: List<EditText>
private var loadingOverlay: com.example.myapplication.ui.common.LoadingOverlay? = null//加载遮罩层
override fun onCreateView(
inflater: LayoutInflater,
@@ -29,12 +37,47 @@ class ForgetPasswordVerifyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingOverlay = com.example.myapplication.ui.common.LoadingOverlay.attach(view as ViewGroup)
//验证码页面
view.findViewById<
TextView>(R.id.nextstep).setOnClickListener {
val savedEmail = EncryptedSharedPreferencesUtil.get(requireContext(), "forget_email", String::class.java)
view.findViewById<TextView>(R.id.tv_code_hint).setText("A verification email has been sent to ${savedEmail}. Please check your inbox to complete the verification.")
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
view.findViewById<TextView>(R.id.nextstep).setOnClickListener {
// 验证验证码
val verifyCode = getVerifyCode()
if (verifyCode.length != 6) {
Toast.makeText(activity, "The verification code format is incorrect", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// 显示加载遮罩层
loadingOverlay?.show()
viewLifecycleOwner.lifecycleScope.launch {
try {
val body = VerifyCodeRequest(
mailAddress = savedEmail!!,
verifyCode = verifyCode
)
val response = RetrofitClient.apiService.verifyCode(body)
if (response.code == 0 && response.data == true){
Toast.makeText(activity, "The verification code has been verified successfully", Toast.LENGTH_SHORT).show()
findNavController().navigate(R.id.action_forgetPasswordVerifyFragment_to_forgetPasswordResetFragment)
}
} catch (e: Exception) {
Log.e("1314520-MineFragment", "验证码验证失败", e)
} finally {
// 隐藏加载遮罩层
loadingOverlay?.hide()
}
}
}
codeInputs = listOf<EditText>(
view.findViewById(R.id.et_code_1),

View File

@@ -21,6 +21,11 @@ import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.button.MaterialButton
import android.widget.Toast
import kotlinx.coroutines.launch
import androidx.fragment.app.setFragmentResult
import com.example.myapplication.ui.mine.MineFragment
import androidx.core.os.bundleOf
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.AuthEvent
class LoginFragment : Fragment() {
@@ -52,18 +57,14 @@ class LoginFragment : Fragment() {
// 注册
view.findViewById<TextView>(R.id.tv_signup).setOnClickListener {
findNavController().navigate(R.id.action_mineFragment_to_registerFragment)
findNavController().navigate(R.id.action_loginFragment_to_registerFragment)
}
// 忘记密码
view.findViewById<TextView>(R.id.tv_forgot_password).setOnClickListener {
findNavController().navigate(R.id.action_loginFragment_to_forgetPasswordEmailFragment)
}
// 返回按钮
// 返回 - 在global_graph中直接popBackStack回到globalEmptyFragment
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
findNavController().previousBackStackEntry
?.savedStateHandle
?.set("from_login", true)
findNavController().popBackStack()
}
// 绑定控件id 必须和 xml 里的一样)
@@ -122,7 +123,9 @@ class LoginFragment : Fragment() {
if (response.code == 0) {
EncryptedSharedPreferencesUtil.save(requireContext(), "user", response.data)
EncryptedSharedPreferencesUtil.save(requireContext(), "email",email)
findNavController().popBackStack()
// 触发登录成功事件让MainActivity关闭全局overlay
AuthEventBus.emit(AuthEvent.LoginSuccess)
// 不在这里popBackStack让MainActivity的LoginSuccess事件处理关闭全局overlay
} else {
Toast.makeText(requireContext(), "Login failed: ${response.message}", Toast.LENGTH_SHORT).show()
}
@@ -136,4 +139,8 @@ class LoginFragment : Fragment() {
}
}
}
private fun isLoggedIn(): Boolean {
return EncryptedSharedPreferencesUtil.contains(requireContext(), "user")
}
}

View File

@@ -1,17 +1,40 @@
package com.example.myapplication.ui.login
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import android.widget.FrameLayout
import android.widget.TextView
import com.example.myapplication.R
import androidx.lifecycle.lifecycleScope
import com.example.myapplication.network.SendVerifyCodeRequest
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.example.myapplication.ui.common.LoadingOverlay
import kotlinx.coroutines.launch
import android.util.Log
class RegisterFragment : Fragment() {
private lateinit var passwordEditText: EditText // 密码输入框
private lateinit var confirmPasswordEditText: EditText // 确认密码输入框
private lateinit var emailEditText: EditText // 邮箱输入框
private lateinit var toggleImageView: ImageView // 密码显示/隐藏按钮
private lateinit var confirmtoggleImageView: ImageView // 确认密码显示/隐藏按钮
private lateinit var nextStepButton: TextView // 下一步按钮
private var isPasswordVisible = false
private var isConfirmPasswordVisible = false
private var loadingOverlay: LoadingOverlay? = null // 加载遮罩层
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -23,9 +46,114 @@ class RegisterFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toggleImageView = view.findViewById<ImageView>(R.id.iv_toggle)
confirmtoggleImageView = view.findViewById<ImageView>(R.id.iv_confirm_toggle)
passwordEditText = view.findViewById<EditText>(R.id.et_password)
confirmPasswordEditText = view.findViewById<EditText>(R.id.et_confirm_password)
emailEditText = view.findViewById<EditText>(R.id.et_email)
nextStepButton = view.findViewById<TextView>(R.id.btn_next_step)
// 初始化加载遮罩层
loadingOverlay = LoadingOverlay.attach(view as ViewGroup)
// 密码显示/隐藏按钮点击事件
passwordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
toggleImageView.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// 显示密码
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
toggleImageView.setImageResource(R.drawable.display)
} else {
// 隐藏密码
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
toggleImageView.setImageResource(R.drawable.hide)
}
// 保持光标在末尾
passwordEditText.setSelection(passwordEditText.text?.length ?: 0)
}
// 确认密码显示/隐藏按钮点击事件
confirmPasswordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
confirmtoggleImageView.setOnClickListener {
isConfirmPasswordVisible = !isConfirmPasswordVisible
if (isConfirmPasswordVisible) {
// 显示密码
confirmPasswordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
confirmtoggleImageView.setImageResource(R.drawable.display)
} else {
// 隐藏密码
confirmPasswordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
confirmtoggleImageView.setImageResource(R.drawable.hide)
}
// 保持光标在末尾
confirmPasswordEditText.setSelection(confirmPasswordEditText.text?.length ?: 0)
}
// 下一步按钮点击事件
nextStepButton.setOnClickListener {
// 对输入框去除首尾空格
val email = emailEditText.text.toString().trim()
val password = passwordEditText.text.toString().trim()
val confirmPassword = confirmPasswordEditText.text.toString().trim()
//判断密码,邮箱,确认密码是否为空
if (password.isEmpty() || email.isEmpty() || confirmPassword.isEmpty()) {
Toast.makeText(activity, "Please fill in the complete information", Toast.LENGTH_SHORT).show()
} else if (password != confirmPassword) {
Toast.makeText(activity, "The two password entries are inconsistent", Toast.LENGTH_SHORT).show()
} else if (!isValidEmail(email)) {
Toast.makeText(activity, "The email address format is incorrect", Toast.LENGTH_SHORT).show()
} else {
loadingOverlay?.show()
viewLifecycleOwner.lifecycleScope.launch {
try {
val body = SendVerifyCodeRequest(email)
val response = RetrofitClient.apiService.sendVerifyCode(body)
if (response.code == 0) {
EncryptedSharedPreferencesUtil.save(requireContext(), "register_email", email)
EncryptedSharedPreferencesUtil.save(requireContext(), "register_password", password)
EncryptedSharedPreferencesUtil.save(requireContext(), "register_confirm_password", confirmPassword)
findNavController().navigate(R.id.action_registerFragment_to_registerVerifyFragment)
Toast.makeText(activity, "A verification email has been sent to ${email}. Please check your inbox to complete the verification.", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Log.e("1314520-MineFragment", "获取失败", e)
} finally {
loadingOverlay?.hide()
}
}
}
}
// 返回按钮
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
}
override fun onDestroyView() {
loadingOverlay?.remove()
super.onDestroyView()
}
// 验证邮箱格式是否有效
// @param email 要验证的邮箱地址
// @return 如果邮箱格式有效返回true否则返回false
private fun isValidEmail(email: String): Boolean {
val emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
return email.matches(emailRegex.toRegex())
}
}

View File

@@ -0,0 +1,175 @@
// 忘记密码验证码输入页面
package com.example.myapplication.ui.login
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.example.myapplication.R
import com.example.myapplication.network.RegisterRequest
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.ui.common.CodeEditText
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import kotlinx.coroutines.launch
import android.widget.TextView
class RegisterVerifyFragment : Fragment() {
private lateinit var codeInputs: List<CodeEditText>
private var loadingOverlay: com.example.myapplication.ui.common.LoadingOverlay? = null // 加载遮罩层
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_register_verify, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 初始化加载遮罩层
loadingOverlay = com.example.myapplication.ui.common.LoadingOverlay.attach(view as ViewGroup)
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
parentFragmentManager.popBackStack()
}
// 注意:布局里的 6 个输入框需要是 CodeEditText
codeInputs = listOf(
view.findViewById(R.id.et_code_1),
view.findViewById(R.id.et_code_2),
view.findViewById(R.id.et_code_3),
view.findViewById(R.id.et_code_4),
view.findViewById(R.id.et_code_5),
view.findViewById(R.id.et_code_6)
)
setupVerifyCodeInputs()
val savedEmail = EncryptedSharedPreferencesUtil.get(requireContext(), "register_email", String::class.java)
val savedPassword = EncryptedSharedPreferencesUtil.get(requireContext(), "register_password", String::class.java)
val savedConfirmPassword = EncryptedSharedPreferencesUtil.get(requireContext(), "register_confirm_password", String::class.java)
val savedGender = EncryptedSharedPreferencesUtil.get(requireContext(), "gender", String::class.java)
val savedInviteCode = EncryptedSharedPreferencesUtil.get(requireContext(), "inviteCode", String::class.java)
view.findViewById<TextView>(R.id.tv_code_hint).text =
"A verification email has been sent to ${savedEmail}. Please check your inbox to complete the verification."
view.findViewById<TextView>(R.id.nextstep).setOnClickListener {
val verifyCode = getVerifyCode()
if (verifyCode.length != 6) {
Toast.makeText(activity, "The verification code format is incorrect", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
loadingOverlay?.show()
viewLifecycleOwner.lifecycleScope.launch {
try {
val body = RegisterRequest(
mailAddress = savedEmail!!,
password = savedPassword!!,
passwordConfirm = savedConfirmPassword!!,
gender = savedGender?.toIntOrNull() ?: 0,
verifyCode = verifyCode,
inviteCode = savedInviteCode
)
val response = RetrofitClient.apiService.register(body)
if (response.code == 0) {
Toast.makeText(activity, "Registration successful", Toast.LENGTH_SHORT).show()
EncryptedSharedPreferencesUtil.remove(requireContext(), "register_email")
EncryptedSharedPreferencesUtil.remove(requireContext(), "register_password")
EncryptedSharedPreferencesUtil.remove(requireContext(), "register_confirm_password")
findNavController().navigate(R.id.action_global_loginFragment)
}
} catch (e: Exception) {
Log.e("1314520-MineFragment", "注册失败", e)
} finally {
loadingOverlay?.hide()
}
}
}
}
override fun onDestroyView() {
loadingOverlay?.remove()
super.onDestroyView()
}
private fun setupVerifyCodeInputs() {
codeInputs.forEachIndexed { index, editText ->
// 输入监听:自动跳到下一格;多字符只取最后一位(可应付部分粘贴/联想)
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val text = s?.toString().orEmpty()
if (text.length > 1) {
editText.setText(text.last().toString())
editText.setSelection(1)
}
if (text.isNotEmpty() && index < codeInputs.size - 1) {
codeInputs[index + 1].requestFocus()
}
}
})
// ✅ 删除统一走这里:软键盘 deleteSurroundingText + 硬件 DEL 都能触发
editText.onDelPressed = {
handleDeleteAction(index, editText)
}
}
}
// 获取完整验证码
private fun getVerifyCode(): String {
return codeInputs.joinToString("") { it.text?.toString().orEmpty() }
}
/**
* 删除逻辑:当前有内容先删当前;当前为空就跳到前面最近有内容的格子并删除
*/
private fun handleDeleteAction(index: Int, currentEditText: CodeEditText) {
// 1) 当前格有内容:清空当前格
if (!currentEditText.text.isNullOrEmpty()) {
currentEditText.text?.clear()
return
}
// 2) 当前为空:向前找最近一个有内容的格子,跳过去并清空
var targetIndex = index - 1
while (targetIndex >= 0) {
val targetEditText = codeInputs[targetIndex]
if (!targetEditText.text.isNullOrEmpty()) {
targetEditText.requestFocus()
targetEditText.text?.clear()
targetEditText.post { targetEditText.setSelection(0) }
return
}
targetIndex--
}
// 3) 前面也都空:就停在第一个
if (index > 0) {
val first = codeInputs[0]
first.requestFocus()
first.post { first.setSelection(0) }
}
}
}

View File

@@ -1,23 +1,32 @@
package com.example.myapplication.ui.mine
import android.os.Bundle
import android.util.Log
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 android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.example.myapplication.R
import android.widget.LinearLayout
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.AuthEvent
import com.example.myapplication.network.AuthEventBus
import com.example.myapplication.network.LoginResponse
import de.hdodenhof.circleimageview.CircleImageView
import android.util.Log
import kotlinx.coroutines.launch
import android.widget.TextView
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import androidx.navigation.navOptions
import de.hdodenhof.circleimageview.CircleImageView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class MineFragment : Fragment() {
@@ -25,129 +34,179 @@ class MineFragment : Fragment() {
private lateinit var time: TextView
private lateinit var logout: TextView
private var loadUserJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_mine, container, false)
}
): View = inflater.inflate(R.layout.fragment_mine, container, false)
override fun onDestroyView() {
loadUserJob?.cancel()
super.onDestroyView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 判断是否登录(门禁)
if (!isLoggedIn()) {
val nav = findNavController()
// 改用 savedStateHandle 的标记LoginFragment 返回时写入
val fromLogin = nav.currentBackStackEntry
?.savedStateHandle
?.get<Boolean>("from_login") == true
// 用完就清掉
nav.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("from_login")
view?.post {
try {
if (fromLogin) {
// 从登录页回来仍未登录:跳首页
nav.navigate(R.id.action_global_homeFragment)
} else {
// 不是从登录页来:跳登录
nav.navigate(R.id.action_mineFragment_to_loginFragment)
}
} catch (e: IllegalArgumentException) {
// 万一你的导航框架在当前时机解析 action 有问题,兜底:直接去目标 Fragment id
if (fromLogin) {
nav.navigate(R.id.homeFragment)
} else {
nav.navigate(R.id.loginFragment)
}
}
}
return
}
nickname = view.findViewById(R.id.nickname)
time = view.findViewById(R.id.time)
logout = view.findViewById(R.id.logout)
// 1) 先用本地缓存秒出首屏
renderFromCache()
// 获取用户信息, 并显示
val user = EncryptedSharedPreferencesUtil.get(requireContext(), "Personal_information", LoginResponse::class.java)
nickname.text = user?.nickName ?: ""
time.text = user?.vipExpiry?.let { "Due on November $it" } ?: ""
// 2) 首次进入不刷新由onResume处理
// 2) 下一帧再请求网络(让首帧先出来)
view.post {
// // ✅ 手动刷新:不改布局也能用
// // - 点昵称刷新
// nickname.setOnClickListener { refreshUser(force = true, showToast = true) }
// // - 长按 time 刷新
// time.setOnLongClickListener {
// refreshUser(force = true, showToast = true)
// true
// }
logout.setOnClickListener {
LogoutDialogFragment { doLogout() }
.show(parentFragmentManager, "logout_dialog")
}
view.findViewById<ImageView>(R.id.imgLeft).setOnClickListener {
// 使用事件总线打开充值页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
}
view.findViewById<ImageView>(R.id.imgRight).setOnClickListener {
// 使用事件总线打开金币充值页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
view.findViewById<CircleImageView>(R.id.avatar).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_personalSettings)
}
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_mykeyboard)
}
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_feedbackFragment)
}
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
safeNavigate(R.id.action_mineFragment_to_notificationFragment)
}
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
AuthEventBus.events.collect { event ->
when (event) {
AuthEvent.LoginSuccess -> {
// 先从本地秒渲染,再强制打接口更新
renderFromCache()
refreshUser(force = true, showToast = false)
}
else -> Unit
}
}
}
}
}
override fun onResume() {
super.onResume()
// ✅ 回到页面先用缓存渲染一遍,再尝试刷新(不强制)
renderFromCache()
refreshUser(force = false)
}
private fun renderFromCache() {
if (!isAdded) return
val cached = EncryptedSharedPreferencesUtil.get(
requireContext(),
"Personal_information",
LoginResponse::class.java
)
nickname.text = cached?.nickName ?: ""
time.text = cached?.vipExpiry?.let { "Due on November $it" } ?: ""
}
/**
* 刷新用户信息:
* - force=true无条件打接口
* - force=false只要已登录就打一次你可以后续加防抖这里先保证“必能拿到数据”
*/
private fun refreshUser(force: Boolean, showToast: Boolean = false) {
if (!isLoggedIn()) {
if (showToast && isAdded) Toast.makeText(requireContext(), "未登录", Toast.LENGTH_SHORT).show()
return
}
loadUserJob?.cancel()
loadUserJob = viewLifecycleOwner.lifecycleScope.launch {
try {
val response = RetrofitClient.apiService.getUser()
nickname.text = response.data?.nickName ?: ""
time.text = response.data?.vipExpiry?.let { "Due on November $it" } ?: ""
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", response.data)
if (!isAdded) return@launch
val u = response.data
Log.d(TAG, "getUser ok: nick=${u?.nickName} vip=${u?.vipExpiry}")
nickname.text = u?.nickName ?: ""
time.text = u?.vipExpiry?.let { "Due on November $it" } ?: ""
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", u)
if (showToast) Toast.makeText(requireContext(), "已刷新", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("1314520-MineFragment", "获取失败", e)
if (e is kotlinx.coroutines.CancellationException) return@launch
Log.e(TAG, "getUser failed", e)
if (showToast && isAdded) Toast.makeText(requireContext(), "刷新失败", Toast.LENGTH_SHORT).show()
}
}
}
// 退出登录(先确认)
logout.setOnClickListener {
LogoutDialogFragment {
// ✅ 用户确认后才执行
private fun doLogout() {
viewLifecycleOwner.lifecycleScope.launch {
try {
val response = RetrofitClient.apiService.logout()
if (!isAdded) return@launch
if (response.code == 0) {
EncryptedSharedPreferencesUtil.remove(requireContext(), "Personal_information")
EncryptedSharedPreferencesUtil.remove(requireContext(), "user")
// ⚠️ 建议用 popUpTo 清栈,避免按返回回到已登录页面
findNavController().navigate(R.id.action_mineFragment_to_loginFragment)
}.show(parentFragmentManager, "logout_dialog")
// 清空 UI
nickname.text = ""
time.text = ""
// 触发登出事件让MainActivity打开登录页面
AuthEventBus.emit(AuthEvent.Logout(returnTabTag = "tab_mine"))
} else {
Log.e(TAG, "logout fail code=${response.code}")
}
} catch (e: Exception) {
Log.e(TAG, "logout exception", e)
}
}
}
// 会员充值按钮点击
view.findViewById<ImageView>(R.id.imgLeft).setOnClickListener {
findNavController().navigate(R.id.action_global_rechargeFragment)
}
// 金币充值按钮点击
view.findViewById<ImageView>(R.id.imgRight).setOnClickListener {
findNavController().navigate(R.id.action_global_goldCoinRechargeFragment)
}
// 头像点击
view.findViewById<CircleImageView>(R.id.avatar).setOnClickListener {
findNavController().navigate(R.id.action_mineFragment_to_personalSettings)
}
//我的键盘
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
findNavController().navigate(R.id.action_mineFragment_to_mykeyboard)
}
// 反馈按钮点击
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
findNavController().navigate(R.id.action_mineFragment_to_feedbackFragment)
}
// 反馈按钮点击
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
findNavController().navigate(R.id.action_mineFragment_to_notificationFragment)
}
//隐私政策
// view.findViewById<LinearLayout>(R.id.click_Privacy).setOnClickListener {
// findNavController().navigate(R.id.action_mineFragment_to_loginFragment)
// }
}
private fun isLoggedIn(): Boolean {
return EncryptedSharedPreferencesUtil.contains(requireContext(), "user")
val ctx = context ?: return false
return EncryptedSharedPreferencesUtil.contains(ctx, "user")
}
private fun safeNavigate(actionId: Int) {
if (!isAdded) return
if (parentFragmentManager.isStateSaved) return
val navController: NavController = findNavController()
runCatching { navController.navigate(actionId) }
.onFailure { Log.e(TAG, "navigate error: $actionId", it) }
}
companion object {
private const val TAG = "1314520-MineFragment"
}
}

View File

@@ -0,0 +1,20 @@
package com.example.myapplication.ui.shop
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
sealed class ShopEvent {
object ThemePurchased : ShopEvent()
}
object ShopEventBus {
private val _events = MutableSharedFlow<ShopEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events = _events.asSharedFlow()
fun post(event: ShopEvent) {
_events.tryEmit(event)
}
}

View File

@@ -1,6 +1,5 @@
package com.example.myapplication.ui.shop
import android.annotation.SuppressLint
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.graphics.Color
@@ -14,41 +13,391 @@ import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.example.myapplication.R
import com.example.myapplication.network.ApiResponse
import com.example.myapplication.network.RetrofitClient
import com.example.myapplication.network.Theme
import com.example.myapplication.network.Wallet
import com.example.myapplication.network.themeStyle
import com.example.myapplication.network.*
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
class ShopFragment : Fragment(R.layout.fragment_shop) {
// ===== View =====
private lateinit var viewPager: ViewPager2
private lateinit var tagScroll: HorizontalScrollView
private lateinit var tagContainer: LinearLayout
private lateinit var balance: TextView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
// 风格 tabs
// ===== Data =====
private var tabTitles: List<Theme> = emptyList()
private var styleIds: List<Int> = emptyList()
// ✅ 共享数据/缓存/加载都交给 VM
private val vm: ShopViewModel by viewModels()
// ===== ViewModel =====
private lateinit var vm: ShopViewModel
// ===== 状态控制 =====
private var uiInited = false // ⭐ 核心UI 是否已初始化
private var loadJob: Job? = null
private val refreshing = AtomicBoolean(false)
private var pageCallback: ViewPager2.OnPageChangeCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProvider(this)[ShopViewModel::class.java]
}
override fun onDestroyView() {
loadJob?.cancel()
pageCallback?.let { viewPager.unregisterOnPageChangeCallback(it) }
pageCallback = null
uiInited = false
super.onDestroyView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
ShopEventBus.events.collect { event ->
when (event) {
ShopEvent.ThemePurchased -> {
vm.clearCache()
refreshData() // ✅ 直接走你现有的刷新逻辑
}
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
AuthEventBus.events.collect { event ->
when (event) {
AuthEvent.LoginSuccess -> {
// 登录态变化会影响钱包、主题列表、已购/可用状态:建议全量刷新
if (!uiInited) {
uiInited = true
loadInitialData()
} else {
vm.clearCache()
refreshData() // 你已有:会 updateBalance + 重新拉主题 + forceLoadStyle
}
}
else -> Unit
}
}
}
}
bindViews(view)
bindClicks(view)
setupSwipe()
fixViewPager2SwipeConflict()
if (!uiInited) {
uiInited = true
loadInitialData()
} else {
// ⭐ 再切回来:只刷新轻量数据
refreshBalanceOnly()
}
}
// ========================== load ==========================
private fun loadInitialData() {
loadJob?.cancel()
loadJob = viewLifecycleOwner.lifecycleScope.launch {
// 钱包
updateBalance(getwalletBalance())
// 主题
val themeResp = getThemeList()
tabTitles = themeResp?.data ?: emptyList()
styleIds = tabTitles.map { it.id }
setupViewPagerOnce()
setupTagsOnce()
styleIds.firstOrNull()?.let { vm.loadStyleIfNeeded(it) }
}
}
private fun refreshBalanceOnly() {
viewLifecycleOwner.lifecycleScope.launch {
updateBalance(getwalletBalance())
}
}
private fun refreshData() {
loadJob?.cancel()
loadJob = viewLifecycleOwner.lifecycleScope.launch {
try {
updateBalance(getwalletBalance())
val newThemes = getThemeList()?.data ?: emptyList()
if (newThemes != tabTitles) {
tabTitles = newThemes
styleIds = tabTitles.map { it.id }
setupViewPagerOnce(force = true)
setupTagsOnce(force = true)
vm.clearCache()
styleIds.forEach { vm.forceLoadStyle(it) }
} else {
styleIds.getOrNull(viewPager.currentItem)
?.let { vm.forceLoadStyle(it) }
}
} catch (e: Exception) {
Log.e("ShopFragment", "refresh error", e)
} finally {
refreshing.set(false)
swipeRefreshLayout.isRefreshing = false
}
}
}
// ========================== ViewPager ==========================
private fun setupViewPagerOnce(force: Boolean = false) {
if (viewPager.adapter != null && !force) return
pageCallback?.let { viewPager.unregisterOnPageChangeCallback(it) }
viewPager.adapter = ShopPagerAdapter(this, styleIds)
viewPager.offscreenPageLimit = 1
pageCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateTagState(position)
styleIds.getOrNull(position)?.let { vm.loadStyleIfNeeded(it) }
}
}
viewPager.registerOnPageChangeCallback(pageCallback!!)
}
private fun setupTagsOnce(force: Boolean = false) {
if (tagContainer.childCount > 0 && !force) return
tagContainer.removeAllViews()
val density = resources.displayMetrics.density
tabTitles.forEachIndexed { index, theme ->
val tv = TextView(requireContext()).apply {
text = theme.styleName
textSize = 12f
setPadding(
(16 * density).toInt(),
(6 * density).toInt(),
(16 * density).toInt(),
(6 * density).toInt()
)
background = createCapsuleBackground()
isSelected = index == 0
updateTagStyleNoAnim(this, isSelected)
setOnClickListener { viewPager.currentItem = index }
}
val gap = (6 * density).toInt() // 标签间距 6dp
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
marginEnd = if (index == tabTitles.lastIndex) 0 else gap
}
tagContainer.addView(tv, lp)
}
}
private fun updateTagState(position: Int) {
for (i in 0 until tagContainer.childCount) {
val tv = tagContainer.getChildAt(i) as TextView
val selected = i == position
if (tv.isSelected != selected) {
tv.isSelected = selected
updateTagStyleWithAnim(tv, selected)
if (selected) {
tv.post {
if (!isAdded) return@post
val scrollX = tv.left - (tagScroll.width - tv.width) / 2
tagScroll.smoothScrollTo(scrollX, 0)
}
}
}
}
}
// ========================== UI helpers ==========================
private fun updateBalance(walletResp: ApiResponse<Wallet>?) {
val value = walletResp?.data?.balanceDisplay ?: 0
val text = value.toString()
balance.text = text
adjustBalanceTextSize(text)
}
private fun adjustBalanceTextSize(text: String) {
balance.textSize = when (text.length) {
in 0..3 -> 40f
4 -> 36f
5 -> 32f
6 -> 28f
7 -> 24f
8 -> 22f
9 -> 20f
else -> 16f
}
}
private fun createCapsuleBackground(selected: Boolean = false): GradientDrawable =
GradientDrawable().apply {
val d = resources.displayMetrics.density
cornerRadius = 50f * d
if (selected) {
setColor(Color.WHITE)
setStroke((1 * d).toInt().coerceAtLeast(1), Color.parseColor("#02BEAC"))
} else {
setColor(Color.parseColor("#F1F1F1"))
setStroke(0, Color.TRANSPARENT)
}
}
private fun updateTagStyleNoAnim(tv: TextView, selected: Boolean) {
tv.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
tv.setTextColor(if (selected) Color.parseColor("#1B1F1A") else Color.parseColor("#9F9F9F"))
tv.background = createCapsuleBackground(selected)
}
private fun updateTagStyleWithAnim(tv: TextView, selected: Boolean) {
val start = if (selected) Color.parseColor("#9F9F9F") else Color.parseColor("#1B1F1A")
val end = if (selected) Color.parseColor("#1B1F1A") else Color.parseColor("#9F9F9F")
ValueAnimator.ofObject(ArgbEvaluator(), start, end).apply {
duration = 150
addUpdateListener { tv.setTextColor(it.animatedValue as Int) }
start()
}
tv.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
tv.background = createCapsuleBackground(selected)
}
private var appBarFullyExpanded = true
private fun setupSwipe() {
swipeRefreshLayout.setOnRefreshListener {
if (refreshing.compareAndSet(false, true)) {
refreshData()
} else {
swipeRefreshLayout.isRefreshing = false
}
}
setupSwipeRefreshConflictFix()
}
private fun setupSwipeRefreshConflictFix() {
val appBar = requireView().findViewById<AppBarLayout>(R.id.appBar)
// 1) 监听 AppBar 是否完全展开
appBar.addOnOffsetChangedListener { _, verticalOffset ->
appBarFullyExpanded = (verticalOffset == 0)
}
// 2) 核心:自定义"子 View 是否能向上滚"的判断
swipeRefreshLayout.setOnChildScrollUpCallback { _, _ ->
// AppBar 没完全展开:不要让刷新抢手势(优先展开/折叠头部)
if (!appBarFullyExpanded) return@setOnChildScrollUpCallback true
// 找到 ViewPager2 当前页的 RecyclerView
val rv = findCurrentPageRecyclerView()
// rv 能向上滚:说明列表不在顶部 -> 禁止触发刷新
rv?.canScrollVertically(-1) ?: false
}
}
private fun findCurrentPageRecyclerView(): RecyclerView? {
// FragmentStateAdapter 默认 tag 通常是 "f0", "f1"...
val pos = viewPager.currentItem
val f = childFragmentManager.findFragmentByTag("f$pos")
return f?.view?.findViewById(R.id.recyclerView)
}
private fun fixViewPager2SwipeConflict() {
(viewPager.getChildAt(0) as? RecyclerView)?.setOnTouchListener { v, e ->
when (e.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// 不再在 ACTION_DOWN 时就 disallow让父容器有机会判断
false
}
MotionEvent.ACTION_MOVE -> {
// 只对横向滑动明显时 disallow
val dx = Math.abs(e.x - v.x)
val dy = Math.abs(e.y - v.y)
if (dx > dy * 2) { // 横向滑动明显
v.parent?.requestDisallowInterceptTouchEvent(true)
}
false
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
v.parent?.requestDisallowInterceptTouchEvent(false)
false
}
}
false
}
}
// ========================== Adapter ==========================
private inner class ShopPagerAdapter(
fragment: Fragment,
private val styleIds: List<Int>
) : FragmentStateAdapter(fragment) {
override fun getItemCount() = styleIds.size
override fun createFragment(position: Int) =
ShopStylePageFragment.newInstance(styleIds[position])
}
// ========================== network ==========================
private suspend fun getwalletBalance(): ApiResponse<Wallet>? =
runCatching { RetrofitClient.apiService.walletBalance() }.getOrNull()
private suspend fun getThemeList(): ApiResponse<List<Theme>>? =
runCatching { RetrofitClient.apiService.themeList() }.getOrNull()
private fun bindViews(view: View) {
viewPager = view.findViewById(R.id.viewPager)
tagScroll = view.findViewById(R.id.tagScroll)
tagContainer = view.findViewById(R.id.tagContainer)
balance = view.findViewById(R.id.balance)
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
swipeRefreshLayout.isEnabled = true
swipeRefreshLayout.setColorSchemeColors(
Color.parseColor("#02BEAC"),
Color.parseColor("#1B1F1A"),
Color.parseColor("#9F9F9F")
)
}
private fun bindClicks(view: View) {
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
findNavController().navigate(R.id.action_global_goldCoinRechargeFragment)
// 使用事件总线打开金币充值页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
}
view.findViewById<View>(R.id.skinButton).setOnClickListener {
findNavController().navigate(R.id.action_shopfragment_to_myskin)
@@ -56,366 +405,7 @@ class ShopFragment : Fragment(R.layout.fragment_shop) {
view.findViewById<View>(R.id.searchButton).setOnClickListener {
findNavController().navigate(R.id.action_shopfragment_to_searchfragment)
}
tagScroll = view.findViewById(R.id.tagScroll)
tagContainer = view.findViewById(R.id.tagContainer)
viewPager = view.findViewById(R.id.viewPager)
balance = view.findViewById(R.id.balance)
swipeRefreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
// 设置下拉刷新监听器
swipeRefreshLayout.setOnRefreshListener {
refreshData()
}
// 设置刷新指示器颜色
swipeRefreshLayout.setColorSchemeColors(
Color.parseColor("#02BEAC"),
Color.parseColor("#1B1F1A"),
Color.parseColor("#9F9F9F")
)
// 禁用默认的刷新行为,使用自定义逻辑
swipeRefreshLayout.isEnabled = false
// 设置 ViewPager 的子页面滚动监听
setupViewPagerScrollListener()
loadInitialData()
// 修复 ViewPager2 和 SwipeRefreshLayout 的手势冲突
fixViewPager2SwipeConflict()
}
private fun loadInitialData() {
viewLifecycleOwner.lifecycleScope.launch {
val walletResp = getwalletBalance()
val balanceText = (walletResp?.data?.balanceDisplay ?: 0).toString()
balance.text = balanceText
adjustBalanceTextSize(balanceText)
val themeListResp = getThemeList()
tabTitles = themeListResp?.data ?: emptyList()
Log.d("1314520-Shop", "风格列表: $tabTitles")
styleIds = tabTitles.map { it.id }
viewPager.adapter = ShopPagerAdapter(this@ShopFragment, styleIds)
setupTags()
setupViewPager()
// ✅ 默认加载第一个(交给 VM
viewPager.post {
styleIds.firstOrNull()?.let { vm.loadStyleIfNeeded(it) }
}
}
}
/**
* 根据字符数量调整余额文本的字体大小
* 字符数量越多,字体越小
*/
private fun adjustBalanceTextSize(text: String) {
val maxFontSize = 40f // 最大字体大小sp
val minFontSize = 16f // 最小字体大小sp
// 根据字符数量计算字体大小
val fontSize = when (text.length) {
0, 1, 2, 3 -> maxFontSize // 0-3个字符使用最大字体
4 -> 36f
5 -> 32f
6 -> 28f
7 -> 24f
8 -> 22f
9 -> 20f
else -> minFontSize // 10个字符及以上使用最小字体
}
balance.textSize = fontSize
}
private fun refreshData() {
viewLifecycleOwner.lifecycleScope.launch {
try {
// 重新获取钱包余额
val walletResp = getwalletBalance()
val balanceText = (walletResp?.data?.balanceDisplay ?: 0).toString()
balance.text = balanceText
adjustBalanceTextSize(balanceText)
// 重新获取主题列表
val themeListResp = getThemeList()
val newTabTitles = themeListResp?.data ?: emptyList()
// 检查主题列表是否有变化
if (newTabTitles != tabTitles) {
tabTitles = newTabTitles
styleIds = tabTitles.map { it.id }
// 重新设置适配器
viewPager.adapter = ShopPagerAdapter(this@ShopFragment, styleIds)
// 重新设置标签
setupTags()
// 通知 ViewModel 清除缓存
vm.clearCache()
// 强制重新加载所有页面的数据
styleIds.forEach { styleId ->
// 强制重新加载,即使有缓存也要重新获取
vm.forceLoadStyle(styleId)
}
} else {
// 主题列表没有变化,强制重新加载当前页面的数据
val currentPosition = viewPager.currentItem
styleIds.getOrNull(currentPosition)?.let { vm.forceLoadStyle(it) }
}
Log.d("1314520-Shop", "下拉刷新完成")
} catch (e: Exception) {
Log.e("1314520-Shop", "下拉刷新失败", e)
} finally {
// 停止刷新动画
swipeRefreshLayout.isRefreshing = false
}
}
}
/** 子页读取缓存(从 VM 读) */
fun getCachedList(styleId: Int): List<themeStyle> = vm.getCached(styleId)
/** 动态创建标签 */
private fun setupTags() {
tagContainer.removeAllViews()
val context = requireContext()
val density = context.resources.displayMetrics.density
val paddingHorizontal = (16 * density).toInt()
val paddingVertical = (6 * density).toInt()
val marginEnd = (8 * density).toInt()
tabTitles.forEachIndexed { index, title ->
val tv = TextView(context).apply {
text = title.styleName
textSize = 12f
setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT
).apply { setMargins(0, 0, marginEnd, 0) }
gravity = android.view.Gravity.CENTER
background = createCapsuleBackground()
isSelected = index == 0
updateTagStyleNoAnim(this, isSelected)
setOnClickListener {
if (viewPager.currentItem != index) viewPager.currentItem = index
}
}
tagContainer.addView(tv)
}
}
private fun createCapsuleBackground(): GradientDrawable {
val density = resources.displayMetrics.density
return GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 50f * density
setColor(Color.parseColor("#F1F1F1"))
setStroke((2 * density).toInt(), Color.parseColor("#F1F1F1"))
}
}
private fun setupViewPager() {
// ✅ 只设置一次
viewPager.offscreenPageLimit = 1
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
updateTagState(position)
// ✅ 切换到某页就按需加载(交给 VM
styleIds.getOrNull(position)?.let { vm.loadStyleIfNeeded(it) }
}
})
}
private fun updateTagState(position: Int) {
for (i in 0 until tagContainer.childCount) {
val child = tagContainer.getChildAt(i) as TextView
val newSelected = i == position
if (child.isSelected == newSelected) continue
child.isSelected = newSelected
updateTagStyleWithAnim(child, newSelected)
if (newSelected) {
child.post {
val scrollX = child.left - (tagScroll.width - child.width) / 2
tagScroll.smoothScrollTo(scrollX, 0)
}
}
}
}
private fun setupViewPagerScrollListener() {
// 监听 AppBarLayout 的展开状态来判断是否在顶部
view?.findViewById<com.google.android.material.appbar.AppBarLayout>(R.id.appBar)?.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
val isAtTop = verticalOffset == 0
swipeRefreshLayout.isEnabled = isAtTop
}
}
@SuppressLint("ClickableViewAccessibility")
private fun fixViewPager2SwipeConflict() {
val rv = viewPager.getChildAt(0) as? RecyclerView ?: return
rv.setOnTouchListener { v, ev ->
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> v.parent?.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
v.parent?.requestDisallowInterceptTouchEvent(false)
}
false
}
}
private fun updateTagStyleNoAnim(textView: TextView, selected: Boolean) {
val density = resources.displayMetrics.density
val bg = (textView.background as? GradientDrawable)
?: createCapsuleBackground().also { textView.background = it }
val strokeWidth = (2 * density).toInt()
if (selected) {
bg.setColor(Color.parseColor("#FFFFFF"))
bg.setStroke(strokeWidth, Color.parseColor("#02BEAC"))
textView.setTextColor(Color.parseColor("#1B1F1A"))
textView.setTypeface(null, Typeface.BOLD)
} else {
bg.setColor(Color.parseColor("#F1F1F1"))
bg.setStroke(strokeWidth, Color.parseColor("#F1F1F1"))
textView.setTextColor(Color.parseColor("#9F9F9F"))
textView.setTypeface(null, Typeface.NORMAL)
}
}
private fun updateTagStyleWithAnim(textView: TextView, selected: Boolean) {
val density = resources.displayMetrics.density
val bg = (textView.background as? GradientDrawable)
?: createCapsuleBackground().also { textView.background = it }
val strokeWidth = (2 * density).toInt()
val selectedTextColor = Color.parseColor("#1B1F1A")
val unselectedTextColor = Color.parseColor("#9F9F9F")
val selectedStrokeColor = Color.parseColor("#02BEAC")
val unselectedStrokeColor = Color.parseColor("#F1F1F1")
val selectedBgColor = Color.parseColor("#FFFFFF")
val unselectedBgColor = Color.parseColor("#F1F1F1")
val colorsArray = if (selected) {
arrayOf(
unselectedTextColor, selectedTextColor,
unselectedStrokeColor, selectedStrokeColor,
unselectedBgColor, selectedBgColor
)
} else {
arrayOf(
selectedTextColor, unselectedTextColor,
selectedStrokeColor, unselectedStrokeColor,
selectedBgColor, unselectedBgColor
)
}
val startTextColor = colorsArray[0]
val endTextColor = colorsArray[1]
val startStrokeColor = colorsArray[2]
val endStrokeColor = colorsArray[3]
val startBgColor = colorsArray[4]
val endBgColor = colorsArray[5]
val evaluator = ArgbEvaluator()
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 200L
addUpdateListener { va ->
val f = va.animatedFraction
textView.setTextColor(evaluator.evaluate(f, startTextColor, endTextColor) as Int)
bg.setStroke(strokeWidth, evaluator.evaluate(f, startStrokeColor, endStrokeColor) as Int)
bg.setColor(evaluator.evaluate(f, startBgColor, endBgColor) as Int)
}
start()
}
textView.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
}
private inner class ShopPagerAdapter(
fragment: Fragment,
private val styleIds: List<Int>
) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = styleIds.size
override fun createFragment(position: Int): Fragment {
val styleId = styleIds[position]
return ShopStylePageFragment.newInstance(styleId)
}
}
// ============================ 网络请求 ============================
private suspend fun getwalletBalance(): ApiResponse<Wallet>? {
return try {
RetrofitClient.apiService.walletBalance()
} catch (e: Exception) {
Log.e("1314520-ShopFragment", "获取钱包余额失败", e)
null
}
}
private suspend fun getThemeList(): ApiResponse<List<Theme>>? {
return try {
RetrofitClient.apiService.themeList()
} catch (e: Exception) {
Log.e("1314520-ShopFragment", "获取主题风格失败", e)
null
}
}
/**
* 根据余额值计算字体大小
* 基础字体大小16sp数字越大字体越小
*/
// private fun calculateFontSize(balance: Double): Float {
// val baseSize = 40f // 基础字体大小
// val minSize = 5f // 最小字体大小
// val maxSize = 40f // 最大字体大小
// // 使用对数函数实现平滑的字体大小变化
// // 当余额为0时使用最大字体余额越大字体越小
// val scaleFactor = when {
// balance <= 0 -> 1.0
// balance < 10 -> 0.93
// balance < 100 -> 0.86
// balance < 1000 -> 0.79
// balance < 10000 -> 0.72
// balance < 100000 -> 0.65
// balance < 1000000 -> 0.58
// balance < 10000000 -> 0.51
// balance < 100000000 -> 0.44
// balance < 1000000000 -> 0.37
// balance < 10000000000 -> 0.3
// balance < 100000000000 -> 0.23
// balance < 1000000000000 -> 0.16
// else -> 0.09
// }
// val calculatedSize = baseSize * scaleFactor.toFloat()
// // 确保字体大小在最小和最大限制范围内
// return calculatedSize.coerceIn(minSize, maxSize)
// }
}

View File

@@ -13,6 +13,8 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
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.themeStyle
import com.google.android.material.card.MaterialCardView
@@ -54,7 +56,8 @@ class ThemeCardAdapter : ListAdapter<themeStyle, ThemeCardAdapter.ThemeCardViewH
val bundle = Bundle().apply {
putInt("themeId", theme.id)
}
itemView.findNavController().navigate(R.id.action_global_keyboardDetailFragment, bundle)
// 使用事件总线打开键盘详情页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.keyboardDetailFragment, bundle))
}
}
}

View File

@@ -10,6 +10,8 @@ import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
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.themeStyle
class MySkinAdapter(
@@ -101,7 +103,8 @@ class MySkinAdapter(
val bundle = Bundle().apply {
putInt("themeId", item.id)
}
holder.itemView.findNavController().navigate(R.id.action_global_keyboardDetailFragment, bundle)
// 使用事件总线打开键盘详情页面
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.keyboardDetailFragment, bundle))
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 选中状态 -->
<item
android:state_checked="true"
android:drawable="@drawable/selected_home" />
<!-- 未选中状态 -->
<item
android:drawable="@drawable/home" />
</selector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 选中状态 -->
<item
android:state_checked="true"
android:drawable="@drawable/selected_mine" />
<!-- 未选中状态 -->
<item
android:drawable="@drawable/mine" />
</selector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 选中状态 -->
<item
android:state_checked="true"
android:drawable="@drawable/selected_shop" />
<!-- 未选中状态 -->
<item
android:drawable="@drawable/shop" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -7,16 +7,27 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 页面内容区域:用于切换 Fragment -->
<FrameLayout
android:id="@+id/root_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 3个Tab NavHost 都 add 到这里 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:id="@+id/tab_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- 全局页面覆盖层login/recharge等 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/global_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true" />
android:visibility="gone" />
</FrameLayout>
<!-- 底部导航栏 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"

View File

@@ -0,0 +1,5 @@
<?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:background="@android:color/transparent"/>

View File

@@ -53,7 +53,7 @@
android:textColor="#1B1F1A"/>
<EditText
android:id="@+id/et_username"
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="20dp"

View File

@@ -84,7 +84,7 @@
android:layout_marginTop="14dp"
android:layout_height="52dp">
<EditText
android:id="@+id/et_password"
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="14sp"
@@ -95,7 +95,7 @@
android:background="@null"
android:inputType="textPassword" />
<ImageView
android:id="@+id/iv_toggle"
android:id="@+id/iv_confirm_toggle"
android:layout_width="52dp"
android:layout_height="52dp"
android:padding="16dp"
@@ -105,7 +105,7 @@
</RelativeLayout>
<TextView
android:id="@+id/btn_login"
android:id="@+id/nextstep"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="20dp"

View File

@@ -1,4 +1,4 @@
<!-- 忘记密码验证码输入页面 -->
<!-- 验证码输入页面 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -59,6 +59,17 @@
android:text="Enter email verification code"
android:textColor="#1B1F1A"/>
<TextView
android:id="@+id/tv_code_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textStyle="bold"
android:text="Please enter the verification code sent to your email"
android:textColor="#02BEAC"/>
<!-- 验证码输入框 -->
<LinearLayout
android:id="@+id/ll_code_container"

View File

@@ -79,8 +79,8 @@
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="54dp"
android:layout_height="44dp"
android:src="@drawable/ai_dialogue"
android:scaleType="fitCenter" />
@@ -103,8 +103,8 @@
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="54dp"
android:layout_height="44dp"
android:src="@drawable/personalized_keyboard"
android:scaleType="fitCenter" />
@@ -127,8 +127,8 @@
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="54dp"
android:layout_height="44dp"
android:src="@drawable/chat_persona"
android:scaleType="fitCenter" />
@@ -151,8 +151,8 @@
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="54dp"
android:layout_height="44dp"
android:src="@drawable/emotional_counseling"
android:scaleType="fitCenter" />

View File

@@ -99,7 +99,7 @@
<!-- 输入框 -->
<EditText
android:id="@+id/et_username"
android:id="@+id/et_email"
android:layout_width="315dp"
android:layout_height="52dp"
android:layout_marginTop="20dp"
@@ -142,7 +142,7 @@
android:layout_marginTop="14dp"
android:layout_height="52dp">
<EditText
android:id="@+id/et_password"
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="14sp"
@@ -153,7 +153,7 @@
android:textColorHint="#CBCBCB"
android:inputType="textPassword" />
<ImageView
android:id="@+id/iv_toggle"
android:id="@+id/iv_confirm_toggle"
android:layout_width="52dp"
android:layout_height="52dp"
android:padding="16dp"
@@ -161,9 +161,9 @@
android:layout_centerVertical="true"
android:src="@drawable/hide" />
</RelativeLayout>
<!-- 登录按钮 -->
<!-- 下一步按钮 -->
<TextView
android:id="@+id/btn_login"
android:id="@+id/btn_next_step"
android:layout_width="315dp"
android:layout_height="52dp"
android:layout_marginTop="20dp"
@@ -172,7 +172,7 @@
android:textStyle="bold"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:text="Login" />
android:text="Next step" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/agreement_container"

View File

@@ -0,0 +1,120 @@
<!-- 验证码输入页面 -->
<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="#FFFFFF"
tools:context=".ui.login.RegisterVerifyFragment">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<!-- 标题和返回 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
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>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:orientation="vertical">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:text="Register"
android:textColor="#1B1F1A"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textStyle="bold"
android:text="Enter email verification code"
android:textColor="#1B1F1A"/>
<TextView
android:id="@+id/tv_code_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textStyle="bold"
android:text="Please enter the verification code sent to your email"
android:textColor="#02BEAC"/>
<!-- 验证码输入框 -->
<LinearLayout
android:id="@+id/ll_code_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:gravity="center"
android:orientation="horizontal">
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_1"
style="@style/VerifyCodeBox" />
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_2"
style="@style/VerifyCodeBox" />
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_3"
style="@style/VerifyCodeBox" />
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_4"
style="@style/VerifyCodeBox" />
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_5"
style="@style/VerifyCodeBox" />
<com.example.myapplication.ui.common.CodeEditText
android:id="@+id/et_code_6"
style="@style/VerifyCodeBox" />
</LinearLayout>
<TextView
android:id="@+id/nextstep"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="20dp"
android:background="@drawable/login_btn_bg"
android:gravity="center"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:text="Next step" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -2,24 +2,15 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/home_selector"
android:title="Home" />
android:id="@+id/home_graph"
android:icon="@drawable/ic_home_selector"/>
<item
android:id="@+id/shopFragment"
android:icon="@drawable/shop_selector"
android:title="Shop" />
<!-- <item
android:id="@+id/circleFragment"
android:icon="@drawable/circle_selector"
android:title="Circle" /> -->
android:id="@+id/shop_graph"
android:icon="@drawable/ic_shop_selector"/>
<item
android:id="@+id/mineFragment"
android:icon="@drawable/mine_selector"
android:title="Mine" />
android:id="@+id/mine_graph"
android:icon="@drawable/ic_mine_selector" />
</menu>

View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/global_graph"
app:startDestination="@id/globalEmptyFragment">
<!-- 一个空白起点,用来表示"没打开全局页" -->
<fragment
android:id="@+id/globalEmptyFragment"
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"
android:name="com.example.myapplication.ui.recharge.RechargeFragment"
android:label="Recharge"
tools:layout="@layout/activity_recharge" />
<!-- 金币充值 -->
<fragment
android:id="@+id/goldCoinRechargeFragment"
android:name="com.example.myapplication.ui.recharge.GoldCoinRechargeFragment"
android:label="Gold Coin Recharge"
tools:layout="@layout/gold_coin_recharge" />
<!-- 键盘详情 -->
<fragment
android:id="@+id/keyboardDetailFragment"
android:name="com.example.myapplication.ui.keyboard.KeyboardDetailFragment"
android:label="Keyboard Detail"
tools:layout="@layout/keyboard_detail">
<argument
android:name="themeId"
android:defaultValue="0"
app:argType="integer" />
</fragment>
<!-- 登录页面 -->
<fragment
android:id="@+id/loginFragment"
android:name="com.example.myapplication.ui.login.LoginFragment"
android:label="Login"
tools:layout="@layout/fragment_login" />
<!-- 注册页面 -->
<fragment
android:id="@+id/registerFragment"
android:name="com.example.myapplication.ui.login.RegisterFragment"
android:label="Register"
tools:layout="@layout/fragment_register" />
<!-- 忘记密码邮箱输入页面 -->
<fragment
android:id="@+id/forgetPasswordEmailFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordEmailFragment"
android:label="Forget Password Email"
tools:layout="@layout/fragment_forget_password_email" />
<!-- 忘记密码验证码输入页面 -->
<fragment
android:id="@+id/forgetPasswordVerifyFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordVerifyFragment"
android:label="Forget Password Verify"
tools:layout="@layout/fragment_forget_password_verify" />
<!-- 注册账号验证码输入页面 -->
<fragment
android:id="@+id/registerVerifyFragment"
android:name="com.example.myapplication.ui.login.RegisterVerifyFragment"
android:label="Register Verify"
tools:layout="@layout/fragment_register_verify" />
<!-- 忘记密码重置密码页面 -->
<fragment
android:id="@+id/forgetPasswordResetFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordResetFragment"
android:label="Forget Password Reset"
tools:layout="@layout/fragment_forget_password_reset" />
<!-- 充值跳转 -->
<action
android:id="@+id/action_global_rechargeFragment"
app:destination="@id/rechargeFragment"
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_goldCoinRechargeFragment"
app:destination="@id/goldCoinRechargeFragment"
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_keyboardDetailFragment"
app:destination="@id/keyboardDetailFragment"
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_loginFragment"
app:destination="@id/loginFragment"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true"
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_loginFragment_to_forgetPasswordEmailFragment"
app:destination="@id/forgetPasswordEmailFragment"
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_forgetPasswordEmailFragment_to_forgetPasswordVerifyFragment"
app:destination="@id/forgetPasswordVerifyFragment"
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_forgetPasswordVerifyFragment_to_forgetPasswordResetFragment"
app:destination="@id/forgetPasswordResetFragment"
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_registerFragment_to_registerVerifyFragment"
app:destination="@id/registerVerifyFragment"
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_loginFragment_to_registerFragment"
app:destination="@id/registerFragment"
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_loginFragment_from_forget_password"
app:destination="@id/loginFragment"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast" />
</navigation>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/home_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.myapplication.ui.home.HomeFragment"
android:label="Home"
tools:layout="@layout/fragment_home" />
</navigation>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/mine_graph"
app:startDestination="@id/mineFragment">
<fragment
android:id="@+id/mineFragment"
android:name="com.example.myapplication.ui.mine.MineFragment"
android:label="Mine"
tools:layout="@layout/fragment_mine">
<action
android:id="@+id/action_mineFragment_to_personalSettings"
app:destination="@id/PersonalSettings"
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_mineFragment_to_mykeyboard"
app:destination="@id/MyKeyboard"
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_mineFragment_to_feedbackFragment"
app:destination="@id/feedbackFragment"
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_mineFragment_to_notificationFragment"
app:destination="@id/notificationFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast" />
</fragment>
<fragment
android:id="@+id/PersonalSettings"
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
android:label="Setting"
tools:layout="@layout/personal_settings" />
<fragment
android:id="@+id/MyKeyboard"
android:name="com.example.myapplication.ui.keyboard.MyKeyboard"
android:label="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" />
</navigation>

View File

@@ -3,285 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
app:startDestination="@id/home_graph">
<!-- 首页 -->
<fragment
android:id="@+id/homeFragment"
android:name="com.example.myapplication.ui.home.HomeFragment"
android:label="Home"
tools:layout="@layout/fragment_home" />
<action
android:id="@+id/action_global_homeFragment"
app:destination="@id/homeFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 商城 -->
<fragment
android:id="@+id/shopFragment"
android:name="com.example.myapplication.ui.shop.ShopFragment"
android:label="Shop"
tools:layout="@layout/fragment_shop">
<action
android:id="@+id/action_shopfragment_to_myskin"
app:destination="@id/MySkin"
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_shopfragment_to_searchfragment"
app:destination="@id/searchFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
</fragment>
<!-- 圈子 -->
<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/mineFragment"
android:name="com.example.myapplication.ui.mine.MineFragment"
android:label="Mine"
tools:layout="@layout/fragment_mine">
<action
android:id="@+id/action_mineFragment_to_personalSettings"
app:destination="@id/PersonalSettings"
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_mineFragment_to_mykeyboard"
app:destination="@id/MyKeyboard"
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_mineFragment_to_feedbackFragment"
app:destination="@id/feedbackFragment"
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_mineFragment_to_notificationFragment"
app:destination="@id/notificationFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
</fragment>
<!-- 充值 -->
<fragment
android:id="@+id/rechargeFragment"
android:name="com.example.myapplication.ui.recharge.RechargeFragment"
android:label="Recharge"
tools:layout="@layout/activity_recharge" />
<!-- 金币充值 -->
<fragment
android:id="@+id/goldCoinRechargeFragment"
android:name="com.example.myapplication.ui.recharge.GoldCoinRechargeFragment"
android:label="Gold Coin Recharge"
tools:layout="@layout/gold_coin_recharge" />
<!-- 充值全局跳转 -->
<action
android:id="@+id/action_global_rechargeFragment"
app:destination="@id/rechargeFragment"
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_goldCoinRechargeFragment"
app:destination="@id/goldCoinRechargeFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 个人设置 -->
<fragment
android:id="@+id/PersonalSettings"
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
android:label="Setting"
tools:layout="@layout/personal_settings"/>
<!-- 我的键盘 -->
<fragment
android:id="@+id/MyKeyboard"
android:name="com.example.myapplication.ui.keyboard.MyKeyboard"
android:label="Keyboard"
tools:layout="@layout/my_keyboard" />
<!-- 键盘详情 -->
<fragment
android:id="@+id/keyboardDetailFragment"
android:name="com.example.myapplication.ui.keyboard.KeyboardDetailFragment"
android:label="Keyboard Detail"
tools:layout="@layout/keyboard_detail">
<argument
android:name="themeId"
android:defaultValue="0"
app:argType="integer" />
</fragment>
<!-- 键盘详情全局跳转 -->
<action
android:id="@+id/action_global_keyboardDetailFragment"
app:destination="@id/keyboardDetailFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 我的皮肤 -->
<fragment
android:id="@+id/MySkin"
android:name="com.example.myapplication.ui.shop.myskin.MySkin"
android:label="My Skin"
tools:layout="@layout/my_skin" />
<!-- 搜索 -->
<fragment
android:id="@+id/searchFragment"
android:name="com.example.myapplication.ui.shop.search.SearchFragment"
android:label="Search"
tools:layout="@layout/fragment_search">
<action
android:id="@+id/action_searchFragment_to_searchResultFragment"
app:destination="@id/searchResultFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
</fragment>
<!-- 搜索结果 -->
<fragment
android:id="@+id/searchResultFragment"
android:name="com.example.myapplication.ui.shop.search.SearchResultFragment"
android:label="Search Result"
tools:layout="@layout/fragment_search_result" />
<!-- 反馈 -->
<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/loginFragment"
android:name="com.example.myapplication.ui.login.LoginFragment"
android:label="Login"
tools:layout="@layout/fragment_login" />
<!-- 全局登录跳转 -->
<action
android:id="@+id/action_global_loginFragment"
app:destination="@id/loginFragment"
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_mineFragment_to_loginFragment"
app:destination="@id/loginFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 注册页面 -->
<fragment
android:id="@+id/registerFragment"
android:name="com.example.myapplication.ui.login.RegisterFragment"
android:label="Register"
tools:layout="@layout/fragment_register" />
<action
android:id="@+id/action_mineFragment_to_registerFragment"
app:destination="@id/registerFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 忘记密码邮箱输入页面 -->
<fragment
android:id="@+id/forgetPasswordEmailFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordEmailFragment"
android:label="Forget Password Email"
tools:layout="@layout/fragment_forget_password_email" />
<action
android:id="@+id/action_loginFragment_to_forgetPasswordEmailFragment"
app:destination="@id/forgetPasswordEmailFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 忘记密码验证码输入页面 -->
<fragment
android:id="@+id/forgetPasswordVerifyFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordVerifyFragment"
android:label="Forget Password Verify"
tools:layout="@layout/fragment_forget_password_verify" />
<action
android:id="@+id/action_forgetPasswordEmailFragment_to_forgetPasswordVerifyFragment"
app:destination="@id/forgetPasswordVerifyFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 忘记密码重置密码页面 -->
<fragment
android:id="@+id/forgetPasswordResetFragment"
android:name="com.example.myapplication.ui.login.ForgetPasswordResetFragment"
android:label="Forget Password Reset"
tools:layout="@layout/fragment_forget_password_reset" />
<action
android:id="@+id/action_forgetPasswordVerifyFragment_to_forgetPasswordResetFragment"
app:destination="@id/forgetPasswordResetFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast"/>
<!-- 通知 -->
<fragment
android:id="@+id/notificationFragment"
android:name="com.example.myapplication.ui.mine.myotherpages.NotificationFragment"
android:label="Notification"
tools:layout="@layout/notification_fragment" />
<!-- 三个 Tab独立 back stack -->
<include app:graph="@navigation/home_graph" />
<include app:graph="@navigation/shop_graph" />
<include app:graph="@navigation/mine_graph" />
</navigation>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/shop_graph"
app:startDestination="@id/shopFragment">
<fragment
android:id="@+id/shopFragment"
android:name="com.example.myapplication.ui.shop.ShopFragment"
android:label="Shop"
tools:layout="@layout/fragment_shop">
<action
android:id="@+id/action_shopfragment_to_myskin"
app:destination="@id/MySkin"
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_shopfragment_to_searchfragment"
app:destination="@id/searchFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast" />
</fragment>
<fragment
android:id="@+id/MySkin"
android:name="com.example.myapplication.ui.shop.myskin.MySkin"
android:label="My Skin"
tools:layout="@layout/my_skin" />
<fragment
android:id="@+id/searchFragment"
android:name="com.example.myapplication.ui.shop.search.SearchFragment"
android:label="Search"
tools:layout="@layout/fragment_search">
<action
android:id="@+id/action_searchFragment_to_searchResultFragment"
app:destination="@id/searchResultFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in_fast"
app:popExitAnim="@anim/fade_out_fast" />
</fragment>
<fragment
android:id="@+id/searchResultFragment"
android:name="com.example.myapplication.ui.shop.search.SearchResultFragment"
android:label="Search Result"
tools:layout="@layout/fragment_search_result" />
</navigation>

View File

@@ -19,6 +19,7 @@
<item name="android:gravity">center</item>
<item name="android:textSize">20sp</item>
<item name="android:maxLength">1</item>
<item name="android:inputType">number</item>
<item name="android:background">@drawable/code_box_bg</item>
<item name="android:textColor">#000000</item>
</style>