QinGang_interested/lib/customWidget/bottom_picker.dart

316 lines
12 KiB
Dart
Raw Normal View History

2025-12-12 09:11:30 +08:00
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
2026-04-08 15:03:56 +08:00
2026-04-21 09:30:13 +08:00
/// 选择结果(单选)
2026-04-08 15:03:56 +08:00
class PickerResult<T> {
final T value;
final int index;
PickerResult(this.value, this.index);
}
2025-12-12 09:11:30 +08:00
2026-04-21 09:30:13 +08:00
/// 选择结果(多选)
class MultiPickerResult<T> {
final List<T> values;
final List<int> indices;
MultiPickerResult(this.values, this.indices);
}
/// 显示样式
enum BottomPickerStyle {
wheel,
list,
}
2025-12-12 09:11:30 +08:00
/// 通用底部弹窗选择器
class BottomPicker {
2026-04-08 15:03:56 +08:00
static Future<dynamic> show<T>(
BuildContext context, {
required List<T> items,
required Widget Function(T item) itemBuilder,
int initialIndex = 0,
double itemExtent = 50.0,
double height = 250,
bool withIndex = false,
2026-04-21 09:30:13 +08:00
BottomPickerStyle style = BottomPickerStyle.wheel,
bool multiSelect = false,
List<int>? initialSelectedIndices,
/// 新增:列表样式标题
String title = '请选择',
String cancelText = '取消',
String confirmText = '确定',
int maxLines = 3,
2026-04-08 15:03:56 +08:00
}) {
2025-12-12 09:11:30 +08:00
if (items.isEmpty) return Future.value(null);
final safeIndex = initialIndex.clamp(0, items.length - 1);
2026-04-21 09:30:13 +08:00
final initialMulti = (initialSelectedIndices ?? const <int>[])
.where((e) => e >= 0 && e < items.length)
.toSet();
if (style == BottomPickerStyle.wheel) {
T selected = items[safeIndex];
int selectedIndex = safeIndex;
2026-04-08 15:03:56 +08:00
2026-04-21 09:30:13 +08:00
return showModalBottomSheet<dynamic>(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (ctx) {
return SizedBox(
height: height,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(ctx).pop();
},
child: Text(
cancelText,
style: const TextStyle(
color: Colors.black54,
fontSize: 16,
),
),
),
TextButton(
onPressed: () {
FocusScope.of(context).unfocus();
if (withIndex) {
Navigator.of(ctx).pop(
PickerResult<T>(selected, selectedIndex),
);
} else {
Navigator.of(ctx).pop(selected);
}
},
child: Text(
confirmText,
style: const TextStyle(
color: Colors.blue,
fontSize: 16,
),
),
),
],
),
),
const Divider(height: 1),
Expanded(
child: CupertinoPicker(
scrollController: FixedExtentScrollController(
initialItem: safeIndex,
),
itemExtent: itemExtent,
onSelectedItemChanged: (index) {
selected = items[index];
selectedIndex = index;
},
children: items.map((item) {
return Center(
child: SizedBox(
width: double.infinity,
child: DefaultTextStyle.merge(
style: const TextStyle(fontSize: 16),
child: itemBuilder(item),
),
),
);
}).toList(),
),
),
],
),
);
},
);
}
2025-12-12 09:11:30 +08:00
2026-04-21 09:30:13 +08:00
// 列表样式
2026-04-08 15:03:56 +08:00
return showModalBottomSheet<dynamic>(
2025-12-12 09:11:30 +08:00
context: context,
2026-04-21 09:30:13 +08:00
isScrollControlled: true,
2025-12-12 09:11:30 +08:00
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (ctx) {
2026-04-21 09:30:13 +08:00
int selectedIndex = safeIndex;
final Set<int> selectedIndices = Set<int>.from(initialMulti);
if (!multiSelect && selectedIndices.isEmpty) {
selectedIndices.add(safeIndex);
}
T currentSingleValue = items[selectedIndex];
return SafeArea(
child: SizedBox(
height: height,
child: StatefulBuilder(
builder: (context, setState) {
return Column(
2025-12-12 09:11:30 +08:00
children: [
2026-04-21 09:30:13 +08:00
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
2026-04-08 15:03:56 +08:00
),
2026-04-21 09:30:13 +08:00
child: Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
TextButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(ctx).pop();
},
child: Text(
cancelText,
style: const TextStyle(
color: Colors.black54,
fontSize: 16,
),
),
),
TextButton(
onPressed: () {
FocusScope.of(context).unfocus();
if (multiSelect) {
final indices = selectedIndices.toList()..sort();
final values = indices.map((e) => items[e]).toList();
2026-04-08 15:03:56 +08:00
2026-04-21 09:30:13 +08:00
if (withIndex) {
Navigator.of(ctx).pop(
MultiPickerResult<T>(values, indices),
);
} else {
Navigator.of(ctx).pop(values);
}
} else {
if (withIndex) {
Navigator.of(ctx).pop(
PickerResult<T>(currentSingleValue, selectedIndex),
);
} else {
Navigator.of(ctx).pop(currentSingleValue);
}
}
},
child: Text(
confirmText,
style: const TextStyle(
color: Colors.blue,
fontSize: 16,
),
),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
final isSelected = multiSelect
? selectedIndices.contains(index)
: selectedIndex == index;
return InkWell(
onTap: () {
setState(() {
if (multiSelect) {
if (selectedIndices.contains(index)) {
selectedIndices.remove(index);
} else {
selectedIndices.add(index);
}
} else {
selectedIndex = index;
currentSingleValue = item;
}
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (multiSelect)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(
isSelected
? Icons.check_box
: Icons.check_box_outline_blank,
color: isSelected ? Colors.blue : Colors.grey,
size: 22,
),
)
else
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_off,
color: isSelected ? Colors.blue : Colors.grey,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
height: 1.35,
),
child: itemBuilder(item),
),
),
],
),
),
2026-04-08 15:03:56 +08:00
);
2026-04-21 09:30:13 +08:00
},
2026-04-08 15:03:56 +08:00
),
2025-12-12 09:11:30 +08:00
),
],
2026-04-21 09:30:13 +08:00
);
},
),
2025-12-12 09:11:30 +08:00
),
);
},
);
}
2026-04-08 15:03:56 +08:00
}