Loading...
Searching...
No Matches
floor_selector_view.dart
Go to the documentation of this file.
1import 'package:flutter/material.dart';
2import 'widget_styles.dart';
3import 'floor_selector_view_config.dart';
4
9
11class LevelInfo {
12 final String levelId;
13 final int sublocationId;
14 LevelInfo({required this.levelId, required this.sublocationId});
15}
16
18typedef FloorSelectedCallback = void Function(int sublocationId, String levelId);
19
29class FloorSelectorView extends StatefulWidget {
30 final FloorSelectedCallback? onFloorSelected;
31 final FloorSelectorViewConfig config;
32
34 Key? key,
35 this.onFloorSelected,
37 }) : super(key: key);
38
39 @override
40 State<FloorSelectorView> createState() => FloorSelectorViewState();
41}
42
43class FloorSelectorViewState extends State<FloorSelectorView> {
44 final ScrollController _scrollController = ScrollController();
45
46 List<LevelInfo> _floors = [];
47 int _selectedFloorIndex = -1;
48
49 final ValueNotifier<bool> _showTopNotifier = ValueNotifier(false);
50 final ValueNotifier<bool> _showBottomNotifier = ValueNotifier(false);
51
52 @override
53 void initState() {
54 super.initState();
55 _scrollController.addListener(_updateScrollButtonsVisibility);
56 }
57
58 @override
59 void dispose() {
60 _scrollController.removeListener(_updateScrollButtonsVisibility);
61 _scrollController.dispose();
62 _showTopNotifier.dispose();
63 _showBottomNotifier.dispose();
64 super.dispose();
65 }
66
67 void _updateScrollButtonsVisibility() {
68 if (!_scrollController.hasClients) {
69 _showTopNotifier.value = false;
70 _showBottomNotifier.value = false;
71 return;
72 }
73
74 final bool tooManyFloors = _floors.length > kMaxVisibleFloors;
75 final double offset = _scrollController.offset;
76 final double maxScroll = _scrollController.position.maxScrollExtent;
77
78 final bool showTop = tooManyFloors && offset > 1.0;
79 final bool showBottom = tooManyFloors && (maxScroll - offset) > 1.0;
80
81 if (_showTopNotifier.value != showTop) {
82 _showTopNotifier.value = showTop;
83 }
84 if (_showBottomNotifier.value != showBottom) {
85 _showBottomNotifier.value = showBottom;
86 }
87 }
88
90 void setFloors(List<LevelInfo> floors) {
91 final newFloors = floors.isNotEmpty ? floors : <LevelInfo>[];
92 setState(() {
93 _floors = newFloors;
94 if (_selectedFloorIndex < 0 || _selectedFloorIndex >= _floors.length) {
95 _selectedFloorIndex = _floors.isNotEmpty ? 0 : -1;
96 }
97 });
98
99 _updateScrollButtonsVisibility();
100
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));
106 });
107 }
108 }
109
113 void setSublocationId(int newSublocationId) {
114 final int newIndex = _floors.indexWhere((floor) => floor.sublocationId == newSublocationId);
115
116 if (newIndex == -1) {
117 return;
118 }
119
120 final bool wasAlreadySelected = _selectedFloorIndex == newIndex;
121 if (wasAlreadySelected) {
122 return;
123 }
124
125 setState(() {
126 _selectedFloorIndex = newIndex;
127 });
128
129 _updateScrollButtonsVisibility();
130
131 if (_scrollController.hasClients) {
132 final bool needAnimation = _floors.length > kMaxVisibleFloors;
133
134 final double targetOffset = newIndex * kFloorRowHeight;
135 final double maxScroll = _scrollController.position.maxScrollExtent;
136 final double clampedOffset = targetOffset.clamp(0.0, maxScroll);
137
138 if (needAnimation) {
139 _scrollController.animateTo(
140 clampedOffset,
141 duration: kScrollAnimationDuration,
142 curve: kScrollAnimationCurve,
143 );
144 } else {
145 _scrollController.jumpTo(clampedOffset);
146 }
147 }
148 }
149
150 void _scrollUp() {
151 final target = (_scrollController.offset - 4 * kFloorRowHeight)
152 .clamp(0.0, _scrollController.position.maxScrollExtent);
153 _scrollController.animateTo(
154 target,
155 duration: kScrollAnimationDuration,
156 curve: kScrollAnimationCurve,
157 );
158 }
159
160 void _scrollDown() {
161 final target = (_scrollController.offset + 4 * kFloorRowHeight)
162 .clamp(0.0, _scrollController.position.maxScrollExtent);
163 _scrollController.animateTo(
164 target,
165 duration: kScrollAnimationDuration,
166 curve: kScrollAnimationCurve,
167 );
168 }
169
170 String _display(String id) => id.length <= 5 ? id : '${id.substring(0, 5)}...';
171
172 double get _height {
173 if (_floors.isEmpty || _floors.length <= 1) return 0.0;
174 return _floors.length >= kMaxVisibleFloors
175 ? kFloorSelectorMaxHeight - 1
176 : _floors.length * kFloorRowHeight - 1;
177 }
178
179 @override
180 Widget build(BuildContext context) {
181 if (_height <= 0) return const SizedBox.shrink();
182
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,
187 );
188
189 return Align(
190 alignment: Alignment.topLeft,
191 child: Padding(
192 padding: padding,
193 child: Container(
194 width: kStandardButtonWidth,
195 height: _height,
196 decoration: BoxDecoration(
197 color: Colors.white,
198 borderRadius: kStandardBorderRadius,
199 boxShadow: kStandardShadows,
200 ),
201 child: Stack(
202 clipBehavior: Clip.none,
203 children: [
204 ClipRRect(
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;
218 return Material(
219 color: selected ? accentColor : Colors.white,
220 child: InkWell(
221 onTap: () {
222 if (i == _selectedFloorIndex) return;
223 setState(() => _selectedFloorIndex = i);
224 widget.onFloorSelected?.call(level.sublocationId, level.levelId);
225
226 final target = i * kFloorRowHeight;
227 final max = _scrollController.position.maxScrollExtent;
228 _scrollController.jumpTo(target.clamp(0.0, max));
229 },
230 child: Center(
231 child: Text(
232 _display(level.levelId),
233 style: TextStyle(
234 color: selected ? Colors.white : textColor,
235 fontSize: kFloorSelectorFontSize,
236 ),
237 ),
238 ),
239 ),
240 );
241 },
242 ),
243 ),
244
245 ValueListenableBuilder<bool>(
246 valueListenable: _showTopNotifier,
247 builder: (_, showTop, __) {
248 return showTop
249 ? Positioned(
250 top: 0,
251 left: 0,
252 right: 0,
253 child: _scrollButton('▲', _scrollUp, kVerticalTopBorderRadius),
254 )
255 : const SizedBox.shrink();
256 },
257 ),
258
259 ValueListenableBuilder<bool>(
260 valueListenable: _showBottomNotifier,
261 builder: (_, showBottom, __) {
262 return showBottom
263 ? Positioned(
264 bottom: 0,
265 left: 0,
266 right: 0,
267 child: _scrollButton('▼', _scrollDown, kVerticalBottomBorderRadius),
268 )
269 : const SizedBox.shrink();
270 },
271 ),
272 ],
273 ),
274 ),
275 ),
276 );
277 }
278
279 Widget _scrollButton(String icon, VoidCallback onTap, BorderRadius radius) {
280 return Container(
281 height: kFloorRowHeight,
282 width: kStandardButtonWidth,
283 decoration: BoxDecoration(
284 color: kButtonBackgroundColor,
285 borderRadius: radius,
286 ),
287 child: Material(
288 color: Colors.transparent,
289 child: InkWell(
290 borderRadius: radius,
291 onTap: onTap,
292 child: Center(
293 child: Text(
294 icon,
295 style: const TextStyle(
296 color: kBaseBlackColor,
297 fontSize: kScrollButtonFontSize,
298 fontWeight: kScrollButtonFontWeight,
299 ),
300 ),
301 ),
302 ),
303 ),
304 );
305 }
306}