banner.dart 11.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8

9
import 'basic.dart';
10
import 'debug.dart';
11 12
import 'framework.dart';

13 14 15
const double _kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards
const double _kHeight = 12.0; // height of banner
const double _kBottomOffset = _kOffset + 0.707 * _kHeight; // offset plus sqrt(2)/2 * banner height
Dan Field's avatar
Dan Field committed
16
const Rect _kRect = Rect.fromLTWH(-_kOffset, _kOffset - _kHeight, _kOffset * 2.0, _kHeight);
17

18 19 20
const Color _kColor = Color(0xA0B71C1C);
const TextStyle _kTextStyle = TextStyle(
  color: Color(0xFFFFFFFF),
21 22
  fontSize: _kHeight * 0.85,
  fontWeight: FontWeight.w900,
23
  height: 1.0,
24 25 26
);

/// Where to show a [Banner].
27 28 29
///
/// The start and end locations are relative to the ambient [Directionality]
/// (which can be overridden by [Banner.layoutDirection]).
30
enum BannerLocation {
31
  /// Show the banner in the top-right corner when the ambient [Directionality]
32 33
  /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-left
  /// corner when the ambient [Directionality] is [TextDirection.ltr].
34 35 36
  topStart,

  /// Show the banner in the top-left corner when the ambient [Directionality]
37 38
  /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-right
  /// corner when the ambient [Directionality] is [TextDirection.ltr].
39 40 41
  topEnd,

  /// Show the banner in the bottom-right corner when the ambient
42 43 44
  /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
  /// in the bottom-left corner when the ambient [Directionality] is
  /// [TextDirection.ltr].
45 46 47
  bottomStart,

  /// Show the banner in the bottom-left corner when the ambient
48 49 50
  /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
  /// in the bottom-right corner when the ambient [Directionality] is
  /// [TextDirection.ltr].
51
  bottomEnd,
52
}
53

54
/// Paints a [Banner].
55
class BannerPainter extends CustomPainter {
56 57
  /// Creates a banner painter.
  ///
58 59
  /// The [message], [textDirection], [location], and [layoutDirection]
  /// arguments must not be null.
60
  BannerPainter({
61 62 63 64
    required this.message,
    required this.textDirection,
    required this.location,
    required this.layoutDirection,
65 66
    this.color = _kColor,
    this.textStyle = _kTextStyle,
67
  }) : assert(message != null),
Ian Hickson's avatar
Ian Hickson committed
68
       assert(textDirection != null),
69 70
       assert(location != null),
       assert(color != null),
71
       assert(textStyle != null),
72
       super(repaint: PaintingBinding.instance.systemFonts);
73

74
  /// The message to show in the banner.
75 76
  final String message;

Ian Hickson's avatar
Ian Hickson committed
77 78
  /// The directionality of the text.
  ///
79
  /// This value is used to disambiguate how to render bidirectional text. For
Ian Hickson's avatar
Ian Hickson committed
80 81 82
  /// example, if the message is an English phrase followed by a Hebrew phrase,
  /// in a [TextDirection.ltr] context the English phrase will be on the left
  /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
83
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
84
  /// its left.
85
  ///
86 87 88 89
  /// See also:
  ///
  ///  * [layoutDirection], which controls the interpretation of values in
  ///    [location].
Ian Hickson's avatar
Ian Hickson committed
90 91
  final TextDirection textDirection;

92
  /// Where to show the banner (e.g., the upper right corner).
93
  final BannerLocation location;
94

95 96 97 98
  /// The directionality of the layout.
  ///
  /// This value is used to interpret the [location] of the banner.
  ///
99 100 101
  /// See also:
  ///
  ///  * [textDirection], which controls the reading direction of the [message].
102 103
  final TextDirection layoutDirection;

104 105 106
  /// The color to paint behind the [message].
  ///
  /// Defaults to a dark red.
107 108
  final Color color;

109 110 111
  /// The text style to use for the [message].
  ///
  /// Defaults to bold, white text.
112 113
  final TextStyle textStyle;

114 115
  static const BoxShadow _shadow = BoxShadow(
    color: Color(0x7F000000),
116
    blurRadius: 6.0,
117 118
  );

119
  bool _prepared = false;
120 121 122
  late TextPainter _textPainter;
  late Paint _paintShadow;
  late Paint _paintBanner;
123 124

