package com.example.myapplication import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.example.myapplication.network.AuthEvent import com.example.myapplication.network.AuthEventBus import com.example.myapplication.network.BehaviorReporter import com.example.myapplication.utils.EncryptedSharedPreferencesUtil import com.google.android.material.bottomnavigation.BottomNavigationView import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private lateinit var bottomNav: BottomNavigationView private 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 // ======================= // 全局路由埋点:新增字段 // ======================= private val ROUTE_TAG = "RouteReport" private var lastHomeDestIdForReport: Int? = null private var lastShopDestIdForReport: Int? = null private var lastMineDestIdForReport: Int? = null private var lastGlobalDestIdForReport: Int? = null // 统一 listener,方便 add/remove private val homeRouteListener = NavController.OnDestinationChangedListener { _, dest, _ -> if (lastHomeDestIdForReport == dest.id) return@OnDestinationChangedListener lastHomeDestIdForReport = dest.id reportPageView(source = "home_tab", destId = dest.id) } private val shopRouteListener = NavController.OnDestinationChangedListener { _, dest, _ -> if (lastShopDestIdForReport == dest.id) return@OnDestinationChangedListener lastShopDestIdForReport = dest.id reportPageView(source = "shop_tab", destId = dest.id) } private val mineRouteListener = NavController.OnDestinationChangedListener { _, dest, _ -> if (lastMineDestIdForReport == dest.id) return@OnDestinationChangedListener lastMineDestIdForReport = dest.id reportPageView(source = "mine_tab", destId = dest.id) } private val globalRouteListener = NavController.OnDestinationChangedListener { _, dest, _ -> if (lastGlobalDestIdForReport == dest.id) return@OnDestinationChangedListener lastGlobalDestIdForReport = dest.id reportPageView(source = "global_overlay", destId = dest.id) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) bottomNav = findViewById(R.id.bottom_nav) bottomNav.itemIconTintList = null bottomNav.setOnItemReselectedListener { /* ignore */ } // 1) 恢复当前tab currentTabTag = savedInstanceState?.getString("current_tab_tag") ?: TAB_HOME // 2) 初始化/找回 3个Tab host + global host initHosts() // 3) 底栏点击:show/hide 切 tab(带登录拦截 + 防 stateSaved) bottomNav.setOnItemSelectedListener { item -> val tabTag = tabMap[item.itemId] ?: return@setOnItemSelectedListener false // 登录拦截:未登录点受保护tab => 打开全局login if (!isLoggedIn() && item.itemId in protectedTabs) { // 强制回到当前tab的选中状态,避免底栏短暂闪一下 bottomNav.selectedItemId = when (currentTabTag) { TAB_SHOP -> R.id.shop_graph TAB_MINE -> R.id.mine_graph else -> R.id.home_graph } pendingTabAfterLogin = tabTag // 记录目标tab openGlobal(R.id.loginFragment) return@setOnItemSelectedListener false } switchTab(tabTag) true } // 4) token过期:走全局 overlay,且避免重复打开 lifecycleScope.launch { AuthEventBus.events.collectLatest { event -> when (event) { is AuthEvent.TokenExpired -> { pendingTabAfterLogin = null // 已经在login就别重复 if (!isGlobalVisible() || globalNavController.currentDestination?.id != R.id.loginFragment) { openGlobal(R.id.loginFragment) } } is AuthEvent.GenericError -> { Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_LONG).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 // 处理intent跳转目标页 if (pendingNavigationAfterLogin == "recharge_fragment") { openGlobal(R.id.rechargeFragment) pendingNavigationAfterLogin = null } // ✅ 登录成功后也刷新一次 bottomNav.post { updateBottomNavVisibility() } } // 登出事件处理 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) } is AuthEvent.UserUpdated -> { // 不需要处理 } is AuthEvent.CharacterDeleted -> { // 不需要处理 } is AuthEvent.CharacterAdded -> { // 不需要处理,由HomeFragment处理 } } } } // 5) intent跳转(充值等)统一走全局 overlay handleNavigationFromIntent() // 6) 返回键规则:优先关闭global,其次pop当前tab setupBackPress() // 7) 初始选中正确tab(不会触发二次创建) bottomNav.post { bottomNav.selectedItemId = when (currentTabTag) { TAB_SHOP -> R.id.shop_graph TAB_MINE -> R.id.mine_graph else -> R.id.home_graph } updateBottomNavVisibility() } } override fun onResume() { super.onResume() // ✅ 最终兜底:从后台回来 / 某些场景没触发 listener,也能恢复底栏 bottomNav.post { updateBottomNavVisibility() } } override fun onDestroy() { // ✅ 防泄漏:移除路由监听(Activity 销毁时) runCatching { homeHost.navController.removeOnDestinationChangedListener(homeRouteListener) shopHost.navController.removeOnDestinationChangedListener(shopRouteListener) mineHost.navController.removeOnDestinationChangedListener(mineRouteListener) globalHost.navController.removeOnDestinationChangedListener(globalRouteListener) } super.onDestroy() } private fun initHosts() { val fm = supportFragmentManager homeHost = fm.findFragmentByTag(TAB_HOME) as? NavHostFragment ?: NavHostFragment.create(R.navigation.home_graph) shopHost = fm.findFragmentByTag(TAB_SHOP) as? NavHostFragment ?: NavHostFragment.create(R.navigation.shop_graph) mineHost = fm.findFragmentByTag(TAB_MINE) as? NavHostFragment ?: NavHostFragment.create(R.navigation.mine_graph) globalHost = fm.findFragmentByTag(GLOBAL_HOST) as? NavHostFragment ?: NavHostFragment.create(R.navigation.global_graph) // 第一次创建时 add;后续进程重建会自动恢复,无需重复 add if (fm.findFragmentByTag(TAB_HOME) == null) { fm.beginTransaction() .setReorderingAllowed(true) .add(R.id.tab_container, homeHost, TAB_HOME) .add(R.id.tab_container, shopHost, TAB_SHOP).hide(shopHost) .add(R.id.tab_container, mineHost, TAB_MINE).hide(mineHost) .commitNow() } if (fm.findFragmentByTag(GLOBAL_HOST) == null) { fm.beginTransaction() .setReorderingAllowed(true) .add(R.id.global_container, globalHost, GLOBAL_HOST) .commitNow() } // 确保当前tab可见 switchTab(currentTabTag, force = true) // 绑定全局导航可见性监听 bindGlobalVisibility() // 绑定底部导航栏可见性监听 bindBottomNavVisibilityForTabs() // ✅ 全局路由埋点监听(每次导航变化上报) bindGlobalRouteReporting() bottomNav.post { updateBottomNavVisibility() } } /** * 这些页面需要隐藏底部导航栏:你按需加/减 */ private fun shouldHideBottomNav(destId: Int): Boolean { return destId in setOf( R.id.searchFragment, R.id.searchResultFragment, R.id.MySkin, R.id.notificationFragment, R.id.feedbackFragment, R.id.MyKeyboard, R.id.PersonalSettings, ) } /** * ✅ 统一底栏显隐逻辑:任何地方状态变化都调用它 */ private fun updateBottomNavVisibility() { // ✅ 只要 global overlay 不在 empty,底栏必须隐藏(用 NavController 判断,别用 View.visibility) if (isGlobalVisible()) { bottomNav.visibility = View.GONE return } // 否则按“当前可见 tab 的当前目的地”判断 val destId = currentTabNavController.currentDestination?.id bottomNav.visibility = if (destId != null && shouldHideBottomNav(destId)) View.GONE else View.VISIBLE } private fun bindGlobalVisibility() { globalNavController.addOnDestinationChangedListener { _, dest, _ -> val isEmpty = dest.id == R.id.globalEmptyFragment findViewById(R.id.global_container).visibility = if (isEmpty) View.GONE else View.VISIBLE // ✅ 底栏统一走 update updateBottomNavVisibility() 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 } if (!isLoggedIn() && currentTabGraphId in protectedTabs) { switchTab(TAB_HOME, force = true) bottomNav.selectedItemId = R.id.home_graph } if (!isLoggedIn()) { pendingTabAfterLogin = null } bottomNav.post { updateBottomNavVisibility() } } } } private fun switchTab(targetTag: String, force: Boolean = false) { if (!force && targetTag == currentTabTag) return val fm = supportFragmentManager if (fm.isStateSaved) return 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() // ✅ 关键:hide/show 切 tab 不会触发 destinationChanged,所以手动刷新 bottomNav.post { updateBottomNavVisibility() } // ✅ 新增:切 tab 后补一次路由上报(不改变其它逻辑) if (!force) { currentTabNavController.currentDestination?.id?.let { destId -> reportPageView(source = "switch_tab", destId = destId) } } } /** 打开全局页(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) { e.printStackTrace() } bottomNav.post { updateBottomNavVisibility() } } /** Tab 内页面变化时刷新底栏显隐 */ private fun bindBottomNavVisibilityForTabs() { val listener = NavController.OnDestinationChangedListener { _, _, _ -> updateBottomNavVisibility() } homeHost.navController.addOnDestinationChangedListener(listener) shopHost.navController.addOnDestinationChangedListener(listener) mineHost.navController.addOnDestinationChangedListener(listener) BehaviorReporter.report( isNewUser = false, "page_id" to "home", ) } // ✅ 绑定全局路由埋点(四个 NavController) private fun bindGlobalRouteReporting() { homeHost.navController.addOnDestinationChangedListener(homeRouteListener) shopHost.navController.addOnDestinationChangedListener(shopRouteListener) mineHost.navController.addOnDestinationChangedListener(mineRouteListener) globalHost.navController.addOnDestinationChangedListener(globalRouteListener) // ✅ 删除:初始化手动上报(否则启动时会重复上报) // runCatching { // currentTabNavController.currentDestination?.id?.let { reportPageView("init_current_tab", it) } // globalNavController.currentDestination?.id?.let { reportPageView("init_global", it) } // } } private fun closeGlobalIfPossible(): Boolean { if (!isGlobalVisible()) return false val popped = globalNavController.popBackStack() // ✅ pop 后刷新一次(注意:currentDestination 可能要等一帧才更新,所以 post) bottomNav.post { updateBottomNavVisibility() } // popped = true 表示确实 pop 了;即使 popped=false 也可能已经在 empty 了 return popped || !isGlobalVisible() } /** * ✅ 改这里:不要再用 View.visibility 判断 overlay * 以 NavController 的目的地为准 */ private fun isGlobalVisible(): Boolean { return globalNavController.currentDestination?.id != R.id.globalEmptyFragment } 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) { bottomNav.post { updateBottomNavVisibility() } return } // 3) 当前tab到根了:如果不是home,切回home;否则退出 if (currentTabTag != TAB_HOME) { bottomNav.post { bottomNav.selectedItemId = R.id.home_graph } switchTab(TAB_HOME) } else { finish() } } }) } private var pendingNavigationAfterLogin: String? = null private fun handleNavigationFromIntent() { val navigateTo = intent.getStringExtra("navigate_to") if (navigateTo == "recharge_fragment") { bottomNav.post { if (!isLoggedIn()) { pendingNavigationAfterLogin = navigateTo openGlobal(R.id.loginFragment) return@post } openGlobal(R.id.rechargeFragment) } } if (navigateTo == "login_fragment") { bottomNav.post { openGlobal(R.id.loginFragment) } } } private fun isLoggedIn(): Boolean { return EncryptedSharedPreferencesUtil.contains(this, "user") } override fun onSaveInstanceState(outState: Bundle) { outState.putString("current_tab_tag", currentTabTag) super.onSaveInstanceState(outState) } // ======================= // 全局路由埋点:page_id 映射 + 上报 // ======================= private fun pageIdForDest(destId: Int): String { return when (destId) { /** ==================== 首页 Home ==================== */ R.id.homeFragment -> "home_main" // 首页-主页面 R.id.keyboardDetailFragment -> "skin_detail" // 键盘详情页 R.id.MyKeyboard -> "my_keyboard" // 键盘设置 /** ==================== 商城 Shop ==================== */ R.id.shopFragment -> "shop" // 商城首页 R.id.searchFragment -> "search" // 搜索页 R.id.searchResultFragment -> "search_result" // 搜索结果页 R.id.MySkin -> "my_skin" // 我的皮肤 /** ==================== 我的 Mine ==================== */ R.id.mineFragment -> "my" // 我的-首页 R.id.PersonalSettings -> "person_info" // 个人设置 R.id.notificationFragment -> "notice" // 消息通知 R.id.feedbackFragment -> "feedback" // 意见反馈 R.id.consumptionRecordFragment -> "consumption_record" // 消费记录 /** ==================== 登录 & 注册 ==================== */ R.id.loginFragment -> "login" // 登录页 R.id.registerFragment -> "register_email" // 注册页 R.id.registerVerifyFragment -> "register_verify_email" // 注册验证码 R.id.forgetPasswordEmailFragment -> "forgot_password_email" // 忘记密码-邮箱 R.id.forgetPasswordVerifyFragment -> "forgot_password_verify" // 忘记密码-验证码 R.id.forgetPasswordResetFragment -> "forgot_password_newpwd" // 忘记密码-重置密码 /** ==================== 充值相关 ==================== */ R.id.rechargeFragment -> "vip_pay" // 充值首页 R.id.goldCoinRechargeFragment -> "points_recharge" // 金币充值 /** ==================== 全局 / 占位 ==================== */ R.id.globalEmptyFragment -> "global_empty" // 全局占位页(兜底) /** ==================== 兜底处理 ==================== */ else -> "unknown_$destId" // 未配置的页面,方便排查遗漏 } } private fun reportPageView(source: String, destId: Int) { val pageId = pageIdForDest(destId) if (destId == R.id.globalEmptyFragment) return if (destId == R.id.loginFragment || destId == R.id.registerFragment){ BehaviorReporter.report( isNewUser = true, "page_id" to pageId, ) return } Log.d(ROUTE_TAG, "route: source=$source destId=$destId page_id=$pageId") BehaviorReporter.report( isNewUser = false, "page_id" to pageId, ) } }