feat: cascade picker common widget developed
This commit is contained in:
parent
cee31e6896
commit
eabb746738
|
@ -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(
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
Item item = _cascadeController
|
||||
.items[selectedIndexes[0]]
|
||||
.children![selectedIndexes[1]]
|
||||
.children![selectedIndexes[2]];
|
||||
print("已选择item( ${item.name} )");
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
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);
|
||||
onConfirm: (List<CascadeItem<int, DeptModel>> value) {
|
||||
if (onSelected != null && value.isNotEmpty) {
|
||||
onSelected!(value.last.label);
|
||||
}
|
||||
},
|
||||
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),
|
||||
))
|
||||
],
|
||||
));
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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([]);
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ class EmployeeDetail extends StatelessWidget {
|
|||
text: '考勤记录',
|
||||
),
|
||||
Tab(
|
||||
text: '工作安排',
|
||||
text: '任务进度',
|
||||
),
|
||||
Tab(
|
||||
text: '相关文件',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,323 +40,80 @@ class CascadePicker extends StatefulWidget {
|
|||
this.itemHeight = 40,
|
||||
this.itemColor = Colors.white,
|
||||
this.itemTitleStyle = const TextStyle(color: Colors.black, fontSize: 14),
|
||||
this.selectedIcon});
|
||||
|
||||
@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 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设为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({required 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: widget.activeColor)
|
||||
: 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: 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
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;
|
||||
});
|
||||
}
|
||||
this.selectedIcon})
|
||||
: cascadeController = Get.put(SkCascadePickerController<T, Z>(
|
||||
initialPageData: initialPageData,
|
||||
nextPageData: nextPageData,
|
||||
getData: getData));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
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>>);
|
||||
}
|
||||
|
||||
// 已选中的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: _sliderAnimation!,
|
||||
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: Row(
|
||||
children: _tabWidgets(),
|
||||
child: Obx(
|
||||
() => SingleChildScrollView(
|
||||
controller: cascadeController.scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: _tabWidgets()),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ValueListenableBuilder<double>(
|
||||
valueListenable: _sliderFixMargin,
|
||||
valueListenable: cascadeController.sliderFixMargin,
|
||||
builder: (_, margin, __) => Positioned(
|
||||
left: margin + _sliderAnimation!.value,
|
||||
left: margin +
|
||||
cascadeController.sliderAnimation!.value,
|
||||
child: Container(
|
||||
key: _sliderKey,
|
||||
width: _sliderWidth,
|
||||
key: cascadeController.sliderKey,
|
||||
width: cascadeController.sliderWidth,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.activeColor,
|
||||
color: activeColor,
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
),
|
||||
|
@ -401,68 +123,352 @@ class _CascadePickerState extends State<CascadePicker>
|
|||
),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
itemCount: _pagesData.length,
|
||||
controller: _pageController,
|
||||
itemCount: cascadeController.pagesData.length,
|
||||
controller: cascadeController.pageController,
|
||||
itemBuilder: (context, index) => _pageWidget(index),
|
||||
onPageChanged: (position) {
|
||||
if (!_isClickAndMoveTab) {
|
||||
_moveSlider(position, movePage: false);
|
||||
if (!cascadeController.isClickAndMoveTab) {
|
||||
cascadeController.moveSlider(position,
|
||||
movePage: false);
|
||||
}
|
||||
if (_currentSelectPage == position) {
|
||||
_isClickAndMoveTab = false;
|
||||
if (cascadeController.urrentSelectPage == position) {
|
||||
cascadeController.isClickAndMoveTab = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
))
|
||||
])));
|
||||
}
|
||||
|
||||
/// 注意:tab渲染完成才开始动画,即调用moveSlider,这个方法会在动画执行期间多次调用
|
||||
Widget _animateTab({required Widget tab}) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
Tween<double>(
|
||||
begin: cascadeController.isAddTabEvent
|
||||
? -cascadeController.animTabWidth
|
||||
: 0,
|
||||
end: 0)
|
||||
.evaluate(cascadeController.curvedAnimation),
|
||||
0),
|
||||
child: Opacity(
|
||||
|
||||
/// 动画未开始前隐藏文本
|
||||
opacity: cascadeController.isAnimateTextHide ? 0 : 1,
|
||||
child: tab),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _tabWidgets() {
|
||||
List<Widget> widgets = [];
|
||||
cascadeController.tabKeys.clear();
|
||||
for (int i = 0; i < cascadeController.pagesData.length; i++) {
|
||||
GlobalKey key = GlobalKey();
|
||||
cascadeController.tabKeys.add(key);
|
||||
final tab = GestureDetector(
|
||||
child: Container(
|
||||
key: key,
|
||||
height: tabHeight,
|
||||
color: tabColor,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
// constraints: BoxConstraints(
|
||||
// maxWidth: MediaQuery.of(Get.context!).size.width /
|
||||
// cascadeController.pagesData.length -
|
||||
// 10),
|
||||
child: Text(
|
||||
cascadeController.selectedTabs[i].label,
|
||||
style: cascadeController.urrentSelectPage == i
|
||||
? tabTitleStyle.copyWith(color: activeColor)
|
||||
: tabTitleStyle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
cascadeController.moveSlider(i);
|
||||
},
|
||||
);
|
||||
if (i == cascadeController.pagesData.length - 1 &&
|
||||
cascadeController.selectedTabs[i].label ==
|
||||
SkCascadePickerController.newTabName) {
|
||||
widgets.add(_animateTab(tab: tab));
|
||||
} 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, CascadeItem item) {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
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 == maxPageNum - 1) {
|
||||
/// 当前页是最后一页
|
||||
cascadeController.selectedTabs[page] = item;
|
||||
cascadeController.selectedIndexes[page] = index;
|
||||
|
||||
/// 调整滑块位置
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
cascadeController.moveSlider(page);
|
||||
});
|
||||
} else if (cascadeController.tabKeys.length >= maxPageNum ||
|
||||
page < cascadeController.tabKeys.length - 1) {
|
||||
if (index == cascadeController.selectedIndexes[page]) {
|
||||
/// 选择相同的item
|
||||
cascadeController.moveSlider(page + 1);
|
||||
} else {
|
||||
/// 选择不同的item,更新tab renderBox
|
||||
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
|
||||
cascadeController.selectedTabs[page] = item;
|
||||
cascadeController.selectedIndexes[page] = index;
|
||||
cascadeController.addTab(page + 1, index, item);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _pageWidget(int page) {
|
||||
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,),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// 第2页的子数据列表
|
||||
item1.children = children2;
|
||||
children1.add(item1);
|
||||
}
|
||||
// 第1页的子数据列表
|
||||
item0.children = children1;
|
||||
items.add(item0);
|
||||
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;
|
||||
}
|
||||
|
||||
late final _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);
|
||||
!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);
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
// 滚动到最底部
|
||||
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设为true,防止滑动PageView
|
||||
/// 时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));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
animateController.dispose();
|
||||
pageController.dispose();
|
||||
scrollController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
|
Loading…
Reference in New Issue