优化
@@ -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
|
||||
// 5) intent跳转(充值等)统一走全局 overlay
|
||||
handleNavigationFromIntent()
|
||||
|
||||
// 2. 找到 BottomNavigationView
|
||||
bottomNav = findViewById(R.id.bottom_nav)
|
||||
// 6) 返回键规则:优先关闭global,其次pop当前tab
|
||||
setupBackPress()
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
// 6. 检查是否有导航参数,处理从键盘跳转过来的请求
|
||||
handleNavigationFromIntent()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
// 删除时少做 IPC:selectedText 也可能慢,所以只在需要时取
|
||||
// 记录删除前的极小上下文,用来判断“是否真的删掉了东西”
|
||||
// 不要取多,避免开销
|
||||
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
|
||||
}
|
||||
|
||||
scheduleRefreshSuggestions()
|
||||
// 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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 层去跳转
|
||||
AuthEventBus.emit(AuthEvent.TokenExpired(errorResponse.message))
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,23 +80,56 @@ 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) {
|
||||
startActivity(Intent(requireActivity(), ImeGuideActivity::class.java))
|
||||
}
|
||||
if (!isAdded) return@setOnClickListener
|
||||
startActivity(Intent(requireActivity(), ImeGuideActivity::class.java))
|
||||
}
|
||||
|
||||
scrim = view.findViewById(R.id.view_scrim)
|
||||
@@ -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) 有缓存:不 notify(onBind 会直接显示缓存)
|
||||
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,22 +576,27 @@ 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 {
|
||||
if (click.persona.added == true) {
|
||||
click.persona.id?.let { id ->
|
||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
if (click.persona.added == true) {
|
||||
click.persona.id?.let { id ->
|
||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
||||
}
|
||||
} else {
|
||||
val req = AddPersonaClick(
|
||||
characterId = click.persona.id?.toInt() ?: 0,
|
||||
emoji = click.persona.emoji ?: ""
|
||||
)
|
||||
RetrofitClient.apiService.addUserCharacter(req)
|
||||
}
|
||||
} else {
|
||||
val req = AddPersonaClick(
|
||||
characterId = click.persona.id?.toInt() ?: 0,
|
||||
emoji = click.persona.emoji ?: ""
|
||||
)
|
||||
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 ->
|
||||
try {
|
||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
||||
} catch (e: Exception) {
|
||||
// 处理错误
|
||||
}
|
||||
}
|
||||
}else{
|
||||
val addPersonaRequest = AddPersonaClick(
|
||||
characterId = p.id?.toInt() ?: 0,
|
||||
emoji = p.emoji ?: ""
|
||||
)
|
||||
try {
|
||||
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
||||
} catch (e: Exception) {
|
||||
// 处理错误
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
if (p.added == true) {
|
||||
p.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
||||
} else {
|
||||
val req = AddPersonaClick(
|
||||
characterId = p.id?.toInt() ?: 0,
|
||||
emoji = p.emoji ?: ""
|
||||
)
|
||||
RetrofitClient.apiService.addUserCharacter(req)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ->
|
||||
try {
|
||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
||||
} catch (e: Exception) {
|
||||
// 处理错误
|
||||
}
|
||||
}
|
||||
}else{
|
||||
val addPersonaRequest = AddPersonaClick(
|
||||
characterId = item.id?.toInt() ?: 0,
|
||||
emoji = item.emoji ?: ""
|
||||
)
|
||||
try {
|
||||
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
||||
} catch (e: Exception) {
|
||||
// 处理错误
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
if (item.added == true) {
|
||||
item.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
||||
} else {
|
||||
val req = AddPersonaClick(
|
||||
characterId = item.id?.toInt() ?: 0,
|
||||
emoji = item.emoji ?: ""
|
||||
)
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
findNavController().navigate(R.id.action_forgetPasswordEmailFragment_to_forgetPasswordVerifyFragment)
|
||||
}
|
||||
// 对输入框去除首尾空格
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
findNavController().navigate(R.id.action_forgetPasswordVerifyFragment_to_forgetPasswordResetFragment)
|
||||
}
|
||||
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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
// 返回按钮
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
findNavController().previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("from_login", true)
|
||||
|
||||
// 返回 - 在global_graph中,直接popBackStack回到globalEmptyFragment
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
Log.e("1314520-MineFragment", "获取失败", e)
|
||||
// // ✅ 手动刷新:不改布局也能用
|
||||
// // - 点昵称刷新
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录(先确认)
|
||||
logout.setOnClickListener {
|
||||
LogoutDialogFragment {
|
||||
// ✅ 用户确认后才执行
|
||||
EncryptedSharedPreferencesUtil.remove(requireContext(), "Personal_information")
|
||||
EncryptedSharedPreferencesUtil.remove(requireContext(), "user")
|
||||
|
||||
// ⚠️ 建议用 popUpTo 清栈,避免按返回回到已登录页面
|
||||
findNavController().navigate(R.id.action_mineFragment_to_loginFragment)
|
||||
}.show(parentFragmentManager, "logout_dialog")
|
||||
}
|
||||
|
||||
|
||||
// 会员充值按钮点击
|
||||
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)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
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) {
|
||||
if (e is kotlinx.coroutines.CancellationException) return@launch
|
||||
Log.e(TAG, "getUser failed", e)
|
||||
if (showToast && isAdded) Toast.makeText(requireContext(), "刷新失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// 清空 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 1.8 KiB |
13
app/src/main/res/drawable/ic_home_selector.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/ic_mine_selector.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/ic_shop_selector.xml
Normal 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>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 2.1 KiB |
@@ -7,16 +7,27 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<!-- 页面内容区域:用于切换 Fragment -->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
<FrameLayout
|
||||
android:id="@+id/root_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:navGraph="@navigation/nav_graph"
|
||||
app:defaultNavHost="true" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- 3个Tab NavHost 都 add 到这里 -->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
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"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_nav"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
5
app/src/main/res/layout/fragment_empty.xml
Normal 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"/>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
120
app/src/main/res/layout/fragment_register_verify.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
181
app/src/main/res/navigation/global_graph.xml
Normal 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>
|
||||
14
app/src/main/res/navigation/home_graph.xml
Normal 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>
|
||||
|
||||
71
app/src/main/res/navigation/mine_graph.xml
Normal 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>
|
||||
@@ -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>
|
||||
58
app/src/main/res/navigation/shop_graph.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||