1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:math' as math; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | |
9 | import 'basic.dart'; |
10 | import 'debug.dart'; |
11 | import 'framework.dart'; |
12 | |
13 | const double _kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards |
14 | const double _kHeight = 12.0; // height of banner |
15 | const double _kBottomOffset = _kOffset + 0.707 * _kHeight; // offset plus sqrt(2)/2 * banner height |
16 | const Rect _kRect = Rect.fromLTWH(-_kOffset, _kOffset - _kHeight, _kOffset * 2.0, _kHeight); |
17 | |
18 | const Color _kColor = Color(0xA0B71C1C); |
19 | const TextStyle _kTextStyle = TextStyle( |
20 | color: Color(0xFFFFFFFF), |
21 | fontSize: _kHeight * 0.85, |
22 | fontWeight: FontWeight.w900, |
23 | height: 1.0, |
24 | ); |
25 | |
26 | const String _flutterWidgetsLibrary = 'package:flutter/widgets.dart' ; |
27 | |
28 | /// Where to show a [Banner]. |
29 | /// |
30 | /// The start and end locations are relative to the ambient [Directionality] |
31 | /// (which can be overridden by [Banner.layoutDirection]). |
32 | enum BannerLocation { |
33 | /// Show the banner in the top-right corner when the ambient [Directionality] |
34 | /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-left |
35 | /// corner when the ambient [Directionality] is [TextDirection.ltr]. |
36 | topStart, |
37 | |
38 | /// Show the banner in the top-left corner when the ambient [Directionality] |
39 | /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-right |
40 | /// corner when the ambient [Directionality] is [TextDirection.ltr]. |
41 | topEnd, |
42 | |
43 | /// Show the banner in the bottom-right corner when the ambient |
44 | /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and |
45 | /// in the bottom-left corner when the ambient [Directionality] is |
46 | /// [TextDirection.ltr]. |
47 | bottomStart, |
48 | |
49 | /// Show the banner in the bottom-left corner when the ambient |
50 | /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and |
51 | /// in the bottom-right corner when the ambient [Directionality] is |
52 | /// [TextDirection.ltr]. |
53 | bottomEnd, |
54 | } |
55 | |
56 | /// Paints a [Banner]. |
57 | class BannerPainter extends CustomPainter { |
58 | /// Creates a banner painter. |
59 | BannerPainter({ |
60 | required this.message, |
61 | required this.textDirection, |
62 | required this.location, |
63 | required this.layoutDirection, |
64 | this.color = _kColor, |
65 | this.textStyle = _kTextStyle, |
66 | }) : super(repaint: PaintingBinding.instance.systemFonts) { |
67 | // TODO(polina-c): stop duplicating code across disposables |
68 | // https://github.com/flutter/flutter/issues/137435 |
69 | if (kFlutterMemoryAllocationsEnabled) { |
70 | FlutterMemoryAllocations.instance.dispatchObjectCreated( |
71 | library: _flutterWidgetsLibrary, |
72 | className: ' $BannerPainter' , |
73 | object: this, |
74 | ); |
75 | } |
76 | } |
77 | |
78 | /// The message to show in the banner. |
79 | final String message; |
80 | |
81 | /// The directionality of the text. |
82 | /// |
83 | /// This value is used to disambiguate how to render bidirectional text. For |
84 | /// example, if the message is an English phrase followed by a Hebrew phrase, |
85 | /// in a [TextDirection.ltr] context the English phrase will be on the left |
86 | /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] |
87 | /// context, the English phrase will be on the right and the Hebrew phrase on |
88 | /// its left. |
89 | /// |
90 | /// See also: |
91 | /// |
92 | /// * [layoutDirection], which controls the interpretation of values in |
93 | /// [location]. |
94 | final TextDirection textDirection; |
95 | |
96 | /// Where to show the banner (e.g., the upper right corner). |
97 | final BannerLocation location; |
98 | |
99 | /// The directionality of the layout. |
100 | /// |
101 | /// This value is used to interpret the [location] of the banner. |
102 | /// |
103 | /// See also: |
104 | /// |
105 | /// * [textDirection], which controls the reading direction of the [message]. |
106 | final TextDirection layoutDirection; |
107 | |
108 | /// The color to paint behind the [message]. |
109 | /// |
110 | /// Defaults to a dark red. |
111 | final Color color; |
112 | |
113 | /// The text style to use for the [message]. |
114 | /// |
115 | /// Defaults to bold, white text. |
116 | final TextStyle textStyle; |
117 | |
118 | static const BoxShadow _shadow = BoxShadow( |
119 | color: Color(0x7F000000), |
120 | blurRadius: 6.0, |
121 | ); |
122 | |
123 | bool _prepared = false; |
124 | TextPainter? _textPainter; |
125 | late Paint _paintShadow; |
126 | late Paint _paintBanner; |
127 | |
128 | /// Release resources held by this painter. |
129 | /// |
130 | /// After calling this method, this object is no longer usable. |
131 | void dispose() { |
132 | // TODO(polina-c): stop duplicating code across disposables |
133 | // https://github.com/flutter/flutter/issues/137435 |
134 | if (kFlutterMemoryAllocationsEnabled) { |
135 | FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); |
136 | } |
137 | _textPainter?.dispose(); |
138 | _textPainter = null; |
139 | } |
140 | |
141 | void _prepare() { |
142 | _paintShadow = _shadow.toPaint(); |
143 | _paintBanner = Paint() |
144 | ..color = color; |
145 | _textPainter?.dispose(); |
146 | _textPainter = TextPainter( |
147 | text: TextSpan(style: textStyle, text: message), |
148 | textAlign: TextAlign.center, |
149 | textDirection: textDirection, |
150 | ); |
151 | _prepared = true; |
152 | } |
153 | |
154 | @override |
155 | void paint(Canvas canvas, Size size) { |
156 | if (!_prepared) { |
157 | _prepare(); |
158 | } |
159 | canvas |
160 | ..translate(_translationX(size.width), _translationY(size.height)) |
161 | ..rotate(_rotation) |
162 | ..drawRect(_kRect, _paintShadow) |
163 | ..drawRect(_kRect, _paintBanner); |
164 | const double width = _kOffset * 2.0; |
165 | _textPainter!.layout(minWidth: width, maxWidth: width); |
166 | _textPainter!.paint(canvas, _kRect.topLeft + Offset(0.0, (_kRect.height - _textPainter!.height) / 2.0)); |
167 | } |
168 | |
169 | @override |
170 | bool shouldRepaint(BannerPainter oldDelegate) { |
171 | return message != oldDelegate.message |
172 | || location != oldDelegate.location |
173 | || color != oldDelegate.color |
174 | || textStyle != oldDelegate.textStyle; |
175 | } |
176 | |
177 | @override |
178 | bool hitTest(Offset position) => false; |
179 | |
180 | double _translationX(double width) { |
181 | switch (layoutDirection) { |
182 | case TextDirection.rtl: |
183 | switch (location) { |
184 | case BannerLocation.bottomEnd: |
185 | return _kBottomOffset; |
186 | case BannerLocation.topEnd: |
187 | return 0.0; |
188 | case BannerLocation.bottomStart: |
189 | return width - _kBottomOffset; |
190 | case BannerLocation.topStart: |
191 | return width; |
192 | } |
193 | case TextDirection.ltr: |
194 | switch (location) { |
195 | case BannerLocation.bottomEnd: |
196 | return width - _kBottomOffset; |
197 | case BannerLocation.topEnd: |
198 | return width; |
199 | case BannerLocation.bottomStart: |
200 | return _kBottomOffset; |
201 | case BannerLocation.topStart: |
202 | return 0.0; |
203 | } |
204 | } |
205 | } |
206 | |
207 | double _translationY(double height) { |
208 | switch (location) { |
209 | case BannerLocation.bottomStart: |
210 | case BannerLocation.bottomEnd: |
211 | return height - _kBottomOffset; |
212 | case BannerLocation.topStart: |
213 | case BannerLocation.topEnd: |
214 | return 0.0; |
215 | } |
216 | } |
217 | |
218 | double get _rotation { |
219 | switch (layoutDirection) { |
220 | case TextDirection.rtl: |
221 | switch (location) { |
222 | case BannerLocation.bottomStart: |
223 | case BannerLocation.topEnd: |
224 | return -math.pi / 4.0; |
225 | case BannerLocation.bottomEnd: |
226 | case BannerLocation.topStart: |
227 | return math.pi / 4.0; |
228 | } |
229 | case TextDirection.ltr: |
230 | switch (location) { |
231 | case BannerLocation.bottomStart: |
232 | case BannerLocation.topEnd: |
233 | return math.pi / 4.0; |
234 | case BannerLocation.bottomEnd: |
235 | case BannerLocation.topStart: |
236 | return -math.pi / 4.0; |
237 | } |
238 | } |
239 | } |
240 | } |
241 | |
242 | /// Displays a diagonal message above the corner of another widget. |
243 | /// |
244 | /// Useful for showing the execution mode of an app (e.g., that asserts are |
245 | /// enabled.) |
246 | /// |
247 | /// See also: |
248 | /// |
249 | /// * [CheckedModeBanner], which the [WidgetsApp] widget includes by default in |
250 | /// debug mode, to show a banner that says "DEBUG". |
251 | class Banner extends StatefulWidget { |
252 | /// Creates a banner. |
253 | const Banner({ |
254 | super.key, |
255 | this.child, |
256 | required this.message, |
257 | this.textDirection, |
258 | required this.location, |
259 | this.layoutDirection, |
260 | this.color = _kColor, |
261 | this.textStyle = _kTextStyle, |
262 | }); |
263 | |
264 | /// The widget to show behind the banner. |
265 | /// |
266 | /// {@macro flutter.widgets.ProxyWidget.child} |
267 | final Widget? child; |
268 | |
269 | /// The message to show in the banner. |
270 | final String message; |
271 | |
272 | /// The directionality of the text. |
273 | /// |
274 | /// This is used to disambiguate how to render bidirectional text. For |
275 | /// example, if the message is an English phrase followed by a Hebrew phrase, |
276 | /// in a [TextDirection.ltr] context the English phrase will be on the left |
277 | /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] |
278 | /// context, the English phrase will be on the right and the Hebrew phrase on |
279 | /// its left. |
280 | /// |
281 | /// Defaults to the ambient [Directionality], if any. |
282 | /// |
283 | /// See also: |
284 | /// |
285 | /// * [layoutDirection], which controls the interpretation of the [location]. |
286 | final TextDirection? textDirection; |
287 | |
288 | /// Where to show the banner (e.g., the upper right corner). |
289 | final BannerLocation location; |
290 | |
291 | /// The directionality of the layout. |
292 | /// |
293 | /// This is used to resolve the [location] values. |
294 | /// |
295 | /// Defaults to the ambient [Directionality], if any. |
296 | /// |
297 | /// See also: |
298 | /// |
299 | /// * [textDirection], which controls the reading direction of the [message]. |
300 | final TextDirection? layoutDirection; |
301 | |
302 | /// The color of the banner. |
303 | final Color color; |
304 | |
305 | /// The style of the text shown on the banner. |
306 | final TextStyle textStyle; |
307 | |
308 | @override |
309 | State<Banner> createState() => _BannerState(); |
310 | } |
311 | |
312 | class _BannerState extends State<Banner> { |
313 | BannerPainter? _painter; |
314 | |
315 | @override |
316 | void dispose() { |
317 | _painter?.dispose(); |
318 | super.dispose(); |
319 | } |
320 | |
321 | @override |
322 | Widget build(BuildContext context) { |
323 | assert((widget.textDirection != null && widget.layoutDirection != null) || debugCheckHasDirectionality(context)); |
324 | |
325 | _painter?.dispose(); |
326 | _painter = BannerPainter( |
327 | message: widget.message, |
328 | textDirection: widget.textDirection ?? Directionality.of(context), |
329 | location: widget.location, |
330 | layoutDirection: widget.layoutDirection ?? Directionality.of(context), |
331 | color: widget.color, |
332 | textStyle: widget.textStyle, |
333 | ); |
334 | |
335 | return CustomPaint( |
336 | foregroundPainter: _painter, |
337 | child: widget.child, |
338 | ); |
339 | } |
340 | |
341 | @override |
342 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
343 | super.debugFillProperties(properties); |
344 | properties.add(StringProperty('message' , widget.message, showName: false)); |
345 | properties.add(EnumProperty<TextDirection>('textDirection' , widget.textDirection, defaultValue: null)); |
346 | properties.add(EnumProperty<BannerLocation>('location' , widget.location)); |
347 | properties.add(EnumProperty<TextDirection>('layoutDirection' , widget.layoutDirection, defaultValue: null)); |
348 | properties.add(ColorProperty('color' , widget.color, showName: false)); |
349 | widget.textStyle.debugFillProperties(properties, prefix: 'text ' ); |
350 | } |
351 | } |
352 | |
353 | /// Displays a [Banner] saying "DEBUG" when running in debug mode. |
354 | /// [MaterialApp] builds one of these by default. |
355 | /// |
356 | /// Does nothing in release mode. |
357 | class CheckedModeBanner extends StatelessWidget { |
358 | /// Creates a const debug mode banner. |
359 | const CheckedModeBanner({ |
360 | super.key, |
361 | required this.child, |
362 | }); |
363 | |
364 | /// The widget to show behind the banner. |
365 | /// |
366 | /// {@macro flutter.widgets.ProxyWidget.child} |
367 | final Widget child; |
368 | |
369 | @override |
370 | Widget build(BuildContext context) { |
371 | Widget result = child; |
372 | assert(() { |
373 | result = Banner( |
374 | message: 'DEBUG' , |
375 | textDirection: TextDirection.ltr, |
376 | location: BannerLocation.topEnd, |
377 | child: result, |
378 | ); |
379 | return true; |
380 | }()); |
381 | return result; |
382 | } |
383 | |
384 | @override |
385 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
386 | super.debugFillProperties(properties); |
387 | String message = 'disabled' ; |
388 | assert(() { |
389 | message = '"DEBUG"' ; |
390 | return true; |
391 | }()); |
392 | properties.add(DiagnosticsNode.message(message)); |
393 | } |
394 | } |
395 | |