import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_swiper_plus/flutter_swiper_plus.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ConsolidatedPage(),
);
}
}
class ConsolidatedPage extends StatefulWidget {
const ConsolidatedPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ConsolidatedPageState();
}
class _ConsolidatedPageState extends State<ConsolidatedPage>
with TickerProviderStateMixin {
final List<String> _tabs = ["1", "2", "3", "4", "5", "6", "7"];
final List<String> _images = [
"https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF",
"https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF",
"https://t7.baidu.com/it/u=727460147,2222092211&fm=193&f=GIF",
"https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF",
"https://t7.baidu.com/it/u=727460147,2222092211&fm=193&f=GIF",
"https://t7.baidu.com/it/u=727460147,2222092211&fm=193&f=GIF",
"https://t7.baidu.com/it/u=727460147,2222092211&fm=193&f=GIF",
];
late TabController _tabController;
late final SwiperController _swiperController = SwiperController();
@override
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: _tabs.length,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverToBoxAdapter(
child: Column(
children: [
SizedBox(
height: 160,
child: IgnorePointer(
child: Swiper(
itemCount: _images.length,
itemBuilder: (BuildContext context, int index) {
return Image.network(
_images[index],
fit: BoxFit.fill,
);
},
controller: _swiperController,
),
),
),
SizedBox(
height: 97,
child: Stack(
children: [
SizedBox(
height: 90,
child: IgnorePointer(
child: Swiper(
itemCount: _images.length,
itemBuilder: (BuildContext context, int index) {
// 图片镜像倒影
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationX(pi),
child: Image.network(
_images[index],
fit: BoxFit.fill,
),
);
},
controller: _swiperController,
),
),
),
Container(
height: 90,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.white],
),
),
),
TabWidget(
tabs: _tabs,
subTabs: "Taiwan Rail",
initIndex: 0,
tabWidgetBuilder: (BuildContext ctx, int index, bool isSelected) {
// TODO: 根据是否选中客制化 tab 样式
return Text("${_tabs[index]}");
},
changeTabIndex: (index) {
_swiperController.move(index);
_tabController.animateTo(index,
duration: const Duration(microseconds: 200));
},
),
],
),
)
],
),
),
),
];
},
body: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: _tabs.map((String name) {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: buildSliverList(50),
),
],
);
},
);
}).toList(),
),
),
);
}
Widget buildSliverList(int length) {
return SliverList(delegate: SliverChildBuilderDelegate((context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text("$index"),
);
}));
}
}
typedef TabWidgetBuilder = Widget Function(
BuildContext ctx, int index, bool isSelected);
class TabWidget extends StatefulWidget {
const TabWidget(
{required this.tabs,
required this.tabWidgetBuilder,
this.initIndex = 0,
this.subTabs,
this.changeTabIndex,
this.subTabOnTap,
Key? key})
: super(key: key);
// TODO: 范型补充
final List tabs;
final String? subTabs;
final int initIndex;
final Function? changeTabIndex;
final Function? subTabOnTap;
final TabWidgetBuilder tabWidgetBuilder;
@override
State<StatefulWidget> createState() => _TabWidgetState();
}
class _TabWidgetState extends State<TabWidget> {
static const _borderRadius = 15.0;
late int _selectedIndex = 0;
late int _tabLength;
@override
void initState() {
super.initState();
_selectedIndex = widget.initIndex;
_tabLength = widget.tabs.length;
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: widget.tabs
.map((tab) => _item(index: widget.tabs.indexOf(tab)))
.toList()),
if (widget.subTabs != null)
GestureDetector(
onTap: () {
debugPrint("展开二级菜单");
widget.subTabs?.call();
},
child: Container(
color: Colors.white,
padding: const EdgeInsets.only(top: 10, left: 16, right: 16),
child: Row(
children: [
Flexible(
child: Text(
widget.subTabs!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Image.asset(
"./assets/ic_launcher_adapter.png",
width: 12,
height: 12,
fit: BoxFit.fill,
)
],
),
),
)
],
),
);
}
Widget _item({required int index}) {
bool isSelected = index == _selectedIndex;
// 除去左右边距后剩余空间
double leftSize = (MediaQuery.of(context).size.width - 40);
// 选中项最少占剩余空间的 30%
double selectedMinWidth = leftSize * 0.3;
// 选中项最大空间
double selectedMaxWidth = max(selectedMinWidth, leftSize / _tabLength);
// 其他非选中项则平分剩余空间
double otherMaxWidth = (leftSize - selectedMaxWidth) / (_tabLength - 1);
BorderRadius borderRadius = BorderRadius.zero;
BorderRadius bgBorderRadius = BorderRadius.zero;
if (index == _selectedIndex - 1) {
borderRadius =
const BorderRadius.only(bottomRight: Radius.circular(_borderRadius));
} else if (index == _selectedIndex + 1) {
borderRadius =
const BorderRadius.only(bottomLeft: Radius.circular(_borderRadius));
}
if (index == 0) {
bgBorderRadius =
const BorderRadius.only(topLeft: Radius.circular(_borderRadius));
borderRadius = borderRadius +
const BorderRadius.only(topLeft: Radius.circular(_borderRadius));
} else if (index == _tabLength - 1) {
bgBorderRadius =
const BorderRadius.only(topRight: Radius.circular(_borderRadius));
borderRadius = borderRadius +
const BorderRadius.only(topRight: Radius.circular(_borderRadius));
}
bgBorderRadius = isSelected
? const BorderRadius.vertical(top: Radius.circular(_borderRadius))
: bgBorderRadius;
borderRadius = isSelected
? const BorderRadius.vertical(top: Radius.circular(_borderRadius))
: borderRadius;
return GestureDetector(
onTap: () {
setState(() {
_selectedIndex = index;
});
// 切换一级菜单
widget.changeTabIndex?.call(index);
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: bgBorderRadius,
),
child: ClipRRect(
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: isSelected ? selectedMinWidth : 0,
maxWidth: isSelected ? selectedMaxWidth : otherMaxWidth),
child: Container(
color: isSelected ? Colors.white : const Color(0xCCCCCCCC),
height: isSelected ? 70 : 60,
alignment: Alignment.center,
child: widget.tabWidgetBuilder(
context, index, index == _selectedIndex),
),
),
),
),
);
}
}
轮播图使用的 flutter_swiper_plus,使用 IgnorePointer 忽略轮播图的手势事件,使用 controller 来控制图片& tabBarView 的滚动,制造页面整体切换的假象。