Android应用隐藏空ViewPager2触发IllegalStateException问题排查
ViewPager2 隐藏后跳转页面崩溃问题分析
问题背景
首页存在一个ViewPager2,初始状态为空。通过API接口根据用户位置返回的数据,决定需要加载的Fragment数量,之后创建Fragment并关联TabLayout。当API异常返回空数据(即nbTab=0)时,会隐藏ViewPager2并显示提示布局,此时跳转到其他页面会触发崩溃。
核心实现代码
@AndroidEntryPoint class ContentHomeFragment : LocationFragment<FragmentContentHomeBinding, ContentHomeViewModel>() { private lateinit var qaInformationAdapter: QaInformationAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) qaInformationAdapter = QaInformationAdapter(childFragmentManager, lifecycle) binding.viewPagerContent.adapter = qaInformationAdapter binding.tabDays.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { // ... 处理Tab选中逻辑,设置当前日期 viewModel.setCurrentDay(day) } override fun onTabUnselected(tab: TabLayout.Tab) { // No op } override fun onTabReselected(tab: TabLayout.Tab) { // No op } }) binding.btnToGoOut.setOnClickListener { // ... 跳转到其他页面 } viewModel.apiInformation.observe(viewLifecycleOwner) { apiInfo -> displayExistingTab(apiInfo) } } private fun displayExistingTab(infoFromApi: List<InfoFromApi>) { tabTitle.clear() // ... 处理API数据,计算nbTab数量和内容 if (nbTab == 0) { binding.viewPagerContent.visibility = View.GONE binding.layoutUnknownInfo.visibility = View.VISIBLE } else { binding.viewPagerContent.visibility = View.VISIBLE binding.layoutUnknownInfo.visibility = View.GONE } tabLayoutMediator?.detach() tabLayoutMediator = TabLayoutMediator(binding.tabDays, binding.viewPagerContent) { tab, position -> setTabInfo(tab, position, infoFromApi) } tabLayoutMediator?.attach() if (displayTodayTab) { displayTodayTab = false binding.tabDays.selectTab(binding.tabDays.getTabAt(todayTab)) } qaInformationAdapter.nbItem = nbTab } private fun setTabInfo( tab: TabLayout.Tab, position: Int, infos: List<InfoFromApi> ) { val tabView = ItemTabHomeBinding.inflate(layoutInflater) // ... 设置Tab的标题、颜色等内容 } } class QaInformationAdapter( fragmentManager: FragmentManager, lifeCycle: Lifecycle ) : FragmentStateAdapter(fragmentManager, lifeCycle) { companion object { private const val NUM_ITEM_MAX = 4 } var nbItem = NUM_ITEM_MAX @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() } override fun getItemCount() = nbItem override fun createFragment(position: Int) = QaInformationFragment.newInstance(Day.values()[position]) }
崩溃场景与错误栈
仅当API异常导致nbTab=0,隐藏ViewPager2并显示提示布局后,跳转到其他页面时崩溃,错误栈如下:
java.lang.IllegalStateException: ViewPager2 does not support direct child views at androidx.viewpager2.widget.ViewPager2.onViewAdded(ViewPager2.java:489) at android.view.ViewGroup.dispatchViewAdded(ViewGroup.java:5129) at android.view.ViewGroup.addViewInner(ViewGroup.java:5301) at android.view.ViewGroup.addView(ViewGroup.java:5076) at android.view.ViewGroup.addView(ViewGroup.java:5016) at androidx.transition.TransitionUtils.createViewBitmap(TransitionUtils.java:138) at androidx.transition.TransitionUtils.copyViewImage(TransitionUtils.java:64) at androidx.transition.Visibility.onDisappear(Visibility.java:404) at androidx.transition.Visibility.createAnimator(Visibility.java:257) at androidx.transition.Transition.createAnimators(Transition.java:744) at androidx.transition.Transition.playTransition(Transition.java:1808) at androidx.transition.TransitionManager$MultiListener.onPreDraw(TransitionManager.java:300) at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:1093) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3300) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2126) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8653) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1037) at android.view.Choreographer.doCallbacks(Choreographer.java:845) at android.view.Choreographer.doFrame(Choreographer.java:780) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1022) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:201) at android.os.Looper.loop(Looper.java:288) at android.app.ActivityThread.main(ActivityThread.java:7839) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
问题根源
这个崩溃的核心原因是页面跳转时的过渡动画逻辑与ViewPager2的内部机制冲突:
- 当ViewPager2被设置为
View.GONE后,在页面跳转的Visibility过渡动画过程中,系统会调用TransitionUtils.createViewBitmap方法尝试生成这个GONE视图的快照,用于过渡动画的视觉效果。 - 在生成快照的过程中,
TransitionUtils会创建一个临时的View容器,并尝试将目标View的内容复制到这个容器中。但当ViewPager2没有加载任何Fragment(nbTab=0)时,过渡逻辑错误地直接向ViewPager2本身添加了子View。 - 而ViewPager2的设计本身禁止直接添加子View(它内部只允许自己的RecyclerView作为子View),所以在
onViewAdded方法中直接抛出了IllegalStateException。
解决方案建议
- 方案1:包裹ViewPager2,隐藏父容器:不要直接设置ViewPager2的可见性,而是给它套一个父Layout(比如
FrameLayout),通过隐藏父容器来达到隐藏ViewPager2的效果,避免过渡动画直接操作ViewPager2。 - 方案2:跳转时禁用过渡动画:在跳转到其他页面的代码中,禁用Fragment/Activity的过渡动画,比如使用
navOptions { anim { enter = 0; exit = 0; popEnter = 0; popExit = 0 } }(如果用Navigation组件)。 - 方案3:清空适配器状态:在设置ViewPager2为GONE之前,先调用
qaInformationAdapter.nbItem = 0并确保适配器完成更新,或者暂时移除适配器,避免过渡逻辑处理空状态的ViewPager2。 - 方案4:使用提示布局覆盖:就是你已经想到的方式,保持ViewPager2可见,用提示布局覆盖在它上面,避免触发ViewPager2的GONE状态过渡逻辑。
内容的提问来源于stack exchange,提问作者Tom
相关产品推荐
相关产品推荐

