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
111 void setSublocationId(int newSublocationId) {
112 final int newIndex = _floors.indexWhere((floor) => floor.sublocationId == newSublocationId);
113
114 if (newIndex == -1) {
115 return;
116 }
117
118 final bool wasAlreadySelected = _selectedFloorIndex == newIndex;
119
120 if (!wasAlreadySelected) {
121 setState(() {
122 _selectedFloorIndex = newIndex;
123 });
124 } else {
125 setState(() {});
126 }
127
128 _updateScrollButtonsVisibility();
129
130 if (_scrollController.hasClients) {
131 final bool needAnimation = _floors.length > kMaxVisibleFloors;
132
133 final double targetOffset = newIndex * kFloorRowHeight;
134 final double maxScroll = _scrollController.position.maxScrollExtent;
135 final double clampedOffset = targetOffset.clamp(0.0, maxScroll);
136
137 if (needAnimation) {
138 _scrollController.animateTo(
139 clampedOffset,
140 duration: kScrollAnimationDuration,
141 curve: kScrollAnimationCurve,
142 );
143 } else {
144 _scrollController.jumpTo(clampedOffset);
145 }
146 }
147
148 final selectedLevel = _floors[newIndex];
149 widget.onFloorSelected?.call(selectedLevel.sublocationId, selectedLevel.levelId);
150 }
151
152 void _scrollUp() {
153 final target = (_scrollController.offset - 4 * kFloorRowHeight)
154 .clamp(0.0, _scrollController.position.maxScrollExtent);
155 _scrollController.animateTo(
156 target,
157 duration: kScrollAnimationDuration,
158 curve: kScrollAnimationCurve,
159 );
160 }
161
162 void _scrollDown() {
163 final target = (_scrollController.offset + 4 * kFloorRowHeight)
164 .clamp(0.0, _scrollController.position.maxScrollExtent);
165 _scrollController.animateTo(
166 target,
167 duration: kScrollAnimationDuration,
168 curve: kScrollAnimationCurve,
169 );
170 }
171
172 String _display(String id) => id.length <= 5 ? id : '${id.substring(0, 5)}...';
173
174 double get _height {
175 if (_floors.isEmpty || _floors.length <= 1) return 0.0;
176 return _floors.length >= kMaxVisibleFloors
177 ? kFloorSelectorMaxHeight - 1
178 : _floors.length * kFloorRowHeight - 1;
179 }
180
181 @override
182 Widget build(BuildContext context) {
183 if (_height <= 0) return const SizedBox.shrink();
184
185 final safePadding = MediaQuery.of(context).padding;
186 final padding = widget.config.padding ?? EdgeInsets.only(
187 left: kStandardLeftPadding + safePadding.left,
188 top: kFloorSelectorTopPadding + safePadding.top,
189 );
190
191 return Align(
192 alignment: Alignment.topLeft,
193 child: Padding(
194 padding: padding,
195 child: Container(
196 width: kStandardButtonWidth,
197 height: _height,
198 decoration: BoxDecoration(
199 color: Colors.white,
200 borderRadius: kStandardBorderRadius,
201 boxShadow: kStandardShadows,
202 ),
203 child: Stack(
204 clipBehavior: Clip.none,
205 children: [
206 ClipRRect(
207 borderRadius: kStandardBorderRadius,
208 child: ListView.builder(
209 controller: _scrollController,
210 physics: _floors.length > kMaxVisibleFloors
211 ? const AlwaysScrollableScrollPhysics()
212 : const NeverScrollableScrollPhysics(),
213 itemCount: _floors.length,
214 itemExtent: kFloorRowHeight,
215 itemBuilder: (context, i) {
216 final level = _floors[i];
217 final selected = i == _selectedFloorIndex;
218 final accentColor = widget.config.accentColor ?? kBaseBlueColor;
219 final textColor = widget.config.textColor ?? kBaseBlackColor;
220 return Material(
221 color: selected ? accentColor : Colors.white,
222 child: InkWell(
223 onTap: () {
224 if (i == _selectedFloorIndex) return;
225 setState(() => _selectedFloorIndex = i);
226 widget.onFloorSelected?.call(level.sublocationId, level.levelId);
227
228 final target = i * kFloorRowHeight;
229 final max = _scrollController.position.maxScrollExtent;
230 _scrollController.jumpTo(target.clamp(0.0, max));
231 },
232 child: Center(
233 child: Text(
234 _display(level.levelId),
235 style: TextStyle(
236 color: selected ? Colors.white : textColor,
237 fontSize: kFloorSelectorFontSize,
238 ),
239 ),
240 ),
241 ),
242 );
243 },
244 ),
245 ),
246
247 ValueListenableBuilder<bool>(
248 valueListenable: _showTopNotifier,
249 builder: (_, showTop, __) {
250 return showTop
251 ? Positioned(
252 top: 0,
253 left: 0,
254 right: 0,
255 child: _scrollButton('▲', _scrollUp, kVerticalTopBorderRadius),
256 )
257 : const SizedBox.shrink();
258 },
259 ),
260
261 ValueListenableBuilder<bool>(
262 valueListenable: _showBottomNotifier,
263 builder: (_, showBottom, __) {
264 return showBottom
265 ? Positioned(
266 bottom: 0,
267 left: 0,
268 right: 0,
269 child: _scrollButton('▼', _scrollDown, kVerticalBottomBorderRadius),
270 )
271 : const SizedBox.shrink();
272 },
273 ),
274 ],
275 ),
276 ),
277 ),
278 );
279 }
280
281 Widget _scrollButton(String icon, VoidCallback onTap, BorderRadius radius) {
282 return Container(
283 height: kFloorRowHeight,
284 width: kStandardButtonWidth,
285 decoration: BoxDecoration(
286 color: kButtonBackgroundColor,
287 borderRadius: radius,
288 ),
289 child: Material(
290 color: Colors.transparent,
291 child: InkWell(
292 borderRadius: radius,
293 onTap: onTap,
294 child: Center(
295 child: Text(
296 icon,
297 style: const TextStyle(
298 color: kBaseBlackColor,
299 fontSize: kScrollButtonFontSize,
300 fontWeight: kScrollButtonFontWeight,
301 ),
302 ),
303 ),
304 ),
305 ),
306 );
307 }
308}