flutter_integrated_whb/lib/customWidget/center_multi_picker.dart

264 lines
8.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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);
},
),
),
],
),
),
],
),
);
}
}