cascade_picker.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. // library cascade_picker;
  2. import 'package:flutter/material.dart';
  3. /// 级联选择器
  4. /// 使用示例:
  5. /// ```dart
  6. /// CascadePicker的page是ListView,没有约束的情况下它的高度是无限的,
  7. /// 因此需要约束高度。
  8. ///
  9. /// final _cascadeController = CascadeController();
  10. ///
  11. /// initialPageData: 第一页的数据
  12. /// nextPageData: 下一页的数据,点击当前页的选择项后调用该方法加载下一页
  13. /// - pageCallback: 用于传递下一页的数据给CascadePicker
  14. /// - currentPage: 当前是第几页
  15. /// - selectIndex: 当前选中第几项
  16. /// controller: 控制器,用于获取已选择的数据
  17. /// maxPageNum: 最大页数
  18. ///
  19. /// Expand(
  20. /// child: CascadePicker(
  21. /// initialPageData: ['a', 'b', 'c', 'd'],
  22. /// nextPageData: (pageCallback, currentPage, selectIndex) async {
  23. /// pageCallback(['one', 'two', 'three'])
  24. /// },
  25. /// controller: _cascadeController,
  26. /// maxPageNum: 4,
  27. /// )
  28. ///
  29. /// InkBox(
  30. /// child: Container(...)
  31. /// onTap: () {
  32. /// /// 判断是否完成选择
  33. /// if (_cascadeController.isCompleted()) {
  34. /// List<String> selectedTitles = _cascadeController.selectedTitles;
  35. /// List<int> selectedIndexes = _cascadeController.selectedIndexes;
  36. /// }
  37. /// }
  38. /// )
  39. /// ```
  40. /// pageData: 下一页的数据
  41. /// currentPage: 当前是第几页,
  42. /// selectIndex: 当前页选中第几项
  43. typedef void NextPageCallback(
  44. Function(List<String>) pageData, int currentPage, int selectIndex);
  45. class CascadePicker extends StatefulWidget {
  46. final List<String>? initialPageData;
  47. final NextPageCallback? nextPageData;
  48. final int maxPageNum;
  49. final CascadeController? controller;
  50. final double? tabWidth;
  51. final Color tabColor;
  52. final double tabHeight;
  53. final TextStyle tabTitleStyle;
  54. final double itemHeight;
  55. final TextStyle itemTitleStyle;
  56. final Color itemColor;
  57. CascadePicker(
  58. {this.initialPageData,
  59. this.nextPageData,
  60. this.maxPageNum = 3,
  61. this.controller,
  62. this.tabWidth,
  63. this.tabHeight = 40,
  64. this.tabColor = Colors.white,
  65. this.tabTitleStyle = const TextStyle(color: Colors.black, fontSize: 14),
  66. this.itemHeight = 40,
  67. this.itemColor = Colors.white,
  68. this.itemTitleStyle =
  69. const TextStyle(color: Colors.black, fontSize: 14)});
  70. @override
  71. _CascadePickerState createState() => _CascadePickerState(this.controller);
  72. }
  73. class _CascadePickerState extends State<CascadePicker>
  74. with SingleTickerProviderStateMixin {
  75. static String _newTabName = "请选择";
  76. final CascadeController? _cascadeController;
  77. _CascadePickerState(this._cascadeController) {
  78. _cascadeController!._setState(this);
  79. }
  80. late AnimationController _controller;
  81. late CurvedAnimation _curvedAnimation;
  82. late Animation _sliderAnimation;
  83. final _sliderFixMargin = ValueNotifier(0.0);
  84. double _sliderWidth = 20;
  85. PageController _pageController = PageController(initialPage: 0);
  86. GlobalKey _sliderKey = GlobalKey();
  87. List<GlobalKey> _tabKeys = [];
  88. /// 选择器数据集合
  89. List<List<String>?> _pagesData = [];
  90. /// 已选择的title集合
  91. List<String> _selectedTabs = [_newTabName];
  92. /// 已选择的item index集合
  93. List<int> _selectedIndexes = [-1];
  94. /// "请选择"tab宽度,添加新的tab时用到
  95. double _animTabWidth = 0;
  96. /// tab添加事件记录,用于隐藏"请选择"tab初始化状态
  97. bool _isAddTabEvent = false;
  98. /// tab移动未开始,渲染'请选择'tab时隐藏文本,这时的tab在终点位置
  99. bool _isAnimateTextHide = false;
  100. /// 防止_moveSlider重复调用
  101. bool _isClickAndMoveTab = false;
  102. /// 当前选择的页面,移动滑块前赋值
  103. int _currentSelectPage = 0;
  104. _addTab(int page, int atIndex, String currentPageItem) {
  105. _loadNextPageData(page, atIndex, currentPageItem);
  106. }
  107. _loadNextPageData(int page, int atIndex, String currentPageItem,
  108. {bool isUpdatePage = false}) {
  109. widget.nextPageData!((data) {
  110. final nextPageDataIsEmpty = data.isEmpty;
  111. if (!nextPageDataIsEmpty) {
  112. /// 下一页有数据,更新本页数据或添加新的页面
  113. setState(() {
  114. if (isUpdatePage) {
  115. /// 更新下一页
  116. _pagesData[page] = data;
  117. _selectedTabs[page] = _newTabName;
  118. _selectedIndexes[page] = -1;
  119. /// 清空下下页以后的所有页面和tab数据
  120. _pagesData.removeRange(page + 1, _pagesData.length);
  121. _selectedIndexes.removeRange(page + 1, _selectedIndexes.length);
  122. _selectedTabs.removeRange(page + 1, _selectedTabs.length);
  123. } else {
  124. /// 添加新的页面
  125. _isAnimateTextHide = true;
  126. _isAddTabEvent = true;
  127. _pagesData.add(data);
  128. _selectedTabs.add(_newTabName);
  129. _selectedIndexes.add(-1);
  130. }
  131. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  132. _moveSlider(page, isAdd: true);
  133. });
  134. });
  135. } else {
  136. /// 如果下一页数据为空,那么更新本页数据
  137. final currentPage = page - 1;
  138. setState(() {
  139. _selectedTabs[currentPage] = currentPageItem;
  140. _selectedIndexes[currentPage] = atIndex;
  141. /// 下一页数据为空,清空下一页以后的所有页面和tab数据
  142. _pagesData.removeRange(page, _pagesData.length);
  143. _selectedIndexes.removeRange(page, _selectedIndexes.length);
  144. _selectedTabs.removeRange(page, _selectedTabs.length);
  145. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  146. // 调整滑块位置
  147. _moveSlider(currentPage);
  148. });
  149. });
  150. }
  151. }, page, atIndex);
  152. }
  153. _moveSlider(int page, {bool movePage = true, bool isAdd = false}) {
  154. if (movePage && _currentSelectPage != page) {
  155. /// 上一次选择的页面和本次选择的页面不同时,移动tab标签,
  156. /// 移动时先把_isClickAndMoveTab设为true,防止滑动PageView
  157. /// 时_moveSlider重复调用。
  158. _isClickAndMoveTab = true;
  159. }
  160. _isAddTabEvent = isAdd;
  161. _currentSelectPage = page;
  162. if (_controller.isAnimating) {
  163. _controller.stop();
  164. }
  165. RenderBox slider =
  166. _sliderKey.currentContext!.findRenderObject() as RenderBox;
  167. Offset sliderPosition = slider.localToGlobal(Offset.zero);
  168. RenderBox currentTabBox =
  169. _tabKeys[page].currentContext!.findRenderObject() as RenderBox;
  170. Offset currentTabPosition = currentTabBox.localToGlobal(Offset.zero);
  171. _animTabWidth = currentTabBox.size.width;
  172. final begin = sliderPosition.dx - _sliderFixMargin.value;
  173. final end = currentTabPosition.dx +
  174. (currentTabBox.size.width - _sliderWidth) / 2 -
  175. _sliderFixMargin.value;
  176. _sliderAnimation =
  177. Tween<double>(begin: begin, end: end).animate(_curvedAnimation);
  178. _controller.value = 0;
  179. _controller.forward();
  180. if (movePage) {
  181. _pageController.animateToPage(page,
  182. curve: Curves.linear, duration: Duration(milliseconds: 500));
  183. }
  184. }
  185. /// 注意:tab渲染完成才开始动画,即调用moveSlider,这个方法会在动画执行期间多次调用
  186. Widget _animateTab({Widget? tab}) {
  187. return Transform.translate(
  188. offset: Offset(
  189. Tween<double>(begin: _isAddTabEvent ? -_animTabWidth : 0, end: 0)
  190. .evaluate(_curvedAnimation),
  191. 0),
  192. child: Opacity(
  193. /// 动画未开始前隐藏文本
  194. opacity: _isAnimateTextHide ? 0 : 1,
  195. child: tab),
  196. );
  197. }
  198. List<Widget> _tabWidgets() {
  199. List<Widget> widgets = [];
  200. _tabKeys.clear();
  201. for (int i = 0; i < _pagesData.length; i++) {
  202. GlobalKey key = GlobalKey();
  203. _tabKeys.add(key);
  204. final tab = GestureDetector(
  205. child: Container(
  206. key: key,
  207. height: widget.tabHeight,
  208. color: widget.tabColor,
  209. alignment: Alignment.center,
  210. padding: EdgeInsets.symmetric(horizontal: 15),
  211. child: ConstrainedBox(
  212. constraints: BoxConstraints(
  213. maxWidth:
  214. MediaQuery.of(context).size.width / _pagesData.length - 10),
  215. child: Text(
  216. _selectedTabs[i],
  217. style: _currentSelectPage == i
  218. ? widget.tabTitleStyle.copyWith(color: Colors.redAccent)
  219. : widget.tabTitleStyle,
  220. maxLines: 1,
  221. overflow: TextOverflow.ellipsis,
  222. ),
  223. ),
  224. ),
  225. onTap: () {
  226. _moveSlider(i);
  227. },
  228. );
  229. if (i == _pagesData.length - 1 && _selectedTabs[i] == _newTabName) {
  230. widgets.add(_animateTab(tab: tab));
  231. _isAnimateTextHide = false;
  232. } else {
  233. widgets.add(tab);
  234. }
  235. }
  236. return widgets;
  237. }
  238. /// 选择项
  239. Widget _pageItemWidget(int index, int page, String item) {
  240. return GestureDetector(
  241. child: Container(
  242. alignment: Alignment.centerLeft,
  243. padding: EdgeInsets.symmetric(horizontal: 15),
  244. height: widget.itemHeight,
  245. color: widget.itemColor,
  246. child: Row(
  247. children: [
  248. item == _selectedTabs[page]
  249. ? Padding(
  250. padding: const EdgeInsets.all(5.0),
  251. child: Image.asset(
  252. "images/ic_select_mark.png",
  253. width: 10,
  254. height: 10,
  255. color: Colors.redAccent,
  256. ),
  257. )
  258. : SizedBox(),
  259. Text("$item",
  260. style: item == _selectedTabs[page]
  261. ? widget.itemTitleStyle.copyWith(color: Colors.redAccent)
  262. : widget.itemTitleStyle),
  263. ],
  264. ),
  265. ),
  266. onTap: () {
  267. if (page == widget.maxPageNum - 1) {
  268. /// 当前页是最后一页
  269. setState(() {
  270. _selectedTabs[page] = item;
  271. _selectedIndexes[page] = index;
  272. /// 调整滑块位置
  273. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  274. _moveSlider(page);
  275. });
  276. });
  277. } else if (_tabKeys.length >= widget.maxPageNum ||
  278. page < _tabKeys.length - 1) {
  279. if (index == _selectedIndexes[page]) {
  280. /// 选择相同的item
  281. _moveSlider(page + 1);
  282. } else {
  283. /// 选择不同的item,更新tab renderBox
  284. setState(() {
  285. _selectedTabs[page] = item;
  286. _selectedIndexes[page] = index;
  287. // _selectedIndexes.removeRange(page + 1, _selectedIndexes.length);
  288. });
  289. _loadNextPageData(page + 1, index, item, isUpdatePage: true);
  290. }
  291. } else {
  292. /// 添加新tab页面
  293. /// page == _tabKeys.length - 1 && _tabKeys.length == widget.maxPageNum
  294. _selectedTabs[page] = item;
  295. _selectedIndexes[page] = index;
  296. _addTab(page + 1, index, item);
  297. }
  298. },
  299. );
  300. }
  301. Widget _pageWidget(int page) {
  302. return ListView.builder(
  303. padding: EdgeInsets.zero,
  304. itemCount: _pagesData[page]!.length,
  305. itemBuilder: (context, index) =>
  306. _pageItemWidget(index, page, _pagesData[page]![index]),
  307. // separatorBuilder: (context, index) => Divider(height: 0.3, thickness: 0.3, color: Color(0xffdddddd), indent: 15, endIndent: 15,),
  308. );
  309. }
  310. @override
  311. void initState() {
  312. super.initState();
  313. _pagesData.add(widget.initialPageData);
  314. _controller = AnimationController(
  315. duration: const Duration(milliseconds: 500), vsync: this);
  316. _curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.ease)
  317. ..addStatusListener((state) {});
  318. _sliderAnimation =
  319. Tween<double>(begin: 0, end: 10).animate(_curvedAnimation);
  320. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  321. RenderBox tabBox =
  322. _tabKeys.first.currentContext!.findRenderObject() as RenderBox;
  323. _sliderFixMargin.value = (tabBox.size.width - _sliderWidth) / 2;
  324. });
  325. }
  326. @override
  327. Widget build(BuildContext context) {
  328. return Column(
  329. crossAxisAlignment: CrossAxisAlignment.start,
  330. children: [
  331. AnimatedBuilder(
  332. animation: _sliderAnimation,
  333. builder: (context, child) => Stack(
  334. clipBehavior: Clip.none,
  335. alignment: Alignment.bottomLeft,
  336. children: [
  337. Container(
  338. width: MediaQuery.of(context).size.width,
  339. child: Row(
  340. children: _tabWidgets(),
  341. ),
  342. ),
  343. ValueListenableBuilder(
  344. valueListenable: _sliderFixMargin,
  345. builder: (_, dynamic margin, __) => Positioned(
  346. left: margin + _sliderAnimation.value,
  347. child: Container(
  348. key: _sliderKey,
  349. width: _sliderWidth,
  350. height: 2,
  351. decoration: BoxDecoration(
  352. color: Colors.redAccent,
  353. borderRadius: BorderRadius.circular(2)),
  354. ),
  355. ),
  356. )
  357. ],
  358. ),
  359. ),
  360. Expanded(
  361. child: PageView.builder(
  362. itemCount: _pagesData.length,
  363. controller: _pageController,
  364. itemBuilder: (context, index) => _pageWidget(index),
  365. onPageChanged: (position) {
  366. if (!_isClickAndMoveTab) {
  367. _moveSlider(position, movePage: false);
  368. }
  369. if (_currentSelectPage == position) {
  370. _isClickAndMoveTab = false;
  371. }
  372. },
  373. ),
  374. )
  375. ],
  376. );
  377. }
  378. }
  379. class CascadeController {
  380. late _CascadePickerState _state;
  381. _setState(_CascadePickerState state) {
  382. _state = state;
  383. }
  384. List<String> get selectedTitles => _state._selectedTabs;
  385. List<int> get selectedIndexes => _state._selectedIndexes;
  386. bool isCompleted() =>
  387. !_state._selectedTabs.contains(_CascadePickerState._newTabName);
  388. }