如何适配Flutter TabBarView子组件的高度?
动态调整TabBarView高度适配异步加载组件的问题
我的TabBarView包含3个高度不同的子组件,其中第二个是需要异步获取数据的Query组件(加载时显示CircularPercentIndicator,加载完成后渲染HospitalsByTreatments)。尝试使用AutoScaleTabBarView后发现,它仅在切换标签时调整高度,导致Query组件数据加载完成后无法正常显示内容。相关代码如下:
TabBarView( controller: tabController, children: [ MarkdownBody( data: currentTreatment.treatmentDescription, styleSheet: MarkdownStyleSheet( p: TextStyle(fontSize: 16 * w), h4: TextStyle( fontSize: 19 * w, fontWeight: FontWeight.w800, color: Colors.grey.shade700), ), ), Query( options: QueryOptions( document: gql(hospitalsGraphQL)), builder: (QueryResult result, {fetchMore, refetch}) { if (result.hasException) { return Text( result.exception.toString()); } if (result.isLoading) { return Center( child: CircularPercentIndicator( curve: Curves.decelerate, lineWidth: 12 * h, progressColor: Colors.green.shade400, backgroundColor: Colors.green.shade200, radius: 50 * h, percent: 1, circularStrokeCap: CircularStrokeCap.round, animation: true, center: Image.asset( "lib/images/Medicte_cropped.png", height: 60 * h, ), animationDuration: 1000, ), ); } final hospitalList = result.data?['hospitals']; return HospitalsByTreatments( currentTreatment: currentTreatment, hospitalList: hospitalList, ); }), SizedBox( height: heightContainer, child: Column( children: [ Expanded( child: PageView.builder( controller: pageController, itemCount: listLengthCalculator( currentTreatment .medicte_hotels.length), itemBuilder: (BuildContext context, int index) { return Padding( padding: EdgeInsets.symmetric( horizontal: 6 * w), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular( 15), child: Container( padding: EdgeInsets.only( right: 12 * w, left: 12 * w, top: 12 * h), color: Colors.orange, width: 400 * w, height: (heightContainer - 35) / 2, child: Column( children: [ CircleAvatar( radius: 65 * h, backgroundImage: NetworkImage( "https://admin.medicte.ca${currentTreatment.medicte_hotels[index * 2]["hotelPicture"]["formats"]["small"]["url"]}", ), ), SizedBox( height: 10 * h, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment .center, children: [ Text( currentTreatment.medicte_hotels[index * 2] [ "hotelTitle"], style: TextStyle( fontWeight: FontWeight .bold, fontSize: 18 * w)), SizedBox( height: 10 * h), Text( currentTreatment .medicte_hotels[ index * 2]["hotelDescription"], style: TextStyle( fontSize: 13 * w), ), ], ), ), ], ), ), ), SizedBox( height: 10 * h, ), if (index * 2 + 1 + 1 < currentTreatment .medicte_hotels .length) ClipRRect( borderRadius: BorderRadius .circular(15), child: Container( padding: EdgeInsets.all( 12 * h), color: Colors.orange, width: 400 * w, height: (heightContainer - 35) / 2, child: Column( children: [ CircleAvatar( radius: 65 * h, backgroundImage: NetworkImage( "https://admin.medicte.ca${currentTreatment.medicte_hotels[index * 2 + 1]["hotelPicture"]["formats"]["small"]["url"]}", ), ), SizedBox( height: 10 * h, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment .center, children: [ Text( currentTreatment.medicte_hotels[index * 2 + 1] [ "hotelTitle"], style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18 * w)), SizedBox( height: 10 * h), Text( currentTreatment .medicte_hotels[index * 2 + 1]["hotelDescription"], overflow: TextOverflow .fade, style: TextStyle( fontSize: 13 * w), ), ], ), ), ], ), ), ), ], ), ); }), ), SmoothPageIndicator( controller: pageController, count: listLengthCalculator( currentTreatment .medicte_hotels.length), effect: ScrollingDotsEffect( activeStrokeWidth: 2.6, activeDotScale: 1.3, maxVisibleDots: 5, radius: 8 * h, spacing: 10 * w, dotHeight: 12 * w, dotWidth: 12 * w, )), ], ), ), ], ),
解决方案
1. 手动监听Tab切换+异步组件高度更新
给每个Tab子组件绑定GlobalKey,监听Tab切换事件计算当前页面高度,同时在Query组件数据加载完成后主动触发高度更新:
// 定义全局Key和高度变量 final GlobalKey _tab1Key = GlobalKey(); final GlobalKey _tab2Key = GlobalKey(); final GlobalKey _tab3Key = GlobalKey(); double _tabBarViewHeight = 0; @override void initState() { super.initState(); // 监听Tab切换事件 tabController.addListener(_updateTabBarViewHeight); } // 计算当前活跃Tab的高度 void _updateTabBarViewHeight() { WidgetsBinding.instance.addPostFrameCallback((_) { RenderBox? renderBox; switch(tabController.index) { case 0: renderBox = _tab1Key.currentContext?.findRenderObject() as RenderBox?; break; case 1: renderBox = _tab2Key.currentContext?.findRenderObject() as RenderBox?; break; case 2: renderBox = _tab3Key.currentContext?.findRenderObject() as RenderBox?; break; } if(renderBox != null) { setState(() { _tabBarViewHeight = renderBox.size.height; }); } }); } // 改造Query组件的builder builder: (QueryResult result, {fetchMore, refetch}) { if (result.hasException) { return Text(result.exception.toString()); } if (result.isLoading) { return Center(/* 原有加载指示器 */); } // 数据加载完成后,延迟触发高度更新 WidgetsBinding.instance.addPostFrameCallback((_) { _updateTabBarViewHeight(); }); final hospitalList = result.data?['hospitals']; return HospitalsByTreatments( key: _tab2Key, // 绑定Key currentTreatment: currentTreatment, hospitalList: hospitalList, ); } // 用SizedBox包裹TabBarView SizedBox( height: _tabBarViewHeight, child: TabBarView( controller: tabController, children: [ MarkdownBody(key: _tab1Key, /* 原有配置 */), Query(/* 原有配置 */), SizedBox(key: _tab3Key, /* 原有配置 */), ], ), )
2. 封装自适应TabBarView组件
封装一个能自动监听子组件高度变化的TabBarView,适配异步组件的重绘:
class AdaptiveTabBarView extends StatefulWidget { final TabController controller; final List<Widget> children; const AdaptiveTabBarView({ super.key, required this.controller, required this.children, }); @override State<AdaptiveTabBarView> createState() => _AdaptiveTabBarViewState(); } class _AdaptiveTabBarViewState extends State<AdaptiveTabBarView> { final List<GlobalKey> _keys = []; double _currentHeight = 0; @override void initState() { super.initState(); _keys.addAll(List.generate(widget.children.length, (_) => GlobalKey())); widget.controller.addListener(_updateHeight); } void _updateHeight() { WidgetsBinding.instance.addPostFrameCallback((_) { final renderBox = _keys[widget.controller.index].currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { setState(() { _currentHeight = renderBox.size.height; }); } }); } @override Widget build(BuildContext context) { return SizedBox( height: _currentHeight, child: TabBarView( controller: widget.controller, children: widget.children.asMap().entries.map((entry) { return KeyedSubtree( key: _keys[entry.key], child: entry.value, ); }).toList(), ), ); } } // 使用方式 AdaptiveTabBarView( controller: tabController, children: [/* 你的三个子组件 */], )
3. 给Query组件设置最小高度兜底
如果不需要高度完全贴合,给Query的加载状态和内容状态设置合理的最小高度,避免高度突变:
Query( options: QueryOptions(document: gql(hospitalsGraphQL)), builder: (result, {fetchMore, refetch}) { if (result.hasException) return Text(result.exception.toString()); if (result.isLoading) { return SizedBox( height: 300, // 设置加载状态的固定高度 child: Center(/* 原有加载指示器 */), ); } return SizedBox( minHeight: 300, // 给内容设置最小高度 child: HospitalsByTreatments(/* ... */), ); }, )
内容的提问来源于stack exchange,提问作者mctracy
相关产品推荐
相关产品推荐

