先上图,无图无真相,本文简单实现,仅供参考。如有问题,欢迎骚扰。
结构上很简单,上边是TabBar,下面使用CustomScrollView。实现原理是将两个控件的滑动关联起来,切换TabBar的时候去设置CustomScrollView的offser,当CustomScrollView的offser滑动到某个区间时准确的设置TabBar的选中位置。
实际需求中宝贝、评价、详情、推荐4个模块数据是后台获取的,也就是4个模块对应的Widget的高度是动态的。那就需要动态获取widget高度
一、那怎么动态获取Widget的height?
怕大家不知道,这里简单说一下Flutter绘制的相关知识。
Widget并不是真正的渲染对象,是Element的配置描述,Widget创建了Element,而后创建RenderObject关联到Element内部的renderObject对象上,最后Flutter通过RenderObject来布局和绘制。换句话说Element持有Widget和RenderObject。
RenderObjectElement.dart 源码
RenderObject是抽象类(abstract),RenderBox是RenderObject的具体实现,它是在继承了RenderObject基础布局和绘制功能上,实现了“笛卡尔坐标系”,保存大小和位置等信息。
也就是我们如果获取到当前Widget生成的RenderBox,就可以拿到布局的大小和位置信息。
二、如何获取到当前Widget生成的RenderBox?
前面讲到Widget生成Element,Element持有Widget和RenderObject,那么通过element就可以获取到RenderObject了,这个道理就很简单了。
在引入两个知识点:
1、BuildContext
介绍BuildContext之前,先介绍一下它的子类。上图:
关键看implements后
是的没错,Element implements BuildContext,BuildContext中提供了findRenderObject()方法,并返回RenderObject对象。
RenderObject renderObject = buildContext.findRenderObject();
if(renderObject is RenderBox){
print(renderObject.size.width);//输出widget的宽度
print(renderObject.size.height);//输出widget的高度
}
通过上面代码可以获取到当前Widget的size。如果想获取当前Widget内部某一个子widget的RenderBox对象呢,这里需要用到GlobalKey。
2、GlobalKey。
简单粗暴上代码,给需要获取RenderBox的Widget设置key。
List<GlobalKey> keys = [];
@overridevoid initState() {
super.initState();
keys.add(GlobalKey());
keys.add(GlobalKey());
keys.add(GlobalKey());
keys.add(GlobalKey());
}
CustomScrollView(
controller: _controller,
scrollDirection: Axis.vertical,
slivers: <Widget>[
SliverToBoxAdapter(
key: keys[0],
child:Container()
),
SliverToBoxAdapter(
key: keys[1],
child:Container()
),
......
]
)
代码只贴大致逻辑,文末有源码链接。
通过key可以获取当前context,
RenderObject renderObject = key.currentContext.findRenderObject();
if(renderObject is RenderBox){
print(renderObject.size.width);//输出widget的宽度
print(renderObject.size.height);//输出widget的高度
}
到这里一些基本基础就说完了,下面开始实现淘宝详情页吧!大概用了几种布局代替具体的UI样式,具体需求具体样式还得自己调,请原谅我偷懒了。
定义Tabbar的TabController和定义CustomScrollView的ScrollController,
ScrollController _controller;
TabController _tabController;
int childCount = 25;//假设详情的List有25条数据
//宝贝、评价、详情、推荐4个模块分别设置一个key
List keys = [];
@overridevoid initState() {
super.initState();
keys.add(GlobalKey());
keys.add(GlobalKey());
keys.add(GlobalKey());
keys.add(GlobalKey());
_controller = ScrollController();
_tabController = TabController(length: 4, vsync: this);
//监听ScrollController的滑动,
_controller.addListener(() {
//选择
if(key0height == null ){
key0height = _getHeiget(0);
print("key0height = $key0height");
}
if(key1height == null ){
key1height = key0height + _getHeiget(1);
print("key1height = $key1height");
}
if(key2height == null ){
key2height = key1height + _getHeiget(2);
print("key2height = $key2height");
}
if( _controller.offset < key0height){
_tabController.animateTo(0);
}else if(_controller.offset >= key0height && _controller.offset < key1height){
_tabController.animateTo(1);
}else if(_controller.offset >= key1height && _controller.offset < key2height){
_tabController.animateTo(2);
}else{
_tabController.animateTo(3);
}
});
}
接下来是页面具体布局(build方法里的代码)
@override
Widget build(BuildContext buildContext) {
return Scaffold(
appBar: AppBar( title: Text('data'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(48),
child: Container(
height: 48,
alignment: Alignment.center,
width: MediaQuery.of(context).size.width,
color: Colors.white,
child: TabBar(
isScrollable: true,
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Color.fromARGB(255, 111, 111, 111),
indicatorColor: Colors.black,
tabs: [
Container( height: 48, alignment: Alignment.center, child: Text('宝贝')),
Container( height: 48, alignment: Alignment.center, child: Text('评价')),
Container( height: 48, alignment: Alignment.center, child: Text('详情')),
Container( height: 48, alignment: Alignment.center, child: Text(推荐')),
],
onTap: (index) {
//通关循环计算
offset double height = 0;
for(int i = 0;i
height += _getHeiget(i);
}
_controller?.animateTo(height.toDouble(), duration: Duration(milliseconds: 200), curve: Curves.linear);
},
),
)),
),
body: CustomScrollView(
controller: _controller,
scrollDirection: Axis.vertical,
slivers: [
//宝贝
SliverToBoxAdapter(
key: keys[0],
child: Container(
color: Colors.red, alignment: Alignment.center,
child: Image.network('https://pic.netbian.com/uploads/allimg/180826/113958-1535254798fc1c.jpg' ,fit: BoxFit.cover)
)
),
//评价
SliverToBoxAdapter(
key: keys[1],
child: Container(
height: 200, color: Colors.green, alignment: Alignment.center,
child: Text('宝贝评价',style: TextStyle(color: Colors.white)),
),
),
//详情
SliverFixedExtentList(
key: keys[2],
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return Container(
alignment: Alignment.center,
child: Text( '详情图片$index', textAlign: TextAlign.center));
},
childCount: 20),
itemExtent: 50),
//推荐
SliverFillRemaining(
key: keys[3],
child: Container(
height: 300, color: Colors.blue, alignment: Alignment.center,
child: Text('宝贝推荐',style: TextStyle(color: Colors.white)),
),
),
],
),
);
}
@overridevoid
dispose() {
//为了避免内存泄露,需要调用
_controller.dispose _controller?.dispose();
_tabController?.dispose();
super.dispose();
}
//获取key对应的widget的高度
double _getHeiget(int i) {
double height = 0;
RenderObject renderObject= keys[i]?.currentContext?.findRenderObject();
if(renderObject is RenderSliverToBoxAdapter){
height = renderObject?.child?.size?.height??0.0;
}else if(renderObject is RenderSliverFixedExtentList){
height = childCount*renderObject.itemExtent;
}else{ //如果用到其他RenderObject的子类这里需要加逻辑,
print('==============');
}
return height;
}