feat: cascade picker common widget developed

This commit is contained in:
louis 2024-04-09 11:44:55 +08:00
parent cee31e6896
commit eabb746738
22 changed files with 502 additions and 480 deletions

View File

@ -15,6 +15,11 @@ class Api {
);
}
///
static Future<Response> getDepts() {
return DioService.dio.get(Urls.depts);
}
/// By key
static Future<Response> getSystemParamConfigByCode(String code) {
return DioService.dio.get(

View File

@ -16,4 +16,5 @@ class Urls {
static String systemParamConfig = 'system/param-config';
static String accountMenus = 'account/menus';
static String userInfo = 'system/users';
static String depts = 'system/depts';
}

View File

@ -1,25 +1,37 @@
class DeptModel {
import 'package:sk_base_mobile/util/common.util.dart';
class DeptModel extends TreeNode<DeptModel> {
DeptModel({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.name,
required this.orderNo,
this.parent,
this.children,
});
final int? id;
final int id;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? name;
final int? orderNo;
final String name;
final int orderNo;
final DeptModel? parent;
List<DeptModel>? children = [];
factory DeptModel.fromJson(Map<String, dynamic> json) {
return DeptModel(
id: json["id"],
id: json["id"] ?? 0,
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
name: json["name"],
orderNo: json["orderNo"],
name: json["name"] ?? "",
orderNo: json["orderNo"] ?? 0,
parent:
json["parent"] == null ? null : DeptModel.fromJson(json["parent"]),
children: json["children"] == null
? []
: List<DeptModel>.from(
json["children"]!.map((x) => DeptModel.fromJson(x))),
);
}
@ -29,5 +41,7 @@ class DeptModel {
"updatedAt": updatedAt?.toIso8601String(),
"name": name,
"orderNo": orderNo,
"parent": parent?.toJson(),
"children": children?.map((x) => x.toJson()).toList(),
};
}

View File

@ -1,90 +1,63 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/models/dept.model.dart';
import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart';
import 'package:sk_base_mobile/widgets/gradient_button.dart';
class DeptPicker extends StatelessWidget {
DeptPicker({super.key});
final _cascadeController = Get.put(CascadeController());
final Function(String)? onSelected;
DeptPicker({super.key, this.onSelected});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: AppTheme.nearlyWhite),
height: ScreenAdaper.height(400),
child: Column(
children: [
GradientButton(
buttonText: '确定',
onPressed: () {
if (_cascadeController.isCompleted()) {
// titles
List<String> selectedTitles =
_cascadeController.selectedTitles;
print("已选中的titles: $selectedTitles");
//
List<int> selectedIndexes =
_cascadeController.selectedIndexes;
print("已选中的序号:$selectedIndexes");
return SkCascadePicker<int, DeptModel>(
getData: getData,
initialPageData: (cascadeController) =>
cascadeController.treeData.toList(),
nextPageData: (pageCallback, currentPage, selectIndex, currentPageItem,
cascadeController) async {
if (currentPageItem.children != null) {
List<CascadeItem<int, DeptModel>>? nextPageData = currentPageItem
.children!
.map<CascadeItem<int, DeptModel>>((e) =>
CascadeItem(label: e.name, value: e.id, children: e.children))
.toList();
if (nextPageData.isNotEmpty) pageCallback(nextPageData);
}
},
onConfirm: (List<CascadeItem<int, DeptModel>> value) {
if (onSelected != null && value.isNotEmpty) {
onSelected!(value.last.label);
}
},
maxPageNum: 10,
tabTitleStyle:
TextStyle(fontSize: ScreenAdaper.height(26), color: Colors.black),
itemTitleStyle:
TextStyle(fontSize: ScreenAdaper.height(26), color: Colors.black),
selectedIcon: const Icon(Icons.check, color: AppTheme.primaryColorLight),
);
}
Item item = _cascadeController
.items[selectedIndexes[0]]
.children![selectedIndexes[1]]
.children![selectedIndexes[2]];
print("已选择item( ${item.name} )");
}
},
),
SizedBox(
height: 10,
),
Expanded(
child: CascadePicker(
initialPageData:
_cascadeController.items.map((e) => e.name!).toList(),
nextPageData: (pageCallback, currentPage, selectIndex) async {
print("当前选择: 第$currentPage页, 第$selectIndex项");
if (currentPage == 1) {
//
List<String>? nextPageData = _cascadeController
.items[selectIndex].children
?.map((e) => e.name!)
.toList();
if (nextPageData != null) pageCallback(nextPageData);
} else if (currentPage == 2) {
//
//
List<int> selectedIndexes =
_cascadeController.selectedIndexes;
// items中获取下一级页面的列表数据
List<String>? nextPageData = _cascadeController
.items[selectedIndexes[0]].children?[selectIndex].children
?.map((e) => e.name!)
.toList();
if (nextPageData != null) pageCallback(nextPageData);
}
},
controller: _cascadeController,
maxPageNum: 3,
tabTitleStyle: TextStyle(
fontSize: ScreenAdaper.height(26), color: Colors.black),
itemTitleStyle: TextStyle(
fontSize: ScreenAdaper.height(26), color: Colors.black),
selectedIcon:
Icon(Icons.check, color: AppTheme.primaryColorLight),
))
],
));
Future<List<CascadeItem<int, DeptModel>>> getData() async {
await Future.delayed(const Duration(milliseconds: 500));
try {
final res = await Api.getDepts();
if (res.data != null) {
List<CascadeItem<int, DeptModel>> result =
res.data.map<CascadeItem<int, DeptModel>>((e) {
DeptModel data = DeptModel.fromJson(e);
return CascadeItem(
label: data.name, value: data.id, children: data.children);
}).toList();
return result;
}
return [];
} catch (e) {
LoggerUtil().error(e);
return [];
}
}
}
class Item {
String? name;
String? code;
String? fatherCode;
String? remark;
List<Item>? children;
}

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/user_info.model.dart';
@ -46,6 +46,8 @@ class EditUserInfo extends StatelessWidget {
SizedBox(
height: ScreenAdaper.height(defaultPadding),
),
///
SkTextInput(
isDense: true,
textController: controller.nameEditController,
@ -54,19 +56,24 @@ class EditUserInfo extends StatelessWidget {
SizedBox(
height: ScreenAdaper.height(defaultPadding),
),
///
SkTextInput(
isDense: true,
keyboardType: TextInputType.none,
textController: TextEditingController(),
textController: controller.deptEditController,
labelText: '所属部门',
onTap: (_) async {
Get.bottomSheet(DeptPicker())
.then((value) => Get.delete<CascadeController>());
Get.bottomSheet(DeptPicker(onSelected: (String label) {
controller.deptEditController.text = label;
}));
},
),
SizedBox(
height: ScreenAdaper.height(defaultPadding),
),
///
SkTextInput(
isDense: true,
keyboardType: TextInputType.none,
@ -75,7 +82,8 @@ class EditUserInfo extends StatelessWidget {
onTap: (_) async {
Get.bottomSheet(Container(
height: ScreenAdaper.height(400),
decoration: BoxDecoration(color: AppTheme.nearlyWhite),
decoration:
const BoxDecoration(color: AppTheme.nearlyWhite),
child: Column(children: [
Container(
padding: EdgeInsets.symmetric(
@ -88,7 +96,7 @@ class EditUserInfo extends StatelessWidget {
style: TextStyle(
fontSize: ScreenAdaper.height(30)),
),
Spacer(),
const Spacer(),
Text(
'确定',
style: TextStyle(
@ -262,6 +270,7 @@ class EditUserInfoController extends GetxController {
int userId;
EditUserInfoController(this.userId);
final nameEditController = TextEditingController();
final deptEditController = TextEditingController();
final userInfo = Rxn<UserInfoModel>();
RxList selectedDepts = RxList([]);

View File

@ -130,7 +130,7 @@ class EmployeeDetail extends StatelessWidget {
text: '考勤记录',
),
Tab(
text: '工作安排',
text: '任务进度',
),
Tab(
text: '相关文件',

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/enum.dart';
import 'package:sk_base_mobile/db_helper/db_help.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/models/app_bottom_nav_item.dart';
import 'package:sk_base_mobile/screens/inventory/inventory.dart';
import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/util/device.util.dart';
import 'package:sk_base_mobile/util/snack_bar.util.dart';
import '../../constants/constants.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/user_info.model.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/index.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/index.dart';

View File

@ -4,7 +4,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/constants/enum.dart';
import 'package:sk_base_mobile/db_helper/db_help.dart';
import 'package:sk_base_mobile/models/index.dart';

View File

@ -7,7 +7,7 @@ import 'package:get/get.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:package_info/package_info.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/models/app_config.dart';

View File

@ -118,10 +118,10 @@ class DioService extends get_package.GetxService {
options.headers['model'] = StorageService.to
.getString(CacheKeys.deviceModel, isWithUser: false); //
}
if (GloablConfig.DEBUG && (options.data is! FormData)) {
LoggerUtil().info(
'[Service-dio] url: ${options.path}, params: ${jsonEncode(options.queryParameters)}, body: ${jsonEncode(options.data)}, Header:${jsonEncode(options.headers)}');
}
// if (GloablConfig.DEBUG && (options.data is! FormData)) {
// LoggerUtil().info(
// '[Service-dio] url: ${options.path}, params: ${jsonEncode(options.queryParameters)}, body: ${jsonEncode(options.data)}, Header:${jsonEncode(options.headers)}');
// }
handler.next(options);
}
@ -135,7 +135,7 @@ class DioService extends get_package.GetxService {
}
if (response.data != null && response.data is Map) {
if (response.data['code'] == 200) {
if (GloablConfig.DEBUG) LoggerUtil().info(response.data['data']);
// if (GloablConfig.DEBUG) LoggerUtil().info(response.data['data']);
response.data = response.data['data'];
//
if (response.data != null &&

View File

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:dio/dio.dart' as dio_package;
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/store/dict.store.dart';
import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/widgets/tap_to_dismiss_keyboard.dart';

View File

@ -2,7 +2,7 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/constants/dict_enum.dart';
import 'package:sk_base_mobile/models/dict_item.model.dart';
import 'package:sk_base_mobile/models/dict_type.model.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/store/auth.store.dart';
import 'package:sk_base_mobile/util/logger_util.dart';

View File

@ -26,4 +26,18 @@ class CommonUtil {
static String firstUppercase(String text) {
return '${text[0].toUpperCase()}${text.substring(1)}';
}
static List<T> flattenTree<T extends TreeNode>(List<T> children) {
List<T> result = children;
for (T child in children) {
if (child.children != null) {
result.addAll(flattenTree(child.children as List<T>));
}
}
return result;
}
}
class TreeNode<T> {
List<T>? children;
}

View File

@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart' as dio_package;
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:sk_base_mobile/apis/index.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/models/upload_result.model.dart';

View File

@ -1,59 +1,21 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/screens/hr_manage/components/dept_picker.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/gradient_button.dart';
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
///
/// 使:
/// ```dart
/// CascadePicker的page是ListView
///
///
/// final _cascadeController = CascadeController();
///
/// initialPageData:
/// nextPageData:
/// - pageCallback: CascadePicker
/// - currentPage:
/// - selectIndex:
/// controller:
/// maxPageNum:
/// selectedIcon: flutter package不能放本地资源文件images文件夹下面
///
/// Expand(
/// child: CascadePicker(
/// initialPageData: ['a', 'b', 'c', 'd'],
/// nextPageData: (pageCallback, currentPage, selectIndex) async {
/// pageCallback(['one', 'two', 'three'])
/// },
/// controller: _cascadeController,
/// maxPageNum: 4,
/// selectedIcon: Image.asset("images/ic_select_mark.png", width: 10, height: 10, color: Colors.redAccent,),
/// )
///
/// InkBox(
/// child: Container(...)
/// onTap: () {
/// ///
/// if (_cascadeController.isCompleted()) {
/// List<String> selectedTitles = _cascadeController.selectedTitles;
/// List<int> selectedIndexes = _cascadeController.selectedIndexes;
/// }
/// }
/// )
/// ```
class CascadeItem<T, Z> {
late String label;
T? value;
List<Z>? children;
CascadeItem({required this.label, this.value, this.children});
}
/// 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;
class SkCascadePicker<T, Z> extends StatelessWidget {
final SkCascadePickerController cascadeController;
final int maxPageNum;
final CascadeController controller;
final Color tabColor;
final double tabHeight;
final TextStyle tabTitleStyle;
@ -62,12 +24,15 @@ class CascadePicker extends StatefulWidget {
final Color itemColor;
final Color activeColor;
final Widget? selectedIcon;
final Function(List<CascadeItem<T, Z>>)? onConfirm;
CascadePicker(
{required this.initialPageData,
required this.nextPageData,
SkCascadePicker(
{required Future<List<CascadeItem<T, Z>>> Function() getData,
required List<CascadeItem<T, Z>> Function(SkCascadePickerController<T, Z>)
initialPageData,
required NextPageCallback<T, Z> nextPageData,
this.onConfirm,
this.maxPageNum = 3,
required this.controller,
this.tabHeight = 40,
this.activeColor = AppTheme.primaryColor,
this.tabColor = Colors.white,
@ -75,394 +40,435 @@ class CascadePicker extends StatefulWidget {
this.itemHeight = 40,
this.itemColor = Colors.white,
this.itemTitleStyle = const TextStyle(color: Colors.black, fontSize: 14),
this.selectedIcon});
this.selectedIcon})
: cascadeController = Get.put(SkCascadePickerController<T, Z>(
initialPageData: initialPageData,
nextPageData: nextPageData,
getData: getData));
@override
_CascadePickerState createState() => _CascadePickerState(this.controller);
}
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(color: AppTheme.nearlyWhite),
height: ScreenAdaper.height(400),
width: Get.width,
child: Obx(() => cascadeController.loading.value
? const LoadingIndicator(
common: true,
)
: Column(children: [
GradientButton(
buttonText: '确定',
onPressed: () {
Get.back();
if (onConfirm != null) {
onConfirm!(cascadeController.selectedTabs
.where((e) =>
e.label != SkCascadePickerController.newTabName)
.toList() as List<CascadeItem<T, Z>>);
}
class _CascadePickerState extends State<CascadePicker>
with SingleTickerProviderStateMixin {
static String _newTabName = "请选择";
final CascadeController _cascadeController;
_CascadePickerState(this._cascadeController) {
_cascadeController._setState(this);
}
late final AnimationController _controller;
late final CurvedAnimation _curvedAnimation;
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设为truePageView
/// _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));
}
// titles
// List<CascadeItem<T,Z>> selectedTitles =
// cascadeController.selectedTabs;
// print("已选中的titles: $selectedTitles");
//
// List<int> selectedIndexes =
// cascadeController.selectedIndexes;
// print("已选中的序号:$selectedIndexes");
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedBuilder(
animation: cascadeController.sliderAnimation!,
builder: (context, child) => Stack(
clipBehavior: Clip.hardEdge,
alignment: Alignment.bottomLeft,
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20)),
width: MediaQuery.of(context).size.width,
child: Obx(
() => SingleChildScrollView(
controller: cascadeController.scrollController,
scrollDirection: Axis.horizontal,
child: Row(children: _tabWidgets()),
),
),
),
const Divider(
height: 1,
),
ValueListenableBuilder<double>(
valueListenable: cascadeController.sliderFixMargin,
builder: (_, margin, __) => Positioned(
left: margin +
cascadeController.sliderAnimation!.value,
child: Container(
key: cascadeController.sliderKey,
width: cascadeController.sliderWidth,
height: 2,
decoration: BoxDecoration(
color: activeColor,
borderRadius: BorderRadius.circular(2)),
),
),
)
],
),
),
Expanded(
child: PageView.builder(
itemCount: cascadeController.pagesData.length,
controller: cascadeController.pageController,
itemBuilder: (context, index) => _pageWidget(index),
onPageChanged: (position) {
if (!cascadeController.isClickAndMoveTab) {
cascadeController.moveSlider(position,
movePage: false);
}
if (cascadeController.urrentSelectPage == position) {
cascadeController.isClickAndMoveTab = false;
}
},
),
)
],
))
])));
}
/// tab渲染完成才开始动画moveSlider
Widget _animateTab({required Widget tab}) {
return Transform.translate(
offset: Offset(
Tween<double>(begin: _isAddTabEvent ? -_animTabWidth : 0, end: 0)
.evaluate(_curvedAnimation),
Tween<double>(
begin: cascadeController.isAddTabEvent
? -cascadeController.animTabWidth
: 0,
end: 0)
.evaluate(cascadeController.curvedAnimation),
0),
child: Opacity(
///
opacity: _isAnimateTextHide ? 0 : 1,
opacity: cascadeController.isAnimateTextHide ? 0 : 1,
child: tab),
);
}
List<Widget> _tabWidgets() {
List<Widget> widgets = [];
_tabKeys.clear();
for (int i = 0; i < _pagesData.length; i++) {
cascadeController.tabKeys.clear();
for (int i = 0; i < cascadeController.pagesData.length; i++) {
GlobalKey key = GlobalKey();
_tabKeys.add(key);
cascadeController.tabKeys.add(key);
final tab = GestureDetector(
child: Container(
key: key,
height: widget.tabHeight,
color: widget.tabColor,
height: tabHeight,
color: tabColor,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth:
MediaQuery.of(context).size.width / _pagesData.length - 10),
child: Container(
// constraints: BoxConstraints(
// maxWidth: MediaQuery.of(Get.context!).size.width /
// cascadeController.pagesData.length -
// 10),
child: Text(
_selectedTabs[i],
style: _currentSelectPage == i
? widget.tabTitleStyle.copyWith(color: widget.activeColor)
: widget.tabTitleStyle,
cascadeController.selectedTabs[i].label,
style: cascadeController.urrentSelectPage == i
? tabTitleStyle.copyWith(color: activeColor)
: tabTitleStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
onTap: () {
_moveSlider(i);
cascadeController.moveSlider(i);
},
);
if (i == _pagesData.length - 1 && _selectedTabs[i] == _newTabName) {
if (i == cascadeController.pagesData.length - 1 &&
cascadeController.selectedTabs[i].label ==
SkCascadePickerController.newTabName) {
widgets.add(_animateTab(tab: tab));
_isAnimateTextHide = false;
} else {
widgets.add(tab);
}
cascadeController.isAnimateTextHide = false;
if (i < cascadeController.pagesData.length - 1) {
widgets.add(Container(
// color: Colors.red,
child: Icon(
Icons.arrow_right,
size: ScreenAdaper.height(50),
),
));
}
}
return widgets;
}
///
Widget _pageItemWidget(int index, int page, String item) {
Widget _pageItemWidget(int index, int page, CascadeItem 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: widget.selectedIcon == null
? Icon(Icons.chevron_right,
size: 15, color: widget.activeColor)
: widget.selectedIcon,
)
: SizedBox(),
Text("$item",
style: item == _selectedTabs[page]
? widget.itemTitleStyle.copyWith(color: widget.activeColor)
: widget.itemTitleStyle),
],
),
padding: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(20)),
height: itemHeight,
color: itemColor,
child: Obx(() => Row(
children: [
item == cascadeController.selectedTabs[page]
? Padding(
padding: const EdgeInsets.all(5.0),
child: selectedIcon == null
? Icon(Icons.chevron_right,
size: ScreenAdaper.height(40),
color: activeColor)
: selectedIcon,
)
: SizedBox(),
Text(item.label,
style: item == cascadeController.selectedTabs[page]
? itemTitleStyle.copyWith(color: activeColor)
: itemTitleStyle),
],
)),
),
onTap: () {
if (page == widget.maxPageNum - 1) {
if (page == maxPageNum - 1) {
///
setState(() {
_selectedTabs[page] = item;
_selectedIndexes[page] = index;
cascadeController.selectedTabs[page] = item;
cascadeController.selectedIndexes[page] = index;
///
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_moveSlider(page);
});
///
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
cascadeController.moveSlider(page);
});
} else if (_tabKeys.length >= widget.maxPageNum ||
page < _tabKeys.length - 1) {
if (index == _selectedIndexes[page]) {
} else if (cascadeController.tabKeys.length >= maxPageNum ||
page < cascadeController.tabKeys.length - 1) {
if (index == cascadeController.selectedIndexes[page]) {
/// item
_moveSlider(page + 1);
cascadeController.moveSlider(page + 1);
} else {
/// itemtab renderBox
setState(() {
_selectedTabs[page] = item;
_selectedIndexes[page] = index;
// _selectedIndexes.removeRange(page + 1, _selectedIndexes.length);
});
_loadNextPageData(page + 1, index, item, isUpdatePage: true);
cascadeController.selectedTabs[page] = item;
cascadeController.selectedIndexes[page] = index;
// selectedIndexes.removeRange(page + 1, selectedIndexes.length);
cascadeController.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);
/// page == tabKeys.length - 1 && tabKeys.length == widget.maxPageNum
cascadeController.selectedTabs[page] = item;
cascadeController.selectedIndexes[page] = index;
cascadeController.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]),
return Obx(() => ListView.builder(
padding: EdgeInsets.zero,
itemCount: cascadeController.pagesData[page].length,
itemBuilder: (context, index) => _pageItemWidget(
index, page, cascadeController.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.hardEdge,
alignment: Alignment.bottomLeft,
children: [
Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: _tabWidgets(),
),
),
ValueListenableBuilder<double>(
valueListenable: _sliderFixMargin,
builder: (_, margin, __) => Positioned(
left: margin + _sliderAnimation!.value,
child: Container(
key: _sliderKey,
width: _sliderWidth,
height: 2,
decoration: BoxDecoration(
color: widget.activeColor,
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 extends GetxController {
late List<Item> items = [];
class SkCascadePickerController<T, Z> extends GetxController
with GetSingleTickerProviderStateMixin {
RxList<CascadeItem<T, Z>> treeData = RxList([]);
final Future<List<CascadeItem<T, Z>>> Function() getData;
final List<CascadeItem<T, Z>> Function(SkCascadePickerController<T, Z>)
initialPageData;
final NextPageCallback<T, Z> nextPageData;
final RxBool loading = false.obs;
final scrollController = ScrollController();
SkCascadePickerController(
{required this.initialPageData,
required this.nextPageData,
required this.getData});
@override
void onInit() {
items = [];
for (int i = 0; i < 5; i++) {
Item item0 = Item();
item0.name = "name_$i";
item0.code = "code_$i";
List<Item> children1 = [];
for (int j = 0; j < 3; j++) {
Item item1 = Item();
item1.name = "name_${i}_$j";
item1.code = "code_${i}_$j";
List<Item> children2 = [];
for (int k = 0; k < 7; k++) {
Item item2 = Item();
item2.name = "name_${i}_${j}_$k";
item2.code = "code_${i}_${j}_$k";
// 3
item2.children = [];
children2.add(item2);
init();
animateController = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
curvedAnimation =
CurvedAnimation(parent: animateController, curve: Curves.ease)
..addStatusListener((state) {});
sliderAnimation = Tween<double>(begin: 0, end: 10).animate(curvedAnimation);
super.onInit();
}
Future<void> init() async {
loading.value = true;
treeData.assignAll(await getData());
//
selectedTabs.add(CascadeItem<T, Z>(label: newTabName));
pagesData.add(initialPageData(this));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
RenderBox tabBox =
tabKeys.first.currentContext?.findRenderObject() as RenderBox;
sliderFixMargin.value = (tabBox.size.width - sliderWidth) / 2;
});
loading.value = false;
}
bool isCompleted() =>
!selectedTabs.map((item) => item.label).contains(newTabName);
static String newTabName = "请选择";
late final AnimationController animateController;
late final CurvedAnimation curvedAnimation;
Animation? sliderAnimation;
final sliderFixMargin = ValueNotifier(0.0);
double sliderWidth = 20;
PageController pageController = PageController(initialPage: 0);
GlobalKey sliderKey = GlobalKey();
List<GlobalKey> tabKeys = [];
///
RxList<List<CascadeItem<T, Z>>> pagesData = RxList([]);
/// title集合
RxList<CascadeItem<T, Z>> selectedTabs = RxList([]);
/// item index集合
RxList<int> selectedIndexes = RxList([-1]);
/// "请选择"tab宽度tab时用到
double animTabWidth = 0;
/// tab添加事件记录"请选择"tab初始化状态
bool isAddTabEvent = false;
/// tab移动未开始'请选择'tab时隐藏文本tab在终点位置
bool isAnimateTextHide = false;
/// moveSlider重复调用
bool isClickAndMoveTab = false;
///
int urrentSelectPage = 0;
addTab(int page, int atIndex, CascadeItem<T, Z> currentPageItem) {
loadNextPageData(page, atIndex, currentPageItem);
}
loadNextPageData(int page, int atIndex, CascadeItem<T, Z> currentPageItem,
{bool isUpdatePage = false}) {
nextPageData((data) {
final nextPageDataIsEmpty = data.isEmpty;
if (!nextPageDataIsEmpty) {
///
if (isUpdatePage) {
///
pagesData[page] = data;
selectedTabs[page] = CascadeItem<T, Z>(label: 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(CascadeItem<T, Z>(label: newTabName));
selectedIndexes.add(-1);
}
// 2
item1.children = children2;
children1.add(item1);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
moveSlider(page, isAdd: true);
});
} else {
///
final currentPage = page - 1;
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);
});
}
// 1
item0.children = children1;
items.add(item0);
super.onInit();
//
Future.delayed(const Duration(milliseconds: 500), () {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}, page, atIndex, currentPageItem, this);
}
moveSlider(int page, {bool movePage = true, bool isAdd = false}) {
if (movePage && urrentSelectPage != page) {
/// tab标签
/// isClickAndMoveTab设为truePageView
/// moveSlider重复调用
isClickAndMoveTab = true;
}
isAddTabEvent = isAdd;
urrentSelectPage = page;
if (animateController.isAnimating) {
animateController.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);
animateController.value = 0;
animateController.forward();
if (movePage) {
pageController.animateToPage(page,
curve: Curves.linear, duration: Duration(milliseconds: 500));
}
}
late final _CascadePickerState _state;
_setState(_CascadePickerState state) {
_state = state;
@override
void onClose() {
animateController.dispose();
pageController.dispose();
scrollController.dispose();
super.onClose();
}
List<String> get selectedTitles => _state._selectedTabs;
List<int> get selectedIndexes => _state._selectedIndexes;
bool isCompleted() =>
!_state._selectedTabs.contains(_CascadePickerState._newTabName);
}
/// pageData:
/// currentPage: ,
/// selectIndex:
typedef void NextPageCallback<T, Z>(
Function(List<CascadeItem<T, Z>>) pageData,
int currentPage,
int selectIndex,
CascadeItem<T, Z> currentPageItem,
SkCascadePickerController<T, Z> cascadeController);