优化
@@ -2,92 +2,354 @@ package com.example.myapplication
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import com.example.myapplication.network.AuthEventBus
|
|
||||||
import com.example.myapplication.network.AuthEvent
|
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.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var bottomNav: BottomNavigationView
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
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 {
|
lifecycleScope.launch {
|
||||||
AuthEventBus.events.collectLatest { event ->
|
AuthEventBus.events.collectLatest { event ->
|
||||||
if (event is AuthEvent.TokenExpired) {
|
when (event) {
|
||||||
val navController = (supportFragmentManager
|
is AuthEvent.TokenExpired -> {
|
||||||
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment)
|
pendingTabAfterLogin = null
|
||||||
.navController
|
// 已经在login就别重复
|
||||||
|
if (!isGlobalVisible() || globalNavController.currentDestination?.id != R.id.loginFragment) {
|
||||||
// 避免重复跳转(比如已经在登录页)
|
openGlobal(R.id.loginFragment)
|
||||||
if (navController.currentDestination?.id != R.id.loginFragment) {
|
}
|
||||||
navController.navigate(R.id.action_global_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
|
// 5) intent跳转(充值等)统一走全局 overlay
|
||||||
val navHostFragment =
|
handleNavigationFromIntent()
|
||||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
|
||||||
navController = navHostFragment.navController
|
|
||||||
|
|
||||||
// 2. 找到 BottomNavigationView
|
// 6) 返回键规则:优先关闭global,其次pop当前tab
|
||||||
bottomNav = findViewById(R.id.bottom_nav)
|
setupBackPress()
|
||||||
|
|
||||||
// 3. 绑定导航控制器(负责切换 Fragment、保持选中状态)
|
// 7) 初始选中正确tab(不会触发二次创建)
|
||||||
bottomNav.setupWithNavController(navController)
|
bottomNav.post {
|
||||||
|
bottomNav.selectedItemId = when (currentTabTag) {
|
||||||
// 4. 取消图标颜色 tint —— 使用原图标颜色
|
TAB_SHOP -> R.id.shop_graph
|
||||||
bottomNav.itemIconTintList = null
|
TAB_MINE -> R.id.mine_graph
|
||||||
|
else -> R.id.home_graph
|
||||||
// 5. 添加导航监听(用于某些 Fragment 隐藏底部导航栏)
|
|
||||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
|
||||||
|
|
||||||
// 只有这些页面显示 BottomNav
|
|
||||||
val pagesWithBottomNav = setOf(
|
|
||||||
R.id.mineFragment,
|
|
||||||
R.id.homeFragment,
|
|
||||||
R.id.shopFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
if (destination.id in pagesWithBottomNav) {
|
|
||||||
bottomNav.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
bottomNav.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 检查是否有导航参数,处理从键盘跳转过来的请求
|
private fun initHosts() {
|
||||||
handleNavigationFromIntent()
|
val fm = supportFragmentManager
|
||||||
|
|
||||||
|
homeHost = fm.findFragmentByTag(TAB_HOME) as? NavHostFragment
|
||||||
|
?: NavHostFragment.create(R.navigation.home_graph)
|
||||||
|
shopHost = fm.findFragmentByTag(TAB_SHOP) as? NavHostFragment
|
||||||
|
?: NavHostFragment.create(R.navigation.shop_graph)
|
||||||
|
mineHost = fm.findFragmentByTag(TAB_MINE) as? NavHostFragment
|
||||||
|
?: NavHostFragment.create(R.navigation.mine_graph)
|
||||||
|
|
||||||
|
globalHost = fm.findFragmentByTag(GLOBAL_HOST) as? NavHostFragment
|
||||||
|
?: NavHostFragment.create(R.navigation.global_graph)
|
||||||
|
|
||||||
|
// 第一次创建时 add;后续进程重建会自动恢复,无需重复 add
|
||||||
|
if (fm.findFragmentByTag(TAB_HOME) == null) {
|
||||||
|
fm.beginTransaction()
|
||||||
|
.setReorderingAllowed(true)
|
||||||
|
.add(R.id.tab_container, homeHost, TAB_HOME)
|
||||||
|
.add(R.id.tab_container, shopHost, TAB_SHOP).hide(shopHost)
|
||||||
|
.add(R.id.tab_container, mineHost, TAB_MINE).hide(mineHost)
|
||||||
|
.commitNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.findFragmentByTag(GLOBAL_HOST) == null) {
|
||||||
|
fm.beginTransaction()
|
||||||
|
.setReorderingAllowed(true)
|
||||||
|
.add(R.id.global_container, globalHost, GLOBAL_HOST)
|
||||||
|
.commitNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保当前tab可见
|
||||||
|
switchTab(currentTabTag, force = true)
|
||||||
|
|
||||||
|
// 绑定全局导航可见性监听
|
||||||
|
bindGlobalVisibility()
|
||||||
|
|
||||||
|
// 绑定底部导航栏可见性监听
|
||||||
|
bindBottomNavVisibilityForTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindGlobalVisibility() {
|
||||||
|
globalNavController.addOnDestinationChangedListener { _, dest, _ ->
|
||||||
|
val isEmpty = dest.id == R.id.globalEmptyFragment
|
||||||
|
|
||||||
|
findViewById<View>(R.id.global_container).visibility =
|
||||||
|
if (isEmpty) View.GONE else View.VISIBLE
|
||||||
|
bottomNav.visibility =
|
||||||
|
if (isEmpty) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// ✅ 只在"刚从某个全局页关闭回 empty"时触发回退逻辑
|
||||||
|
val justClosedOverlay = (dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
|
||||||
|
lastGlobalDestId = dest.id
|
||||||
|
|
||||||
|
if (justClosedOverlay) {
|
||||||
|
val currentTabGraphId = when (currentTabTag) {
|
||||||
|
TAB_SHOP -> R.id.shop_graph
|
||||||
|
TAB_MINE -> R.id.mine_graph
|
||||||
|
else -> R.id.home_graph
|
||||||
|
}
|
||||||
|
// 未登录且当前处在受保护tab:强制回首页
|
||||||
|
if (!isLoggedIn() && currentTabGraphId in protectedTabs) {
|
||||||
|
switchTab(TAB_HOME, force = true)
|
||||||
|
bottomNav.selectedItemId = R.id.home_graph
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 只有"没登录就关闭登录页"才清 pending
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
pendingTabAfterLogin = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchTab(targetTag: String, force: Boolean = false) {
|
||||||
|
if (!force && targetTag == currentTabTag) return
|
||||||
|
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
if (fm.isStateSaved) return // ✅ 防崩:stateSaved 时不做事务
|
||||||
|
|
||||||
|
currentTabTag = targetTag
|
||||||
|
|
||||||
|
fm.beginTransaction()
|
||||||
|
.setReorderingAllowed(true)
|
||||||
|
.hide(homeHost)
|
||||||
|
.hide(shopHost)
|
||||||
|
.hide(mineHost)
|
||||||
|
.also { ft ->
|
||||||
|
when (targetTag) {
|
||||||
|
TAB_SHOP -> ft.show(shopHost)
|
||||||
|
TAB_MINE -> ft.show(mineHost)
|
||||||
|
else -> ft.show(homeHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开全局页(login/recharge等) */
|
||||||
|
private fun openGlobal(destId: Int, bundle: Bundle? = null) {
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
if (fm.isStateSaved) return // ✅ 防崩
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bundle != null) {
|
||||||
|
globalNavController.navigate(destId, bundle)
|
||||||
|
} else {
|
||||||
|
globalNavController.navigate(destId)
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// 可选:防止偶发重复 navigate 崩溃
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭全局页:pop到 empty */
|
||||||
|
private fun bindBottomNavVisibilityForTabs() {
|
||||||
|
fun shouldHideBottomNav(destId: Int): Boolean {
|
||||||
|
return destId in setOf(
|
||||||
|
R.id.searchFragment,
|
||||||
|
R.id.searchResultFragment,
|
||||||
|
R.id.MySkin
|
||||||
|
// 你还有其他需要全屏的页,也加在这里
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val listener = NavController.OnDestinationChangedListener { _, dest, _ ->
|
||||||
|
// 只要 global overlay 打开了,仍然以 overlay 为准(你已有逻辑)
|
||||||
|
if (isGlobalVisible()) return@OnDestinationChangedListener
|
||||||
|
bottomNav.visibility = if (shouldHideBottomNav(dest.id)) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
homeHost.navController.addOnDestinationChangedListener(listener)
|
||||||
|
shopHost.navController.addOnDestinationChangedListener(listener)
|
||||||
|
mineHost.navController.addOnDestinationChangedListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeGlobalIfPossible(): Boolean {
|
||||||
|
if (!isGlobalVisible()) return false
|
||||||
|
|
||||||
|
val popped = globalNavController.popBackStack()
|
||||||
|
val stillVisible = globalNavController.currentDestination?.id != R.id.globalEmptyFragment
|
||||||
|
return popped || stillVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isGlobalVisible(): Boolean {
|
||||||
|
return findViewById<View>(R.id.global_container).visibility == View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBackPress() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
// 1) 优先关 global
|
||||||
|
if (closeGlobalIfPossible()) return
|
||||||
|
|
||||||
|
// 2) 再 pop 当前tab
|
||||||
|
val popped = currentTabNavController.popBackStack()
|
||||||
|
if (popped) return
|
||||||
|
|
||||||
|
// 3) 当前tab到根了:如果不是home,切回home;否则退出
|
||||||
|
if (currentTabTag != TAB_HOME) {
|
||||||
|
bottomNav.post {
|
||||||
|
bottomNav.selectedItemId = R.id.home_graph
|
||||||
|
}
|
||||||
|
switchTab(TAB_HOME)
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigationFromIntent() {
|
private fun handleNavigationFromIntent() {
|
||||||
val navigateTo = intent.getStringExtra("navigate_to")
|
val navigateTo = intent.getStringExtra("navigate_to")
|
||||||
if (navigateTo == "recharge_fragment") {
|
if (navigateTo == "recharge_fragment") {
|
||||||
// 延迟执行导航,确保导航控制器已经准备好
|
|
||||||
bottomNav.post {
|
bottomNav.post {
|
||||||
try {
|
if (!isLoggedIn()) {
|
||||||
navController.navigate(R.id.action_global_rechargeFragment)
|
openGlobal(R.id.loginFragment)
|
||||||
} catch (e: Exception) {
|
return@post
|
||||||
// 如果导航失败,记录错误日志
|
|
||||||
android.util.Log.e("MainActivity", "Failed to navigate to recharge fragment", e)
|
|
||||||
}
|
}
|
||||||
|
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.view.inputmethod.ExtractedTextRequest
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import java.text.BreakIterator
|
||||||
|
import android.widget.EditText
|
||||||
|
|
||||||
|
|
||||||
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
||||||
@@ -85,6 +86,10 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
private const val TAG = "MyIME"
|
private const val TAG = "MyIME"
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "input_method_channel"
|
private const val NOTIFICATION_CHANNEL_ID = "input_method_channel"
|
||||||
private const val NOTIFICATION_ID = 1
|
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
|
private var emojiKeyboardView: View? = null
|
||||||
@@ -604,6 +609,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showMainKeyboard() {
|
override fun showMainKeyboard() {
|
||||||
|
clearEditorState()
|
||||||
val kb = ensureMainKeyboard()
|
val kb = ensureMainKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputView(kb.rootView)
|
||||||
@@ -611,6 +617,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showNumberKeyboard() {
|
override fun showNumberKeyboard() {
|
||||||
|
clearEditorState()
|
||||||
val kb = ensureNumberKeyboard()
|
val kb = ensureNumberKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputView(kb.rootView)
|
||||||
@@ -618,6 +625,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showSymbolKeyboard() {
|
override fun showSymbolKeyboard() {
|
||||||
|
clearEditorState()
|
||||||
val kb = ensureSymbolKeyboard()
|
val kb = ensureSymbolKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputView(kb.rootView)
|
||||||
@@ -625,6 +633,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showAiKeyboard() {
|
override fun showAiKeyboard() {
|
||||||
|
clearEditorState()
|
||||||
val kb = ensureAiKeyboard()
|
val kb = ensureAiKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputView(kb.rootView)
|
||||||
@@ -632,6 +641,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showEmojiKeyboard() {
|
override fun showEmojiKeyboard() {
|
||||||
|
clearEditorState()
|
||||||
val kb = ensureEmojiKeyboard()
|
val kb = ensureEmojiKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputView(kb.rootView)
|
||||||
@@ -704,24 +714,94 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
playKeyClick()
|
playKeyClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除一个字符(原 handleBackspace)
|
// 删除
|
||||||
override fun deleteOne() {
|
override fun deleteOne() {
|
||||||
val ic = currentInputConnection ?: return
|
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)
|
val selected = ic.getSelectedText(0)
|
||||||
if (!selected.isNullOrEmpty()) {
|
if (!selected.isNullOrEmpty()) {
|
||||||
// 删选区
|
|
||||||
ic.commitText("", 1)
|
ic.commitText("", 1)
|
||||||
} else {
|
scheduleRefreshSuggestions()
|
||||||
// 删光标前一个字符(更同步)
|
playKeyClick()
|
||||||
ic.deleteSurroundingText(1, 0)
|
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()
|
playKeyClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun refreshSuggestionsAfterEdit() {
|
private fun refreshSuggestionsAfterEdit() {
|
||||||
val ic = currentInputConnection ?: return
|
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() {
|
private fun stopRepeatDelete() {
|
||||||
|
|||||||
@@ -26,6 +26,35 @@ interface ApiService {
|
|||||||
@Body body: LoginRequest
|
@Body body: LoginRequest
|
||||||
): ApiResponse<LoginResponse>
|
): 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")
|
@GET("user/detail")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.myapplication.network
|
package com.example.myapplication.network
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
@@ -21,4 +22,7 @@ object AuthEventBus {
|
|||||||
sealed class AuthEvent {
|
sealed class AuthEvent {
|
||||||
data class TokenExpired(val message: String? = null) : AuthEvent()
|
data class TokenExpired(val message: String? = null) : AuthEvent()
|
||||||
data class GenericError(val message: String) : 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 android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import android.content.Context
|
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 等
|
* 请求拦截器:统一加 Header、token 等
|
||||||
*/
|
*/
|
||||||
@@ -102,14 +122,24 @@ val responseInterceptor = Interceptor { chain ->
|
|||||||
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
|
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
|
||||||
|
|
||||||
if (errorResponse.code == 40102) {
|
if (errorResponse.code == 40102) {
|
||||||
Log.w("1314520-HTTP", "token 过期: ${errorResponse.message}")
|
val isNoLoginApi = noLoginRequired(request.url)
|
||||||
|
|
||||||
|
Log.w(
|
||||||
|
"1314520-HTTP",
|
||||||
|
"40102 path=${request.url.encodedPath}, noLogin=$isNoLoginApi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ✅ 只有"需要登录"的接口,才触发全局跳转
|
||||||
|
if (!isNoLoginApi) {
|
||||||
|
AuthEventBus.emit(AuthEvent.TokenExpired(errorResponse.message))
|
||||||
|
}
|
||||||
|
|
||||||
// 只发事件,UI 层去跳转
|
|
||||||
AuthEventBus.emit(AuthEvent.TokenExpired(errorResponse.message))
|
|
||||||
|
|
||||||
return@Interceptor response.newBuilder()
|
return@Interceptor response.newBuilder()
|
||||||
.code(401)
|
.code(401)
|
||||||
.message("Login expired: ${errorResponse.message}")
|
.message(
|
||||||
|
if (isNoLoginApi) response.message
|
||||||
|
else "Login required: ${errorResponse.message}"
|
||||||
|
)
|
||||||
.body(bodyString.toResponseBody(mediaType))
|
.body(bodyString.toResponseBody(mediaType))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,34 @@ data class LoginResponse(
|
|||||||
val token: String
|
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(
|
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 android.widget.TextView
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.example.myapplication.ImeGuideActivity
|
import com.example.myapplication.ImeGuideActivity
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.ApiResponse
|
import com.example.myapplication.network.*
|
||||||
import com.example.myapplication.network.RetrofitClient
|
|
||||||
import com.example.myapplication.network.listByTagWithNotLogin
|
|
||||||
import com.example.myapplication.network.PersonaClick
|
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import com.example.myapplication.network.AddPersonaClick
|
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
@@ -49,21 +46,17 @@ class HomeFragment : Fragment() {
|
|||||||
private lateinit var tabList1: TextView
|
private lateinit var tabList1: TextView
|
||||||
private lateinit var tabList2: TextView
|
private lateinit var tabList2: TextView
|
||||||
private lateinit var backgroundImage: ImageView
|
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 var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
|
||||||
private val sharedPool = RecyclerView.RecycledViewPool()
|
private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>()
|
||||||
|
|
||||||
private var parentWidth = 0
|
private var parentWidth = 0
|
||||||
private var parentHeight = 0
|
private var parentHeight = 0
|
||||||
|
|
||||||
// 你点了哪个 tag(列表二)
|
|
||||||
private var clickedTagId: Int? = null
|
private var clickedTagId: Int? = null
|
||||||
|
|
||||||
// ✅ 列表二:每个 tagId 对应一份 persona 数据,避免串页
|
|
||||||
private val personaCache = mutableMapOf<Int, List<listByTagWithNotLogin>>()
|
|
||||||
|
|
||||||
data class Tag(val id: Int, val tagName: String)
|
data class Tag(val id: Int, val tagName: String)
|
||||||
|
|
||||||
private val tags = mutableListOf<Tag>()
|
private val tags = mutableListOf<Tag>()
|
||||||
|
|
||||||
private val dragToCloseThreshold by lazy {
|
private val dragToCloseThreshold by lazy {
|
||||||
@@ -71,11 +64,9 @@ class HomeFragment : Fragment() {
|
|||||||
(dp * resources.displayMetrics.density)
|
(dp * resources.displayMetrics.density)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val list1Adapter: List1Adapter by lazy {
|
// ---------------- ViewPager2 + Tabs ----------------
|
||||||
List1Adapter { item: String ->
|
private var sheetAdapter: SheetPagerAdapter? = null
|
||||||
Log.d("HomeFragment", "list1 click: $item")
|
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
preloadJob?.cancel()
|
preloadJob?.cancel()
|
||||||
@@ -89,23 +80,56 @@ class HomeFragment : Fragment() {
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? = inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 {
|
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 {
|
view.findViewById<ImageView>(R.id.floatingImage).setOnClickListener {
|
||||||
if (isAdded) {
|
if (!isAdded) return@setOnClickListener
|
||||||
startActivity(Intent(requireActivity(), ImeGuideActivity::class.java))
|
startActivity(Intent(requireActivity(), ImeGuideActivity::class.java))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrim = view.findViewById(R.id.view_scrim)
|
scrim = view.findViewById(R.id.view_scrim)
|
||||||
@@ -117,11 +141,14 @@ class HomeFragment : Fragment() {
|
|||||||
tabList2 = view.findViewById(R.id.tab_list2)
|
tabList2 = view.findViewById(R.id.tab_list2)
|
||||||
viewPager = view.findViewById(R.id.viewPager)
|
viewPager = view.findViewById(R.id.viewPager)
|
||||||
viewPager.isSaveEnabled = false
|
viewPager.isSaveEnabled = false
|
||||||
|
viewPager.offscreenPageLimit = 2
|
||||||
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
|
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
|
||||||
|
|
||||||
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
|
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
|
||||||
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
|
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
|
||||||
|
|
||||||
root.post {
|
root.post {
|
||||||
|
if (!isAdded) return@post
|
||||||
parentWidth = root.width
|
parentWidth = root.width
|
||||||
parentHeight = root.height
|
parentHeight = root.height
|
||||||
}
|
}
|
||||||
@@ -130,43 +157,96 @@ class HomeFragment : Fragment() {
|
|||||||
setupBottomSheet(view)
|
setupBottomSheet(view)
|
||||||
setupTopTabs()
|
setupTopTabs()
|
||||||
|
|
||||||
// 先把 ViewPager / Tags 初始化为空(避免你下面网络回来前被调用多次)
|
// ✅ setupViewPager 只初始化一次
|
||||||
setupViewPager()
|
setupViewPagerOnce()
|
||||||
|
|
||||||
|
// 标签 UI 初始为空
|
||||||
setupTags()
|
setupTags()
|
||||||
|
|
||||||
//刚进来强制显示列表1
|
// 默认显示列表1
|
||||||
viewPager.setCurrentItem(0, false)
|
viewPager.setCurrentItem(0, false)
|
||||||
updateTabsAndTags(0)
|
updateTabsAndTags(0)
|
||||||
|
|
||||||
// 加载标签列表(列表一)
|
// 加载列表一
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val list = fetchAllPersonaList()
|
val list = fetchAllPersonaList()
|
||||||
|
if (!isAdded) return@launch
|
||||||
allPersonaCache = list
|
allPersonaCache = list
|
||||||
viewPager.adapter?.notifyItemChanged(0) // 只刷新第一页
|
|
||||||
|
// ✅ 关键:数据变了就清 renderKey,允许重建一次 UI
|
||||||
|
lastList1RenderKey = null
|
||||||
|
|
||||||
|
notifyPageChangedOnMain(0)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("1314520-HomeFragment", "获取列表一失败", e)
|
Log.e("1314520-HomeFragment", "获取列表一失败", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 拉标签 + 默认加载第一个 tag 的 persona(列表二第一个页)
|
|
||||||
|
// 拉标签 + 预加载
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val response = RetrofitClient.apiService.tagList()
|
val response = RetrofitClient.apiService.tagList()
|
||||||
|
if (!isAdded) return@launch
|
||||||
|
|
||||||
tags.clear()
|
tags.clear()
|
||||||
response.data?.let { networkTags ->
|
response.data?.let { networkTags ->
|
||||||
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
|
tags.addAll(networkTags.map { Tag(it.id, it.tagName) })
|
||||||
}
|
}
|
||||||
// 刷新:页数和标签栏
|
|
||||||
setupViewPager()
|
// ✅ 只更新页数(不重建 adapter/callback)
|
||||||
|
sheetAdapter?.updatePageCount(1 + tags.size)
|
||||||
|
|
||||||
|
// 重新画 tags
|
||||||
setupTags()
|
setupTags()
|
||||||
startPreloadAllTags()
|
|
||||||
|
// ✅ 预加载只填缓存,不刷 UI
|
||||||
|
startPreloadAllTagsFillCacheOnly()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("1314520-HomeFragment", "获取标签失败", e)
|
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) {
|
private fun initDrag(target: View, parent: ViewGroup) {
|
||||||
var dX = 0f
|
var dX = 0f
|
||||||
var dY = 0f
|
var dY = 0f
|
||||||
@@ -245,6 +325,7 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- BottomSheet 行为 ----------------
|
// ---------------- BottomSheet 行为 ----------------
|
||||||
|
|
||||||
private fun setupBottomSheet(root: View) {
|
private fun setupBottomSheet(root: View) {
|
||||||
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
|
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
|
||||||
|
|
||||||
@@ -254,6 +335,7 @@ class HomeFragment : Fragment() {
|
|||||||
bottomSheetBehavior.halfExpandedRatio = 0.7f
|
bottomSheetBehavior.halfExpandedRatio = 0.7f
|
||||||
|
|
||||||
root.post {
|
root.post {
|
||||||
|
if (!isAdded) return@post
|
||||||
val coordinatorHeight = root.height - 40
|
val coordinatorHeight = root.height - 40
|
||||||
val button = root.findViewById<View>(R.id.rechargeButton)
|
val button = root.findViewById<View>(R.id.rechargeButton)
|
||||||
val peek = (coordinatorHeight - button.bottom).coerceAtLeast(200)
|
val peek = (coordinatorHeight - button.bottom).coerceAtLeast(200)
|
||||||
@@ -296,69 +378,17 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- ViewPager2 + Tabs ----------------
|
// ---------------- 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun setupTopTabs() {
|
private fun setupTopTabs() {
|
||||||
tabList1.setOnClickListener { viewPager.currentItem = 0 }
|
tabList1.setOnClickListener { viewPager.currentItem = 0 }
|
||||||
tabList2.setOnClickListener {
|
tabList2.setOnClickListener {
|
||||||
// 没有标签就别切
|
|
||||||
if (tags.isNotEmpty()) viewPager.currentItem = 1
|
if (tags.isNotEmpty()) viewPager.currentItem = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- Tags ----------------
|
||||||
|
|
||||||
private fun setupTags() {
|
private fun setupTags() {
|
||||||
tagContainer.removeAllViews()
|
tagContainer.removeAllViews()
|
||||||
|
|
||||||
@@ -369,28 +399,26 @@ class HomeFragment : Fragment() {
|
|||||||
tv.setOnClickListener {
|
tv.setOnClickListener {
|
||||||
clickedTagId = tag.id
|
clickedTagId = tag.id
|
||||||
val pagePos = 1 + index
|
val pagePos = 1 + index
|
||||||
|
|
||||||
// ✅ 先切页:用户体感立刻响应
|
// 先切页:响应快
|
||||||
viewPager.setCurrentItem(pagePos, true)
|
viewPager.setCurrentItem(pagePos, true)
|
||||||
|
|
||||||
// ✅ 有缓存就不阻塞(可选:同时后台刷新)
|
// ✅ Tag 切换不触发多余 notify:
|
||||||
|
// 1) 有缓存:不 notify(onBind 会直接显示缓存)
|
||||||
val cached = personaCache[tag.id]
|
val cached = personaCache[tag.id]
|
||||||
if (cached != null) {
|
if (cached != null) return@setOnClickListener
|
||||||
viewPager.adapter?.notifyItemChanged(pagePos)
|
|
||||||
return@setOnClickListener
|
// 2) 没缓存:只 notify 一次,让页显示 loading
|
||||||
}
|
notifyPageChangedOnMain(pagePos)
|
||||||
|
|
||||||
// ✅ 没缓存:页内显示 loading(你 onBind 已经处理 cached==null 的 loading)
|
// 后台拉取,回来再 notify 一次
|
||||||
viewPager.adapter?.notifyItemChanged(pagePos)
|
|
||||||
|
|
||||||
// 后台拉取,回来只刷新这一页
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val list = fetchPersonaByTag(tag.id)
|
val list = fetchPersonaByTag(tag.id)
|
||||||
|
if (!isAdded) return@launch
|
||||||
personaCache[tag.id] = list
|
personaCache[tag.id] = list
|
||||||
viewPager.adapter?.notifyItemChanged(pagePos) // ✅ 只刷新这一页
|
notifyPageChangedOnMain(pagePos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tagContainer.addView(tv)
|
tagContainer.addView(tv)
|
||||||
}
|
}
|
||||||
@@ -399,14 +427,16 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTabsAndTags(position: Int) {
|
private fun updateTabsAndTags(position: Int) {
|
||||||
|
val ctx = context ?: return
|
||||||
|
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
tabList1.setTextColor(requireContext().getColor(R.color.black))
|
tabList1.setTextColor(ctx.getColor(R.color.black))
|
||||||
tabList2.setTextColor(requireContext().getColor(R.color.light_black))
|
tabList2.setTextColor(ctx.getColor(R.color.light_black))
|
||||||
tagScroll.isVisible = false
|
tagScroll.isVisible = false
|
||||||
fadeImage(backgroundImage, R.drawable.option_background)
|
fadeImage(backgroundImage, R.drawable.option_background)
|
||||||
} else {
|
} else {
|
||||||
tabList1.setTextColor(requireContext().getColor(R.color.light_black))
|
tabList1.setTextColor(ctx.getColor(R.color.light_black))
|
||||||
tabList2.setTextColor(requireContext().getColor(R.color.black))
|
tabList2.setTextColor(ctx.getColor(R.color.black))
|
||||||
tagScroll.isVisible = true
|
tagScroll.isVisible = true
|
||||||
fadeImage(backgroundImage, R.drawable.option_background_two)
|
fadeImage(backgroundImage, R.drawable.option_background_two)
|
||||||
|
|
||||||
@@ -416,8 +446,9 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fadeImage(imageView: ImageView, newImageRes: Int) {
|
private fun fadeImage(imageView: ImageView, newImageRes: Int) {
|
||||||
|
val ctx = context ?: return
|
||||||
val oldDrawable = imageView.drawable
|
val oldDrawable = imageView.drawable
|
||||||
val newDrawable = ContextCompat.getDrawable(requireContext(), newImageRes) ?: return
|
val newDrawable = ContextCompat.getDrawable(ctx, newImageRes) ?: return
|
||||||
|
|
||||||
if (oldDrawable == null) {
|
if (oldDrawable == null) {
|
||||||
imageView.setImageDrawable(newDrawable)
|
imageView.setImageDrawable(newDrawable)
|
||||||
@@ -430,14 +461,17 @@ class HomeFragment : Fragment() {
|
|||||||
imageView.setImageDrawable(transitionDrawable)
|
imageView.setImageDrawable(transitionDrawable)
|
||||||
transitionDrawable.startTransition(300)
|
transitionDrawable.startTransition(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun highlightTag(index: Int) {
|
private fun highlightTag(index: Int) {
|
||||||
|
val ctx = context ?: return
|
||||||
|
|
||||||
for (i in 0 until tagContainer.childCount) {
|
for (i in 0 until tagContainer.childCount) {
|
||||||
val child = tagContainer.getChildAt(i) as TextView
|
val child = tagContainer.getChildAt(i) as TextView
|
||||||
if (i == index) {
|
if (i == index) {
|
||||||
child.setBackgroundResource(R.drawable.tag_selected_bg)
|
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 {
|
tagScroll.post {
|
||||||
|
if (!isAdded) return@post
|
||||||
val scrollViewWidth = tagScroll.width
|
val scrollViewWidth = tagScroll.width
|
||||||
val childCenter = child.left + child.width / 2
|
val childCenter = child.left + child.width / 2
|
||||||
val targetScrollX = childCenter - scrollViewWidth / 2
|
val targetScrollX = childCenter - scrollViewWidth / 2
|
||||||
@@ -445,25 +479,71 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
child.setBackgroundResource(R.drawable.tag_unselected_bg)
|
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 ----------------
|
// ---------------- ViewPager Adapter ----------------
|
||||||
|
|
||||||
inner class SheetPagerAdapter(
|
inner class SheetPagerAdapter(
|
||||||
private var pageCount: Int
|
private var pageCount: Int
|
||||||
) : RecyclerView.Adapter<SheetPagerAdapter.PageViewHolder>() {
|
) : RecyclerView.Adapter<SheetPagerAdapter.PageViewHolder>() {
|
||||||
|
|
||||||
inner class PageViewHolder(val root: View) : RecyclerView.ViewHolder(root)
|
inner class PageViewHolder(val root: View) : RecyclerView.ViewHolder(root)
|
||||||
|
|
||||||
fun updatePageCount(newCount: Int) {
|
fun updatePageCount(newCount: Int) {
|
||||||
|
if (newCount == pageCount) return
|
||||||
|
|
||||||
|
val old = pageCount
|
||||||
pageCount = newCount
|
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
|
override fun getItemViewType(position: Int): Int = if (position == 0) 0 else 1
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
|
||||||
val layoutId = if (viewType == 0) {
|
val layoutId = if (viewType == 0) {
|
||||||
R.layout.bottom_page_list1
|
R.layout.bottom_page_list1
|
||||||
@@ -473,42 +553,50 @@ class HomeFragment : Fragment() {
|
|||||||
val root = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
|
val root = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
|
||||||
return PageViewHolder(root)
|
return PageViewHolder(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
|
||||||
val root = holder.root
|
val root = holder.root
|
||||||
|
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
renderList1(root, allPersonaCache)
|
renderList1(root, allPersonaCache)
|
||||||
} else {
|
} else {
|
||||||
val rv2 = root.findViewById<RecyclerView>(R.id.recyclerView)
|
val rv2 = root.findViewById<RecyclerView>(R.id.recyclerView)
|
||||||
val loadingView = root.findViewById<View>(R.id.loadingView)
|
val loadingView = root.findViewById<View>(R.id.loadingView)
|
||||||
|
|
||||||
rv2.setHasFixedSize(true)
|
rv2.setHasFixedSize(true)
|
||||||
|
|
||||||
|
// ✅ 禁止 itemAnimator(减少 layout 抖动)
|
||||||
rv2.itemAnimator = null
|
rv2.itemAnimator = null
|
||||||
|
|
||||||
rv2.isNestedScrollingEnabled = false
|
rv2.isNestedScrollingEnabled = false
|
||||||
|
|
||||||
var adapter = rv2.adapter as? PersonaAdapter
|
var adapter = rv2.adapter as? PersonaAdapter
|
||||||
if (adapter == null) {
|
if (adapter == null) {
|
||||||
adapter = PersonaAdapter { click ->
|
adapter = PersonaAdapter { click ->
|
||||||
when (click) {
|
when (click) {
|
||||||
is PersonaClick.Item -> {
|
is PersonaClick.Item -> {
|
||||||
val id = click.persona.id
|
val id = click.persona.id
|
||||||
|
if (!isAdded || childFragmentManager.isStateSaved) return@PersonaAdapter
|
||||||
PersonaDetailDialogFragment
|
PersonaDetailDialogFragment
|
||||||
.newInstance(id)
|
.newInstance(id)
|
||||||
.show(childFragmentManager, "persona_detail")
|
.show(childFragmentManager, "persona_detail")
|
||||||
}
|
}
|
||||||
|
|
||||||
is PersonaClick.Add -> {
|
is PersonaClick.Add -> {
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (click.persona.added == true) {
|
try {
|
||||||
click.persona.id?.let { id ->
|
if (click.persona.added == true) {
|
||||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
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 {
|
} catch (_: Exception) {
|
||||||
val req = AddPersonaClick(
|
|
||||||
characterId = click.persona.id?.toInt() ?: 0,
|
|
||||||
emoji = click.persona.emoji ?: ""
|
|
||||||
)
|
|
||||||
RetrofitClient.apiService.addUserCharacter(req)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,17 +605,17 @@ class HomeFragment : Fragment() {
|
|||||||
rv2.layoutManager = GridLayoutManager(root.context, 2)
|
rv2.layoutManager = GridLayoutManager(root.context, 2)
|
||||||
rv2.adapter = adapter
|
rv2.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagIndex = position - 1
|
val tagIndex = position - 1
|
||||||
if (tagIndex !in tags.indices) {
|
if (tagIndex !in tags.indices) {
|
||||||
loadingView.isVisible = false
|
loadingView.isVisible = false
|
||||||
adapter.submitList(emptyList())
|
adapter.submitList(emptyList())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagId = tags[tagIndex].id
|
val tagId = tags[tagIndex].id
|
||||||
val cached = personaCache[tagId]
|
val cached = personaCache[tagId]
|
||||||
|
|
||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
loadingView.isVisible = true
|
loadingView.isVisible = true
|
||||||
adapter.submitList(emptyList())
|
adapter.submitList(emptyList())
|
||||||
@@ -537,175 +625,147 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = pageCount
|
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>) {
|
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 sorted = list.sortedBy { it.rank ?: Int.MAX_VALUE }
|
||||||
|
|
||||||
val top3 = sorted.take(3)
|
val top3 = sorted.take(3)
|
||||||
val others = if (sorted.size > 3) sorted.drop(3) else emptyList()
|
val others = if (sorted.size > 3) sorted.drop(3) else emptyList()
|
||||||
|
|
||||||
// 2) 绑定前三名(注意:你的 UI 排列是:第二/第一/第三)
|
bindTopItem(
|
||||||
bindTopItem(root,
|
root,
|
||||||
avatarId = R.id.avatar_first,
|
avatarId = R.id.avatar_first,
|
||||||
nameId = R.id.name_first,
|
nameId = R.id.name_first,
|
||||||
addBtnId = R.id.btn_add_first,
|
addBtnId = R.id.btn_add_first,
|
||||||
container = R.id.container_first,
|
containerId = R.id.container_first,
|
||||||
item = top3.getOrNull(0) // rank 最小 = 第一名
|
item = top3.getOrNull(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
bindTopItem(root,
|
bindTopItem(
|
||||||
|
root,
|
||||||
avatarId = R.id.avatar_second,
|
avatarId = R.id.avatar_second,
|
||||||
nameId = R.id.name_second,
|
nameId = R.id.name_second,
|
||||||
addBtnId = R.id.btn_add_second,
|
addBtnId = R.id.btn_add_second,
|
||||||
container = R.id.container_second,
|
containerId = R.id.container_second,
|
||||||
item = top3.getOrNull(1) // 第二名
|
item = top3.getOrNull(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
bindTopItem(root,
|
bindTopItem(
|
||||||
|
root,
|
||||||
avatarId = R.id.avatar_third,
|
avatarId = R.id.avatar_third,
|
||||||
nameId = R.id.name_third,
|
nameId = R.id.name_third,
|
||||||
addBtnId = R.id.btn_add_third,
|
addBtnId = R.id.btn_add_third,
|
||||||
container = R.id.container_third,
|
containerId = R.id.container_third,
|
||||||
item = top3.getOrNull(2) // 第三名
|
item = top3.getOrNull(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3) 渲染后面的内容卡片
|
|
||||||
val container = root.findViewById<LinearLayout>(R.id.container_others)
|
val container = root.findViewById<LinearLayout>(R.id.container_others)
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
val inflater = LayoutInflater.from(root.context)
|
val inflater = LayoutInflater.from(root.context)
|
||||||
others.forEach { p ->
|
others.forEach { p ->
|
||||||
val itemView = inflater.inflate(R.layout.item_rank_other, container, false)
|
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_rank).text = (p.rank ?: "--").toString()
|
||||||
itemView.findViewById<TextView>(R.id.tv_name).text = p.characterName ?: ""
|
itemView.findViewById<TextView>(R.id.tv_name).text = p.characterName ?: ""
|
||||||
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
|
itemView.findViewById<TextView>(R.id.tv_desc).text = p.characterBackground ?: ""
|
||||||
|
|
||||||
// 头像
|
|
||||||
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
||||||
// Glide 示例
|
|
||||||
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
|
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
val id = p.id
|
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
|
||||||
Log.d("HomeFragment", "list1 others click id=$id")
|
|
||||||
PersonaDetailDialogFragment
|
PersonaDetailDialogFragment
|
||||||
.newInstance(id)
|
.newInstance(p.id)
|
||||||
.show(childFragmentManager, "persona_detail")
|
.show(childFragmentManager, "persona_detail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只点“添加”按钮
|
|
||||||
itemView.findViewById<View>(R.id.btn_add).setOnClickListener {
|
itemView.findViewById<View>(R.id.btn_add).setOnClickListener {
|
||||||
val id = p.id
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
lifecycleScope.launch {
|
try {
|
||||||
if(p.added == true){
|
if (p.added == true) {
|
||||||
//取消收藏
|
p.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
||||||
p.id?.let { id ->
|
} else {
|
||||||
try {
|
val req = AddPersonaClick(
|
||||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
characterId = p.id?.toInt() ?: 0,
|
||||||
} catch (e: Exception) {
|
emoji = p.emoji ?: ""
|
||||||
// 处理错误
|
)
|
||||||
}
|
RetrofitClient.apiService.addUserCharacter(req)
|
||||||
}
|
|
||||||
}else{
|
|
||||||
val addPersonaRequest = AddPersonaClick(
|
|
||||||
characterId = p.id?.toInt() ?: 0,
|
|
||||||
emoji = p.emoji ?: ""
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 处理错误
|
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.addView(itemView)
|
container.addView(itemView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindTopItem(
|
private fun bindTopItem(
|
||||||
root: View,
|
root: View,
|
||||||
avatarId: Int,
|
avatarId: Int,
|
||||||
nameId: Int,
|
nameId: Int,
|
||||||
addBtnId: Int,
|
addBtnId: Int,
|
||||||
container: Int,
|
containerId: Int,
|
||||||
item: listByTagWithNotLogin?
|
item: listByTagWithNotLogin?
|
||||||
) {
|
) {
|
||||||
val avatar = root.findViewById<de.hdodenhof.circleimageview.CircleImageView>(avatarId)
|
val avatar = root.findViewById<de.hdodenhof.circleimageview.CircleImageView>(avatarId)
|
||||||
val name = root.findViewById<TextView>(nameId)
|
val name = root.findViewById<TextView>(nameId)
|
||||||
val addBtn = root.findViewById<View>(addBtnId)
|
val addBtn = root.findViewById<View>(addBtnId)
|
||||||
val container = root.findViewById<LinearLayout>(container)
|
val container = root.findViewById<LinearLayout>(containerId)
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
// 没数据就隐藏(或者显示占位)
|
|
||||||
// avatar.isVisible = false
|
|
||||||
name.isVisible = false
|
name.isVisible = false
|
||||||
addBtn.isVisible = false
|
addBtn.isVisible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar.isVisible = true
|
|
||||||
name.isVisible = true
|
name.isVisible = true
|
||||||
addBtn.isVisible = true
|
addBtn.isVisible = true
|
||||||
|
|
||||||
name.text = item.characterName ?: ""
|
name.text = item.characterName ?: ""
|
||||||
|
|
||||||
// 头像
|
|
||||||
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
|
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
|
||||||
|
|
||||||
addBtn.setOnClickListener {
|
addBtn.setOnClickListener {
|
||||||
val id = item.id
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
lifecycleScope.launch {
|
try {
|
||||||
if(item.added == true){
|
if (item.added == true) {
|
||||||
//取消收藏
|
item.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
||||||
item.id?.let { id ->
|
} else {
|
||||||
try {
|
val req = AddPersonaClick(
|
||||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
characterId = item.id?.toInt() ?: 0,
|
||||||
} catch (e: Exception) {
|
emoji = item.emoji ?: ""
|
||||||
// 处理错误
|
)
|
||||||
}
|
RetrofitClient.apiService.addUserCharacter(req)
|
||||||
}
|
|
||||||
}else{
|
|
||||||
val addPersonaRequest = AddPersonaClick(
|
|
||||||
characterId = item.id?.toInt() ?: 0,
|
|
||||||
emoji = item.emoji ?: ""
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 处理错误
|
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.setOnClickListener {
|
container.setOnClickListener {
|
||||||
val id = item.id
|
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
|
||||||
Log.d("HomeFragment", "list1 top click id=$id rank=${item.rank}")
|
|
||||||
PersonaDetailDialogFragment
|
PersonaDetailDialogFragment
|
||||||
.newInstance(id)
|
.newInstance(item.id)
|
||||||
.show(childFragmentManager, "persona_detail")
|
.show(childFragmentManager, "persona_detail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------- 网络请求 ----------------
|
// ---------------- 网络请求 ----------------
|
||||||
|
|
||||||
private suspend fun fetchPersonaByTag(tagId: Int): List<listByTagWithNotLogin> {
|
private suspend fun fetchPersonaByTag(tagId: Int): List<listByTagWithNotLogin> {
|
||||||
return try {
|
return try {
|
||||||
val resp = if (!isLoggedIn()) {
|
val resp = if (!isLoggedIn()) {
|
||||||
@@ -715,54 +775,36 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
resp.data ?: emptyList()
|
resp.data ?: emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if(!isLoggedIn()){
|
Log.e("1314520-HomeFragment", "按标签获取人设列表失败 tagId=$tagId", e)
|
||||||
//未登录用户获取人设列表
|
|
||||||
Log.e("1314520-HomeFragment", "未登录根据标签获取人设列表", e)
|
|
||||||
}else{
|
|
||||||
Log.e("1314520-HomeFragment", "登录根据标签获取人设列表", e)
|
|
||||||
}
|
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchAllPersonaList(): List<listByTagWithNotLogin> {
|
private suspend fun fetchAllPersonaList(): List<listByTagWithNotLogin> {
|
||||||
return try {
|
return try {
|
||||||
val personaData = if (!isLoggedIn()) {
|
val resp = if (!isLoggedIn()) {
|
||||||
RetrofitClient.apiService.personaListWithNotLogin()
|
RetrofitClient.apiService.personaListWithNotLogin()
|
||||||
} else {
|
} else {
|
||||||
RetrofitClient.apiService.personaByTag()
|
RetrofitClient.apiService.personaByTag()
|
||||||
}
|
}
|
||||||
personaData.data ?: emptyList()
|
resp.data ?: emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if(!isLoggedIn()){
|
Log.e("1314520-HomeFragment", "获取列表一失败", e)
|
||||||
//未登录用户获取人设列表
|
|
||||||
Log.e("1314520-HomeFragment", "未登录用户人设列表", e)
|
|
||||||
}else{
|
|
||||||
Log.e("1314520-HomeFragment", "登录用户人设列表", e)
|
|
||||||
}
|
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getpersonaLis(id: Int): ApiResponse<List<listByTagWithNotLogin>>? {
|
// ✅ 不再 requireContext(),避免 detach 直接崩
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLoggedIn(): Boolean {
|
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 kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import com.example.myapplication.ui.shop.ShopEvent
|
||||||
|
import com.example.myapplication.ui.shop.ShopEventBus
|
||||||
|
|
||||||
class KeyboardDetailFragment : Fragment() {
|
class KeyboardDetailFragment : Fragment() {
|
||||||
|
|
||||||
@@ -291,6 +293,8 @@ class KeyboardDetailFragment : Fragment() {
|
|||||||
if (response?.code == 0) {
|
if (response?.code == 0) {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShopEventBus.post(ShopEvent.ThemePurchased)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KeyboardDetailFragment", "购买主题失败", e)
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import android.widget.EditText
|
||||||
import com.example.myapplication.R
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
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() {
|
class ForgetPasswordEmailFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var emailEditText: EditText // 邮箱输入框
|
||||||
|
private var loadingOverlay: com.example.myapplication.ui.common.LoadingOverlay? = null // 加载遮罩层
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -20,12 +31,63 @@ class ForgetPasswordEmailFragment : Fragment() {
|
|||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(R.layout.fragment_forget_password_email, container, false)
|
return inflater.inflate(R.layout.fragment_forget_password_email, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 {
|
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
|
package com.example.myapplication.ui.login
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.example.myapplication.R
|
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() {
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -17,5 +40,98 @@ class ForgetPasswordResetFragment : Fragment() {
|
|||||||
return inflater.inflate(R.layout.fragment_forget_password_reset, container, false)
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.navigation.fragment.findNavController
|
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() {
|
class ForgetPasswordVerifyFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var codeInputs: List<EditText>
|
private lateinit var codeInputs: List<EditText>
|
||||||
|
private var loadingOverlay: com.example.myapplication.ui.common.LoadingOverlay? = null//加载遮罩层
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -29,12 +37,47 @@ class ForgetPasswordVerifyFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
loadingOverlay = com.example.myapplication.ui.common.LoadingOverlay.attach(view as ViewGroup)
|
||||||
|
|
||||||
//验证码页面
|
val savedEmail = EncryptedSharedPreferencesUtil.get(requireContext(), "forget_email", String::class.java)
|
||||||
view.findViewById<
|
|
||||||
TextView>(R.id.nextstep).setOnClickListener {
|
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.")
|
||||||
findNavController().navigate(R.id.action_forgetPasswordVerifyFragment_to_forgetPasswordResetFragment)
|
|
||||||
}
|
|
||||||
|
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>(
|
codeInputs = listOf<EditText>(
|
||||||
view.findViewById(R.id.et_code_1),
|
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 com.google.android.material.button.MaterialButton
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.coroutines.launch
|
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() {
|
class LoginFragment : Fragment() {
|
||||||
|
|
||||||
@@ -52,18 +57,14 @@ class LoginFragment : Fragment() {
|
|||||||
|
|
||||||
// 注册
|
// 注册
|
||||||
view.findViewById<TextView>(R.id.tv_signup).setOnClickListener {
|
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 {
|
view.findViewById<TextView>(R.id.tv_forgot_password).setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_loginFragment_to_forgetPasswordEmailFragment)
|
findNavController().navigate(R.id.action_loginFragment_to_forgetPasswordEmailFragment)
|
||||||
}
|
}
|
||||||
// 返回按钮
|
// 返回 - 在global_graph中,直接popBackStack回到globalEmptyFragment
|
||||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||||
findNavController().previousBackStackEntry
|
|
||||||
?.savedStateHandle
|
|
||||||
?.set("from_login", true)
|
|
||||||
|
|
||||||
findNavController().popBackStack()
|
findNavController().popBackStack()
|
||||||
}
|
}
|
||||||
// 绑定控件(id 必须和 xml 里的一样)
|
// 绑定控件(id 必须和 xml 里的一样)
|
||||||
@@ -122,7 +123,9 @@ class LoginFragment : Fragment() {
|
|||||||
if (response.code == 0) {
|
if (response.code == 0) {
|
||||||
EncryptedSharedPreferencesUtil.save(requireContext(), "user", response.data)
|
EncryptedSharedPreferencesUtil.save(requireContext(), "user", response.data)
|
||||||
EncryptedSharedPreferencesUtil.save(requireContext(), "email",email)
|
EncryptedSharedPreferencesUtil.save(requireContext(), "email",email)
|
||||||
findNavController().popBackStack()
|
// 触发登录成功事件,让MainActivity关闭全局overlay
|
||||||
|
AuthEventBus.emit(AuthEvent.LoginSuccess)
|
||||||
|
// 不在这里popBackStack,让MainActivity的LoginSuccess事件处理关闭全局overlay
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), "Login failed: ${response.message}", Toast.LENGTH_SHORT).show()
|
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
|
package com.example.myapplication.ui.login
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.example.myapplication.R
|
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() {
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -23,9 +46,114 @@ class RegisterFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 {
|
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
|
package com.example.myapplication.ui.mine
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
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.Fragment
|
||||||
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import android.widget.LinearLayout
|
import com.example.myapplication.network.AuthEvent
|
||||||
import com.example.myapplication.network.RetrofitClient
|
import com.example.myapplication.network.AuthEventBus
|
||||||
import com.example.myapplication.network.LoginResponse
|
import com.example.myapplication.network.LoginResponse
|
||||||
import de.hdodenhof.circleimageview.CircleImageView
|
import com.example.myapplication.network.RetrofitClient
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
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() {
|
class MineFragment : Fragment() {
|
||||||
|
|
||||||
@@ -25,129 +34,179 @@ class MineFragment : Fragment() {
|
|||||||
private lateinit var time: TextView
|
private lateinit var time: TextView
|
||||||
private lateinit var logout: TextView
|
private lateinit var logout: TextView
|
||||||
|
|
||||||
|
private var loadUserJob: Job? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = inflater.inflate(R.layout.fragment_mine, container, false)
|
||||||
return inflater.inflate(R.layout.fragment_mine, container, false)
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
loadUserJob?.cancel()
|
||||||
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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)
|
nickname = view.findViewById(R.id.nickname)
|
||||||
time = view.findViewById(R.id.time)
|
time = view.findViewById(R.id.time)
|
||||||
logout = view.findViewById(R.id.logout)
|
logout = view.findViewById(R.id.logout)
|
||||||
|
|
||||||
|
// 1) 先用本地缓存秒出首屏
|
||||||
|
renderFromCache()
|
||||||
|
|
||||||
// 获取用户信息, 并显示
|
// 2) 首次进入不刷新,由onResume处理
|
||||||
val user = EncryptedSharedPreferencesUtil.get(requireContext(), "Personal_information", LoginResponse::class.java)
|
|
||||||
nickname.text = user?.nickName ?: ""
|
// // ✅ 手动刷新:不改布局也能用
|
||||||
time.text = user?.vipExpiry?.let { "Due on November $it" } ?: ""
|
// // - 点昵称刷新
|
||||||
|
// nickname.setOnClickListener { refreshUser(force = true, showToast = true) }
|
||||||
// 2) 下一帧再请求网络(让首帧先出来)
|
// // - 长按 time 刷新
|
||||||
view.post {
|
// time.setOnLongClickListener {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
// refreshUser(force = true, showToast = true)
|
||||||
try {
|
// true
|
||||||
val response = RetrofitClient.apiService.getUser()
|
// }
|
||||||
nickname.text = response.data?.nickName ?: ""
|
|
||||||
time.text = response.data?.vipExpiry?.let { "Due on November $it" } ?: ""
|
logout.setOnClickListener {
|
||||||
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", response.data)
|
LogoutDialogFragment { doLogout() }
|
||||||
} catch (e: Exception) {
|
.show(parentFragmentManager, "logout_dialog")
|
||||||
Log.e("1314520-MineFragment", "获取失败", e)
|
}
|
||||||
|
|
||||||
|
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 {
|
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
|
package com.example.myapplication.ui.shop
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -14,41 +13,391 @@ import android.widget.HorizontalScrollView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
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.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.ApiResponse
|
import com.example.myapplication.network.*
|
||||||
import com.example.myapplication.network.RetrofitClient
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import com.example.myapplication.network.Theme
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.example.myapplication.network.Wallet
|
import kotlinx.coroutines.Job
|
||||||
import com.example.myapplication.network.themeStyle
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class ShopFragment : Fragment(R.layout.fragment_shop) {
|
class ShopFragment : Fragment(R.layout.fragment_shop) {
|
||||||
|
|
||||||
|
// ===== View =====
|
||||||
private lateinit var viewPager: ViewPager2
|
private lateinit var viewPager: ViewPager2
|
||||||
private lateinit var tagScroll: HorizontalScrollView
|
private lateinit var tagScroll: HorizontalScrollView
|
||||||
private lateinit var tagContainer: LinearLayout
|
private lateinit var tagContainer: LinearLayout
|
||||||
private lateinit var balance: TextView
|
private lateinit var balance: TextView
|
||||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
|
|
||||||
// 风格 tabs
|
// ===== Data =====
|
||||||
private var tabTitles: List<Theme> = emptyList()
|
private var tabTitles: List<Theme> = emptyList()
|
||||||
private var styleIds: List<Int> = emptyList()
|
private var styleIds: List<Int> = emptyList()
|
||||||
|
|
||||||
// ✅ 共享数据/缓存/加载都交给 VM
|
// ===== ViewModel =====
|
||||||
private val vm: ShopViewModel by viewModels()
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 {
|
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 {
|
view.findViewById<View>(R.id.skinButton).setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_shopfragment_to_myskin)
|
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 {
|
view.findViewById<View>(R.id.searchButton).setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_shopfragment_to_searchfragment)
|
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 androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
import com.example.myapplication.network.themeStyle
|
import com.example.myapplication.network.themeStyle
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
|
||||||
@@ -54,7 +56,8 @@ class ThemeCardAdapter : ListAdapter<themeStyle, ThemeCardAdapter.ThemeCardViewH
|
|||||||
val bundle = Bundle().apply {
|
val bundle = Bundle().apply {
|
||||||
putInt("themeId", theme.id)
|
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 androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
import com.example.myapplication.network.themeStyle
|
import com.example.myapplication.network.themeStyle
|
||||||
|
|
||||||
class MySkinAdapter(
|
class MySkinAdapter(
|
||||||
@@ -101,7 +103,8 @@ class MySkinAdapter(
|
|||||||
val bundle = Bundle().apply {
|
val bundle = Bundle().apply {
|
||||||
putInt("themeId", item.id)
|
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"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<!-- 页面内容区域:用于切换 Fragment -->
|
<FrameLayout
|
||||||
<androidx.fragment.app.FragmentContainerView
|
android:id="@+id/root_container"
|
||||||
android:id="@+id/nav_host_fragment"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
app:navGraph="@navigation/nav_graph"
|
|
||||||
app:defaultNavHost="true" />
|
<!-- 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
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
android:id="@+id/bottom_nav"
|
android:id="@+id/bottom_nav"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -27,4 +38,4 @@
|
|||||||
app:itemIconTint="@null"
|
app:itemIconTint="@null"
|
||||||
app:itemTextColor="@color/nav_text_color"/>
|
app:itemTextColor="@color/nav_text_color"/>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
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"/>
|
android:textColor="#1B1F1A"/>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_username"
|
android:id="@+id/et_email"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
android:layout_marginTop="14dp"
|
android:layout_marginTop="14dp"
|
||||||
android:layout_height="52dp">
|
android:layout_height="52dp">
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_password"
|
android:id="@+id/et_confirm_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:inputType="textPassword" />
|
android:inputType="textPassword" />
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_toggle"
|
android:id="@+id/iv_confirm_toggle"
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/btn_login"
|
android:id="@+id/nextstep"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 忘记密码验证码输入页面 -->
|
<!-- 验证码输入页面 -->
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
@@ -59,6 +59,17 @@
|
|||||||
android:text="Enter email verification code"
|
android:text="Enter email verification code"
|
||||||
android:textColor="#1B1F1A"/>
|
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
|
<LinearLayout
|
||||||
android:id="@+id/ll_code_container"
|
android:id="@+id/ll_code_container"
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="54dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:src="@drawable/ai_dialogue"
|
android:src="@drawable/ai_dialogue"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
@@ -103,8 +103,8 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="54dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:src="@drawable/personalized_keyboard"
|
android:src="@drawable/personalized_keyboard"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
@@ -127,8 +127,8 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="54dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:src="@drawable/chat_persona"
|
android:src="@drawable/chat_persona"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
@@ -151,8 +151,8 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="54dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:src="@drawable/emotional_counseling"
|
android:src="@drawable/emotional_counseling"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
<!-- 输入框 -->
|
<!-- 输入框 -->
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_username"
|
android:id="@+id/et_email"
|
||||||
android:layout_width="315dp"
|
android:layout_width="315dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
android:layout_marginTop="14dp"
|
android:layout_marginTop="14dp"
|
||||||
android:layout_height="52dp">
|
android:layout_height="52dp">
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_password"
|
android:id="@+id/et_confirm_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
android:textColorHint="#CBCBCB"
|
android:textColorHint="#CBCBCB"
|
||||||
android:inputType="textPassword" />
|
android:inputType="textPassword" />
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_toggle"
|
android:id="@+id/iv_confirm_toggle"
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
@@ -161,9 +161,9 @@
|
|||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:src="@drawable/hide" />
|
android:src="@drawable/hide" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
<!-- 登录按钮 -->
|
<!-- 下一步按钮 -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/btn_login"
|
android:id="@+id/btn_next_step"
|
||||||
android:layout_width="315dp"
|
android:layout_width="315dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="#FFFFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:text="Login" />
|
android:text="Next step" />
|
||||||
|
|
||||||
<com.google.android.flexbox.FlexboxLayout
|
<com.google.android.flexbox.FlexboxLayout
|
||||||
android:id="@+id/agreement_container"
|
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">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/homeFragment"
|
android:id="@+id/home_graph"
|
||||||
android:icon="@drawable/home_selector"
|
android:icon="@drawable/ic_home_selector"/>
|
||||||
android:title="Home" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/shopFragment"
|
android:id="@+id/shop_graph"
|
||||||
android:icon="@drawable/shop_selector"
|
android:icon="@drawable/ic_shop_selector"/>
|
||||||
android:title="Shop" />
|
|
||||||
|
|
||||||
<!-- <item
|
|
||||||
android:id="@+id/circleFragment"
|
|
||||||
android:icon="@drawable/circle_selector"
|
|
||||||
android:title="Circle" /> -->
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/mineFragment"
|
android:id="@+id/mine_graph"
|
||||||
android:icon="@drawable/mine_selector"
|
android:icon="@drawable/ic_mine_selector" />
|
||||||
android:title="Mine" />
|
|
||||||
|
|
||||||
</menu>
|
</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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/nav_graph"
|
android:id="@+id/nav_graph"
|
||||||
app:startDestination="@id/homeFragment">
|
app:startDestination="@id/home_graph">
|
||||||
|
|
||||||
<!-- 首页 -->
|
<!-- 三个 Tab:独立 back stack -->
|
||||||
<fragment
|
<include app:graph="@navigation/home_graph" />
|
||||||
android:id="@+id/homeFragment"
|
<include app:graph="@navigation/shop_graph" />
|
||||||
android:name="com.example.myapplication.ui.home.HomeFragment"
|
<include app:graph="@navigation/mine_graph" />
|
||||||
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"/>
|
|
||||||
|
|
||||||
|
</navigation>
|
||||||
<!-- 商城 -->
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
</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:gravity">center</item>
|
||||||
<item name="android:textSize">20sp</item>
|
<item name="android:textSize">20sp</item>
|
||||||
<item name="android:maxLength">1</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:background">@drawable/code_box_bg</item>
|
||||||
<item name="android:textColor">#000000</item>
|
<item name="android:textColor">#000000</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||