  void _prepare() {
125
    _paintShadow = _shadow.toPaint();
126
    _paintBanner = Paint()
127
      ..color = color;
128 129
    _textPainter = TextPainter(
      text: TextSpan(style: textStyle, text: message),
130
      textAlign: TextAlign.center,
Ian Hickson's avatar
Ian Hickson committed
131
      textDirection: textDirection,
132 133 134 135 136 137 138 139
    );
    _prepared = true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    if (!_prepared)
      _prepare();
140 141 142
    canvas
      ..translate(_translationX(size.width), _translationY(size.height))
      ..rotate(_rotation)
143 144
      ..drawRect(_kRect, _paintShadow)
      ..drawRect(_kRect, _paintBanner);
145
    const double width = _kOffset * 2.0;
146
    _textPainter.layout(minWidth: width, maxWidth: width);
147
    _textPainter.paint(canvas, _kRect.topLeft + Offset(0.0, (_kRect.height - _textPainter.height) / 2.0));
148 149 150
  }

  @override
151 152 153 154 155
  bool shouldRepaint(BannerPainter oldDelegate) {
    return message != oldDelegate.message
        || location != oldDelegate.location
        || color != oldDelegate.color
        || textStyle != oldDelegate.textStyle;
156
  }
157 158

  @override
159
  bool hitTest(Offset position) => false;
160 161

  double _translationX(double width) {
162
    assert(location != null);
163 164
    assert(layoutDirection != null);
    switch (layoutDirection) {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
      case TextDirection.rtl:
        switch (location) {
          case BannerLocation.bottomEnd:
            return _kBottomOffset;
          case BannerLocation.topEnd:
            return 0.0;
          case BannerLocation.bottomStart:
            return width - _kBottomOffset;
          case BannerLocation.topStart:
            return width;
        }
      case TextDirection.ltr:
        switch (location) {
          case BannerLocation.bottomEnd:
            return width - _kBottomOffset;
          case BannerLocation.topEnd:
            return width;
          case BannerLocation.bottomStart:
            return _kBottomOffset;
          case BannerLocation.topStart:
            return 0.0;
        }
187 188 189 190
    }
  }

  double _translationY(double height) {
191
    assert(location != null);
192
    switch (location) {
193 194
      case BannerLocation.bottomStart:
      case BannerLocation.bottomEnd:
195
        return height - _kBottomOffset;
196 197
      case BannerLocation.topStart:
      case BannerLocation.topEnd:
198 199 200 201 202
        return 0.0;
    }
  }

  double get _rotation {
203
    assert(location != null);
204 205
    assert(layoutDirection != null);
    switch (layoutDirection) {
206 207 208 209
      case TextDirection.rtl:
        switch (location) {
          case BannerLocation.bottomStart:
          case BannerLocation.topEnd:
210
            return -math.pi / 4.0;
211 212
          case BannerLocation.bottomEnd:
          case BannerLocation.topStart:
213
            return math.pi / 4.0;
214 215 216 217 218
        }
      case TextDirection.ltr:
        switch (location) {
          case BannerLocation.bottomStart:
          case BannerLocation.topEnd:
219
            return math.pi / 4.0;
220 221
          case BannerLocation.bottomEnd:
          case BannerLocation.topStart:
222
            return -math.pi / 4.0;
223
        }
224 225 226 227
    }
  }
}

