1import 'package:flutter/material.dart';
2import 'widget_styles.dart';
3import 'floor_selector_view_config.dart';
13 final int sublocationId;
14 LevelInfo({required this.levelId, required this.sublocationId});
18typedef FloorSelectedCallback =
void Function(
int sublocationId, String levelId);
40 State<FloorSelectorView>
createState() => FloorSelectorViewState();
43class FloorSelectorViewState
extends State<FloorSelectorView> {
44 final ScrollController _scrollController = ScrollController();
46 List<LevelInfo> _floors = [];
47 int _selectedFloorIndex = -1;
49 final ValueNotifier<bool> _showTopNotifier = ValueNotifier(
false);
50 final ValueNotifier<bool> _showBottomNotifier = ValueNotifier(
false);
55 _scrollController.addListener(_updateScrollButtonsVisibility);
60 _scrollController.removeListener(_updateScrollButtonsVisibility);
61 _scrollController.dispose();
62 _showTopNotifier.dispose();
63 _showBottomNotifier.dispose();
67 void _updateScrollButtonsVisibility() {
68 if (!_scrollController.hasClients) {
69 _showTopNotifier.value =
false;
70 _showBottomNotifier.value =
false;
74 final bool tooManyFloors = _floors.length > kMaxVisibleFloors;
75 final double offset = _scrollController.offset;
76 final double maxScroll = _scrollController.position.maxScrollExtent;
78 final bool showTop = tooManyFloors && offset > 1.0;
79 final bool showBottom = tooManyFloors && (maxScroll - offset) > 1.0;
81 if (_showTopNotifier.value != showTop) {
82 _showTopNotifier.value = showTop;
84 if (_showBottomNotifier.value != showBottom) {
85 _showBottomNotifier.value = showBottom;
90 void setFloors(List<LevelInfo> floors) {
91 final newFloors = floors.isNotEmpty ? floors : <LevelInfo>[];
94 if (_selectedFloorIndex < 0 || _selectedFloorIndex >= _floors.length) {
95 _selectedFloorIndex = _floors.isNotEmpty ? 0 : -1;
99 _updateScrollButtonsVisibility();
101 if (_selectedFloorIndex >= 0 && _scrollController.hasClients) {
102 WidgetsBinding.instance.addPostFrameCallback((_) {
103 final target = _selectedFloorIndex * kFloorRowHeight;
104 final max = _scrollController.position.maxScrollExtent;
105 _scrollController.jumpTo(target.clamp(0.0, max));
113 void setSublocationId(
int newSublocationId) {
114 final int newIndex = _floors.indexWhere((floor) => floor.sublocationId == newSublocationId);
116 if (newIndex == -1) {
120 final bool wasAlreadySelected = _selectedFloorIndex == newIndex;
121 if (wasAlreadySelected) {
126 _selectedFloorIndex = newIndex;
129 _updateScrollButtonsVisibility();
131 if (_scrollController.hasClients) {
132 final bool needAnimation = _floors.length > kMaxVisibleFloors;
134 final double targetOffset = newIndex * kFloorRowHeight;
135 final double maxScroll = _scrollController.position.maxScrollExtent;
136 final double clampedOffset = targetOffset.clamp(0.0, maxScroll);
139 _scrollController.animateTo(
141 duration: kScrollAnimationDuration,
142 curve: kScrollAnimationCurve,
145 _scrollController.jumpTo(clampedOffset);
151 final target = (_scrollController.offset - 4 * kFloorRowHeight)
152 .clamp(0.0, _scrollController.position.maxScrollExtent);
153 _scrollController.animateTo(
155 duration: kScrollAnimationDuration,
156 curve: kScrollAnimationCurve,
161 final target = (_scrollController.offset + 4 * kFloorRowHeight)
162 .clamp(0.0, _scrollController.position.maxScrollExtent);
163 _scrollController.animateTo(
165 duration: kScrollAnimationDuration,
166 curve: kScrollAnimationCurve,
170 String _display(String
id) => id.length <= 5 ? id :
'${id.substring(0, 5)}...';
173 if (_floors.isEmpty || _floors.length <= 1)
return 0.0;
174 return _floors.length >= kMaxVisibleFloors
175 ? kFloorSelectorMaxHeight - 1
176 : _floors.length * kFloorRowHeight - 1;
180 Widget build(BuildContext context) {
181 if (_height <= 0)
return const SizedBox.shrink();
183 final safePadding = MediaQuery.of(context).padding;
184 final padding = widget.config.padding ?? EdgeInsets.only(
185 left: kStandardLeftPadding + safePadding.left,
186 top: kFloorSelectorTopPadding + safePadding.top,
190 alignment: Alignment.topLeft,
194 width: kStandardButtonWidth,
196 decoration: BoxDecoration(
198 borderRadius: kStandardBorderRadius,
199 boxShadow: kStandardShadows,
202 clipBehavior: Clip.none,
205 borderRadius: kStandardBorderRadius,
206 child: ListView.builder(
207 controller: _scrollController,
208 physics: _floors.length > kMaxVisibleFloors
209 ?
const AlwaysScrollableScrollPhysics()
210 :
const NeverScrollableScrollPhysics(),
211 itemCount: _floors.length,
212 itemExtent: kFloorRowHeight,
213 itemBuilder: (context, i) {
214 final level = _floors[i];
215 final selected = i == _selectedFloorIndex;
216 final accentColor = widget.config.accentColor ?? kBaseBlueColor;
217 final textColor = widget.config.textColor ?? kBaseBlackColor;
219 color: selected ? accentColor : Colors.white,
222 if (i == _selectedFloorIndex) return;
223 setState(() => _selectedFloorIndex = i);
224 widget.onFloorSelected?.call(level.sublocationId, level.levelId);
226 final target = i * kFloorRowHeight;
227 final max = _scrollController.position.maxScrollExtent;
228 _scrollController.jumpTo(target.clamp(0.0, max));
232 _display(level.levelId),
234 color: selected ? Colors.white : textColor,
235 fontSize: kFloorSelectorFontSize,
245 ValueListenableBuilder<bool>(
246 valueListenable: _showTopNotifier,
247 builder: (_, showTop, __) {
253 child: _scrollButton(
'▲', _scrollUp, kVerticalTopBorderRadius),
255 : const SizedBox.shrink();
259 ValueListenableBuilder<bool>(
260 valueListenable: _showBottomNotifier,
261 builder: (_, showBottom, __) {
267 child: _scrollButton(
'▼', _scrollDown, kVerticalBottomBorderRadius),
269 : const SizedBox.shrink();
279 Widget _scrollButton(String icon, VoidCallback onTap, BorderRadius radius) {
281 height: kFloorRowHeight,
282 width: kStandardButtonWidth,
283 decoration: BoxDecoration(
284 color: kButtonBackgroundColor,
285 borderRadius: radius,
288 color: Colors.transparent,
290 borderRadius: radius,
295 style:
const TextStyle(
296 color: kBaseBlackColor,
297 fontSize: kScrollButtonFontSize,
298 fontWeight: kScrollButtonFontWeight,