flutter_integrated_whb/lib/customWidget/center_multi_picker.dart

264 lines
8.7 KiB
Dart
Raw Normal View History

2025-09-10 13:48:03 +08:00
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
/// 居中多选弹窗Dialog
/// 返回 Future<List<T>?>:用户点击确定返回所选项列表;取消或关闭返回 null。
class CenterMultiPicker {
static Future<List<T>?> show<T>(
BuildContext context, {
required List<T> items,
required Widget Function(T item) itemBuilder,
List<int>? initialSelectedIndices,
int? maxSelection,
bool allowEmpty = false,
double itemHeight = 52,
double maxHeightFactor = 0.75, // 屏幕高度的最大占比
String? title,
}) {
if (items.isEmpty) return Future.value(null);
// 安全化初始索引
final initialSet = <int>{};
if (initialSelectedIndices != null) {
for (final i in initialSelectedIndices) {
// if (i >= 0 && i < items.length) initialSet.add(i);
}
}
return showDialog<List<T>?>(
context: context,
barrierDismissible: true,
builder: (ctx) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
insetPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 24,
),
child: _CenterMultiPickerBody<T>(
items: items,
itemBuilder: itemBuilder,
initialSelected: initialSet,
maxSelection: maxSelection,
allowEmpty: allowEmpty,
itemHeight: itemHeight,
maxHeightFactor: maxHeightFactor,
title: title,
),
);
},
);
}
}
class _CenterMultiPickerBody<T> extends StatefulWidget {
const _CenterMultiPickerBody({
Key? key,
required this.items,
required this.itemBuilder,
required this.initialSelected,
required this.maxSelection,
required this.allowEmpty,
required this.itemHeight,
required this.maxHeightFactor,
this.title,
}) : super(key: key);
final List<T> items;
final Widget Function(T item) itemBuilder;
final Set<int> initialSelected;
final int? maxSelection;
final bool allowEmpty;
final double itemHeight;
final double maxHeightFactor;
final String? title;
@override
State<_CenterMultiPickerBody<T>> createState() =>
_CenterMultiPickerBodyState<T>();
}
class _CenterMultiPickerBodyState<T> extends State<_CenterMultiPickerBody<T>> {
late Set<int> _selected;
// 固定的 header / footer 高度估算
static const double _headerHeight = 56;
static const double _footerHeight = 58;
static const double _verticalPadding = 16; // Dialog 内上下 padding
@override
void initState() {
super.initState();
_selected = Set<int>.from(widget.initialSelected);
}
void _toggle(int idx) {
setState(() {
if (_selected.contains(idx)) {
_selected.remove(idx);
} else {
if (widget.maxSelection != null &&
_selected.length >= widget.maxSelection!) {
ToastUtil.showNormal(context, '最多可选择 ${widget.maxSelection}');
return;
}
_selected.add(idx);
}
});
}
@override
Widget build(BuildContext context) {
final items = widget.items;
final screenH = MediaQuery.of(context).size.height;
final contentHeight =
items.length * widget.itemHeight +
_headerHeight +
_footerHeight +
_verticalPadding * 2;
final maxAllowed = screenH * widget.maxHeightFactor;
final dialogHeight =
contentHeight <= maxAllowed ? contentHeight : maxAllowed;
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10)
),
width: double.infinity,
height: dialogHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// header
Container(
height: _headerHeight,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Row(
children: [
if (widget.title != null)
Expanded(
child: Text(
widget.title!,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
)
else
Expanded(
child: Text(
'已选 ${_selected.length}${widget.maxSelection != null ? '/${widget.maxSelection}' : ''}',
style: const TextStyle(fontSize: 15),
),
),
// 可将一些快捷按钮放在右侧(如全选/反选),这里暂不显示
],
),
),
const Divider(height: 1),
// 列表区域(可滚动)
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Scrollbar(
thumbVisibility: true,
child: ListView.separated(
physics: const BouncingScrollPhysics(),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (ctx, idx) {
final isSelected = _selected.contains(idx);
return InkWell(
onTap: () => _toggle(idx),
child: Container(
height: widget.itemHeight,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 6,
),
child: Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color:
isSelected
? Colors.blue
: Colors.transparent,
border: Border.all(
color:
isSelected ? Colors.blue : Colors.black26,
),
borderRadius: BorderRadius.circular(4),
),
child:
isSelected
? const Icon(
Icons.check,
size: 18,
color: Colors.white,
)
: null,
),
const SizedBox(width: 12),
Expanded(child: widget.itemBuilder(items[idx])),
],
),
),
);
},
),
),
),
),
const Divider(height: 1),
// footer: 取消 / 确定(固定在底部)
Container(
height: _footerHeight,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
child: CustomButton(
text: '取消',
backgroundColor: Colors.grey.shade200,
textStyle: TextStyle(fontSize: 14, color: Colors.black),
onPressed: () {
Navigator.of(context).pop(null);
},
),
),
const SizedBox(width: 12),
Expanded(
child: CustomButton(
text: '确定',
backgroundColor: Colors.blue,
onPressed: () {
if (!widget.allowEmpty && _selected.isEmpty) {
ToastUtil.showNormal(context, '请至少选择一项');
return;
}
final result = _selected
.map((i) => items[i])
.toList(growable: false);
Navigator.of(context).pop(result);
},
),
),
],
),
),
],
),
);
}
}