228 229 230 231 232 233 234
/// Displays a diagonal message above the corner of another widget.
///
/// Useful for showing the execution mode of an app (e.g., that asserts are
/// enabled.)
///
/// See also:
///
235
///  * [CheckedModeBanner], which the [WidgetsApp] widget includes by default in
236
///    debug mode, to show a banner that says "DEBUG".
237
class Banner extends StatelessWidget {
238 239
  /// Creates a banner.
  ///
240
  /// The [message] and [location] arguments must not be null.
241
  const Banner({
242
    Key? key,
243
    this.child,
244
    required this.message,
Ian Hickson's avatar
Ian Hickson committed
245
    this.textDirection,
246
    required this.location,
247
    this.layoutDirection,
248 249
    this.color = _kColor,
    this.textStyle = _kTextStyle,
250 251 252 253 254
  }) : assert(message != null),
       assert(location != null),
       assert(color != null),
       assert(textStyle != null),
       super(key: key);
255

256
  /// The widget to show behind the banner.
257
  ///
258
  /// {@macro flutter.widgets.ProxyWidget.child}
259
  final Widget? child;
260 261

  /// The message to show in the banner.
262
  final String message;
263

Ian Hickson's avatar
Ian Hickson committed
264 265 266 267 268 269
  /// The directionality of the text.
  ///
  /// This is used to disambiguate how to render bidirectional text. For
  /// example, if the message is an English phrase followed by a Hebrew phrase,
  /// in a [TextDirection.ltr] context the English phrase will be on the left
  /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
270
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
271 272 273
  /// its left.
  ///
  /// Defaults to the ambient [Directionality], if any.
274
  ///
275 276 277
  /// See also:
  ///
  ///  * [layoutDirection], which controls the interpretation of the [location].
278
  final TextDirection? textDirection;
Ian Hickson's avatar
Ian Hickson committed
279

280
  /// Where to show the banner (e.g., the upper right corner).
281 282
  final BannerLocation location;

283 284 285 286 287 288
  /// The directionality of the layout.
  ///
  /// This is used to resolve the [location] values.
  ///
  /// Defaults to the ambient [Directionality], if any.
  ///
289 290 291
  /// See also:
  ///
  ///  * [textDirection], which controls the reading direction of the [message].
292
  final TextDirection? layoutDirection;
293

294 295 296 297 298 299
  /// The color of the banner.
  final Color color;

  /// The style of the text shown on the banner.
  final TextStyle textStyle;

300 301
  @override
  Widget build(BuildContext context) {
302
    assert((textDirection != null && layoutDirection != null) || debugCheckHasDirectionality(context));
303 304
    return CustomPaint(
      foregroundPainter: BannerPainter(
305
        message: message,
306
        textDirection: textDirection ?? Directionality.of(context),
307
        location: location,
308
        layoutDirection: layoutDirection ?? Directionality.of(context),
309 310 311 312
        color: color,
        textStyle: textStyle,
      ),
      child: child,
313 314
    );
  }
315 316

  @override
317 318
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
319 320 321 322
    properties.add(StringProperty('message', message, showName: false));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(EnumProperty<BannerLocation>('location', location));
    properties.add(EnumProperty<TextDirection>('layoutDirection', layoutDirection, defaultValue: null));
323
    properties.add(ColorProperty('color', color, showName: false));
324
    textStyle.debugFillProperties(properties, prefix: 'text ');
325
  }
326 327
}

328
/// Displays a [Banner] saying "DEBUG" when running in debug mode.
329
/// [MaterialApp] builds one of these by default.
330 331
/// Does nothing in release mode.
class CheckedModeBanner extends StatelessWidget {
332
  /// Creates a const debug mode banner.
333
  const CheckedModeBanner({
334 335
    Key? key,
    required this.child,
336 337
  }) : super(key: key);

338
  /// The widget to show behind the banner.
339
  ///
340
  /// {@macro flutter.widgets.ProxyWidget.child}
341 342 343 344 345 346
  final Widget child;

  @override
  Widget build(BuildContext context) {
    Widget result = child;
    assert(() {
347
      result = Banner(
348
        message: 'DEBUG',
Ian Hickson's avatar
Ian Hickson committed
349
        textDirection: TextDirection.ltr,
350
        location: BannerLocation.topEnd,
351
        child: result,
Ian Hickson's avatar
Ian Hickson committed
352
      );
353
      return true;
354
    }());
355 356
    return result;
  }
357 358

  @override
359 360
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
361 362
    String message = 'disabled';
    assert(() {
363
      message = '"DEBUG"';
364
      return true;
365
    }());
366
    properties.add(DiagnosticsNode.message(message));
367
  }
368
}