123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- // library cascade_picker;
- import 'package:flutter/material.dart';
- /// 级联选择器
- /// 使用示例:
- /// ```dart
- /// CascadePicker的page是ListView,没有约束的情况下它的高度是无限的,
- /// 因此需要约束高度。
- ///
- /// final _cascadeController = CascadeController();
- ///
- /// initialPageData: 第一页的数据
- /// nextPageData: 下一页的数据,点击当前页的选择项后调用该方法加载下一页
- /// - pageCallback: 用于传递下一页的数据给CascadePicker
- /// - currentPage: 当前是第几页
- /// - selectIndex: 当前选中第几项
- /// controller: 控制器,用于获取已选择的数据
- /// maxPageNum: 最大页数
- ///
- /// Expand(
- /// child: CascadePicker(
- /// initialPageData: ['a', 'b', 'c', 'd'],
- /// nextPageData: (pageCallback, currentPage, selectIndex) async {
- /// pageCallback(['one', 'two', 'three'])
- /// },
- /// controller: _cascadeController,
- /// maxPageNum: 4,
- /// )
- ///
- /// InkBox(
- /// child: Container(...)
- /// onTap: () {
- /// /// 判断是否完成选择
- /// if (_cascadeController.isCompleted()) {
- /// List<String> selectedTitles = _cascadeController.selectedTitles;
- /// List<int> selectedIndexes = _cascadeController.selectedIndexes;
- /// }
- /// }
- /// )
- /// ```
- /// pageData: 下一页的数据
- /// currentPage: 当前是第几页,
- /// selectIndex: 当前页选中第几项
- typedef void NextPageCallback(
- Function(List<String>) pageData, int currentPage, int selectIndex);
- class CascadePicker extends StatefulWidget {
- final List<String>? initialPageData;
- final NextPageCallback? nextPageData;
- final int maxPageNum;
- final CascadeController? controller;
- final double? tabWidth;
- final Color tabColor;
- final double tabHeight;
- final TextStyle tabTitleStyle;
- final double itemHeight;
- final TextStyle itemTitleStyle;
- final Color itemColor;
- CascadePicker(
- {this.initialPageData,
- this.nextPageData,
- this.maxPageNum = 3,
- this.controller,
- this.tabWidth,
- this.tabHeight = 40,
- this.tabColor = Colors.white,
- this.tabTitleStyle = const TextStyle(color: Colors.black, fontSize: 14),
- this.itemHeight = 40,
- this.itemColor = Colors.white,
- this.itemTitleStyle =
- const TextStyle(color: Colors.black, fontSize: 14)});
- @override
- _CascadePickerState createState() => _CascadePickerState(this.controller);
- }
- class _CascadePickerState extends State<CascadePicker>
- with SingleTickerProviderStateMixin {
- static String _newTabName = "请选择";
- final CascadeController? _cascadeController;
- _CascadePickerState(this._cascadeController) {
- _cascadeController!._setState(this);
- }
- late AnimationController _controller;
- late CurvedAnimation _curvedAnimation;
- late Animation _sliderAnimation;
- final _sliderFixMargin = ValueNotifier(0.0);
- double _sliderWidth = 20;
- PageController _pageController = PageController(initialPage: 0);
- GlobalKey _sliderKey = GlobalKey();
- List<GlobalKey> _tabKeys = [];
- /// 选择器数据集合
- List<List<String>?> _pagesData = [];
- /// 已选择的title集合
- List<String> _selectedTabs = [_newTabName];
- /// 已选择的item index集合
- List<int> _selectedIndexes = [-1];
- /// "请选择"tab宽度,添加新的tab时用到
- double _animTabWidth = 0;
- /// tab添加事件记录,用于隐藏"请选择"tab初始化状态
- bool _isAddTabEvent = false;
- /// tab移动未开始,渲染'请选择'tab时隐藏文本,这时的tab在终点位置
- bool _isAnimateTextHide = false;
- /// 防止_moveSlider重复调用
- bool _isClickAndMoveTab = false;
- /// 当前选择的页面,移动滑块前赋值
- int _currentSelectPage = 0;
- _addTab(int page, int atIndex, String currentPageItem) {
- _loadNextPageData(page, atIndex, currentPageItem);
- }
- _loadNextPageData(int page, int atIndex, String currentPageItem,
- {bool isUpdatePage = false}) {
- widget.nextPageData!((data) {
- final nextPageDataIsEmpty = data.isEmpty;
- if (!nextPageDataIsEmpty) {
- /// 下一页有数据,更新本页数据或添加新的页面
- setState(() {
- if (isUpdatePage) {
- /// 更新下一页
- _pagesData[page] = data;
- _selectedTabs[page] = _newTabName;
- _selectedIndexes[page] = -1;
- /// 清空下下页以后的所有页面和tab数据
- _pagesData.removeRange(page + 1, _pagesData.length);
- _selectedIndexes.removeRange(page + 1, _selectedIndexes.length);
- _selectedTabs.removeRange(page + 1, _selectedTabs.length);
- } else {
- /// 添加新的页面
- _isAnimateTextHide = true;
- _isAddTabEvent = true;
- _pagesData.add(data);
- _selectedTabs.add(_newTabName);
- _selectedIndexes.add(-1);
- }
- WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
- _moveSlider(page, isAdd: true);
- });
- });
- } else {
- /// 如果下一页数据为空,那么更新本页数据
- final currentPage = page - 1;
- setState(() {
- _selectedTabs[currentPage] = currentPageItem;
- _selectedIndexes[currentPage] = atIndex;
- /// 下一页数据为空,清空下一页以后的所有页面和tab数据
- _pagesData.removeRange(page, _pagesData.length);
- _selectedIndexes.removeRange(page, _selectedIndexes.length);
- _selectedTabs.removeRange(page, _selectedTabs.length);
- WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
- // 调整滑块位置
- _moveSlider(currentPage);
- });
- });
- }
- }, page, atIndex);
- }
- _moveSlider(int page, {bool movePage = true, bool isAdd = false}) {
- if (movePage && _currentSelectPage != page) {
- /// 上一次选择的页面和本次选择的页面不同时,移动tab标签,
- /// 移动时先把_isClickAndMoveTab设为true,防止滑动PageView
- /// 时_moveSlider重复调用。
- _isClickAndMoveTab = true;
- }
- _isAddTabEvent = isAdd;
- _currentSelectPage = page;
- if (_controller.isAnimating) {
- _controller.stop();
- }
- RenderBox slider =
- _sliderKey.currentContext!.findRenderObject() as RenderBox;
- Offset sliderPosition = slider.localToGlobal(Offset.zero);
- RenderBox currentTabBox =
- _tabKeys[page].currentContext!.findRenderObject() as RenderBox;
- Offset currentTabPosition = currentTabBox.localToGlobal(Offset.zero);
- _animTabWidth = currentTabBox.size.width;
- final begin = sliderPosition.dx - _sliderFixMargin.value;
- final end = currentTabPosition.dx +
- (currentTabBox.size.width - _sliderWidth) / 2 -
- _sliderFixMargin.value;
- _sliderAnimation =
- Tween<double>(begin: begin, end: end).animate(_curvedAnimation);
- _controller.value = 0;
- _controller.forward();
- if (movePage) {
- _pageController.animateToPage(page,
- curve: Curves.linear, duration: Duration(milliseconds: 500));
- }
- }
- /// 注意:tab渲染完成才开始动画,即调用moveSlider,这个方法会在动画执行期间多次调用
- Widget _animateTab({Widget? tab}) {
- return Transform.translate(
- offset: Offset(
- Tween<double>(begin: _isAddTabEvent ? -_animTabWidth : 0, end: 0)
- .evaluate(_curvedAnimation),
- 0),
- child: Opacity(
- /// 动画未开始前隐藏文本
- opacity: _isAnimateTextHide ? 0 : 1,
- child: tab),
- );
- }
- List<Widget> _tabWidgets() {
- List<Widget> widgets = [];
- _tabKeys.clear();
- for (int i = 0; i < _pagesData.length; i++) {
- GlobalKey key = GlobalKey();
- _tabKeys.add(key);
- final tab = GestureDetector(
- child: Container(
- key: key,
- height: widget.tabHeight,
- color: widget.tabColor,
- alignment: Alignment.center,
- padding: EdgeInsets.symmetric(horizontal: 15),
- child: ConstrainedBox(
- constraints: BoxConstraints(
- maxWidth:
- MediaQuery.of(context).size.width / _pagesData.length - 10),
- child: Text(
- _selectedTabs[i],
- style: _currentSelectPage == i
- ? widget.tabTitleStyle.copyWith(color: Colors.redAccent)
- : widget.tabTitleStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
- onTap: () {
- _moveSlider(i);
- },
- );
- if (i == _pagesData.length - 1 && _selectedTabs[i] == _newTabName) {
- widgets.add(_animateTab(tab: tab));
- _isAnimateTextHide = false;
- } else {
- widgets.add(tab);
- }
- }
- return widgets;
- }
- /// 选择项
- Widget _pageItemWidget(int index, int page, String item) {
- return GestureDetector(
- child: Container(
- alignment: Alignment.centerLeft,
- padding: EdgeInsets.symmetric(horizontal: 15),
- height: widget.itemHeight,
- color: widget.itemColor,
- child: Row(
- children: [
- item == _selectedTabs[page]
- ? Padding(
- padding: const EdgeInsets.all(5.0),
- child: Image.asset(
- "images/ic_select_mark.png",
- width: 10,
- height: 10,
- color: Colors.redAccent,
- ),
- )
- : SizedBox(),
- Text("$item",
- style: item == _selectedTabs[page]
- ? widget.itemTitleStyle.copyWith(color: Colors.redAccent)
- : widget.itemTitleStyle),
- ],
- ),
- ),
- onTap: () {
- if (page == widget.maxPageNum - 1) {
- /// 当前页是最后一页
- setState(() {
- _selectedTabs[page] = item;
- _selectedIndexes[page] = index;
- /// 调整滑块位置
- WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
- _moveSlider(page);
- });
- });
- } else if (_tabKeys.length >= widget.maxPageNum ||
- page < _tabKeys.length - 1) {
- if (index == _selectedIndexes[page]) {
- /// 选择相同的item
- _moveSlider(page + 1);
- } else {
- /// 选择不同的item,更新tab renderBox
- setState(() {
- _selectedTabs[page] = item;
- _selectedIndexes[page] = index;
- // _selectedIndexes.removeRange(page + 1, _selectedIndexes.length);
- });
- _loadNextPageData(page + 1, index, item, isUpdatePage: true);
- }
- } else {
- /// 添加新tab页面
- /// page == _tabKeys.length - 1 && _tabKeys.length == widget.maxPageNum
- _selectedTabs[page] = item;
- _selectedIndexes[page] = index;
- _addTab(page + 1, index, item);
- }
- },
- );
- }
- Widget _pageWidget(int page) {
- return ListView.builder(
- padding: EdgeInsets.zero,
- itemCount: _pagesData[page]!.length,
- itemBuilder: (context, index) =>
- _pageItemWidget(index, page, _pagesData[page]![index]),
- // separatorBuilder: (context, index) => Divider(height: 0.3, thickness: 0.3, color: Color(0xffdddddd), indent: 15, endIndent: 15,),
- );
- }
- @override
- void initState() {
- super.initState();
- _pagesData.add(widget.initialPageData);
- _controller = AnimationController(
- duration: const Duration(milliseconds: 500), vsync: this);
- _curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.ease)
- ..addStatusListener((state) {});
- _sliderAnimation =
- Tween<double>(begin: 0, end: 10).animate(_curvedAnimation);
- WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
- RenderBox tabBox =
- _tabKeys.first.currentContext!.findRenderObject() as RenderBox;
- _sliderFixMargin.value = (tabBox.size.width - _sliderWidth) / 2;
- });
- }
- @override
- Widget build(BuildContext context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- AnimatedBuilder(
- animation: _sliderAnimation,
- builder: (context, child) => Stack(
- clipBehavior: Clip.none,
- alignment: Alignment.bottomLeft,
- children: [
- Container(
- width: MediaQuery.of(context).size.width,
- child: Row(
- children: _tabWidgets(),
- ),
- ),
- ValueListenableBuilder(
- valueListenable: _sliderFixMargin,
- builder: (_, dynamic margin, __) => Positioned(
- left: margin + _sliderAnimation.value,
- child: Container(
- key: _sliderKey,
- width: _sliderWidth,
- height: 2,
- decoration: BoxDecoration(
- color: Colors.redAccent,
- borderRadius: BorderRadius.circular(2)),
- ),
- ),
- )
- ],
- ),
- ),
- Expanded(
- child: PageView.builder(
- itemCount: _pagesData.length,
- controller: _pageController,
- itemBuilder: (context, index) => _pageWidget(index),
- onPageChanged: (position) {
- if (!_isClickAndMoveTab) {
- _moveSlider(position, movePage: false);
- }
- if (_currentSelectPage == position) {
- _isClickAndMoveTab = false;
- }
- },
- ),
- )
- ],
- );
- }
- }
- class CascadeController {
- late _CascadePickerState _state;
- _setState(_CascadePickerState state) {
- _state = state;
- }
- List<String> get selectedTitles => _state._selectedTabs;
- List<int> get selectedIndexes => _state._selectedIndexes;
- bool isCompleted() =>
- !_state._selectedTabs.contains(_CascadePickerState._newTabName);
- }
|