banner.dart 11.3 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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
import 'package:flutter/painting.dart';
9

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

14 15 16
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
17
final Rect _kRect = Rect.fromLTWH(-_kOffset, _kOffset - _kHeight, _kOffset * 2.0, _kHeight);
18

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

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

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

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

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

55
/// Paints a [Banner].
56
class BannerPainter extends CustomPainter {
57 58
  /// Creates a banner painter.
  ///
59 60
  /// The [message], [textDirection], [location], and [layoutDirection]
  /// arguments must not be null.
61
  BannerPainter({
62
    @required this.message,
Ian Hickson's avatar
Ian Hickson committed
63
    @required this.textDirection,
64
    @required this.location,
65
    @required this.layoutDirection,
66 67
    this.color = _kColor,
    this.textStyle = _kTextStyle,
68
  }) : assert(message != null),
Ian Hickson's avatar
Ian Hickson committed
69
       assert(textDirection != null),
70 71 72
       assert(location != null),
       assert(color != null),
       assert(textStyle != null);
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
  /// See also [layoutDirection], which controls the interpretation of values in
  /// [location].
Ian Hickson's avatar
Ian Hickson committed
88 89
  final TextDirection textDirection;

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

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

101 102 103
  /// The color to paint behind the [message].
  ///
  /// Defaults to a dark red.
104 105
  final Color color;

106 107 108
  /// The text style to use for the [message].
  ///
  /// Defaults to bold, white text.
109 110
  final TextStyle textStyle;

111 112
  static const BoxShadow _shadow = BoxShadow(
    color: Color(0x7F000000),
113
    blurRadius: 6.0,
114 115
  );

116 117 118 119 120 121
  bool _prepared = false;
  TextPainter _textPainter;
  Paint _paintShadow;
  Paint _paintBanner;

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

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

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

  @override
156
  bool hitTest(Offset position) => false;
157 158

  double _translationX(double width) {
159
    assert(location != null);
160 161
    assert(layoutDirection != null);
    switch (layoutDirection) {
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
      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;
        }
        break;
      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;
        }
        break;
186
    }
pq's avatar
pq committed
187
    return null;
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
        return 0.0;
    }
pq's avatar
pq committed
200
    return null;
201 202 203
  }

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

232 233 234 235 236 237 238
/// 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:
///
239
///  * [CheckedModeBanner], which the [WidgetsApp] widget includes by default in
240
///    debug mode, to show a banner that says "DEBUG".
241
class Banner extends StatelessWidget {
242 243
  /// Creates a banner.
  ///
244
  /// The [message] and [location] arguments must not be null.
245
  const Banner({
246 247
    Key key,
    this.child,
248
    @required this.message,
Ian Hickson's avatar
Ian Hickson committed
249
    this.textDirection,
250
    @required this.location,
251
    this.layoutDirection,
252 253
    this.color = _kColor,
    this.textStyle = _kTextStyle,
254 255 256 257 258
  }) : assert(message != null),
       assert(location != null),
       assert(color != null),
       assert(textStyle != null),
       super(key: key);
259

260
  /// The widget to show behind the banner.
261 262
  ///
  /// {@macro flutter.widgets.child}
263
  final Widget child;
264 265

  /// The message to show in the banner.
266
  final String message;
267

Ian Hickson's avatar
Ian Hickson committed
268 269 270 271 272 273
  /// 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]
274
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
275 276 277
  /// its left.
  ///
  /// Defaults to the ambient [Directionality], if any.
278 279 280
  ///
  /// See also [layoutDirection], which controls the interpretation of the
  /// [location].
Ian Hickson's avatar
Ian Hickson committed
281 282
  final TextDirection textDirection;

283
  /// Where to show the banner (e.g., the upper right corner).
284 285
  final BannerLocation location;

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

296 297 298 299 300 301
  /// The color of the banner.
  final Color color;

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

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

  @override
319 320
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
321 322 323 324 325
    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));
    properties.add(DiagnosticsProperty<Color>('color', color, showName: false));
326
    textStyle?.debugFillProperties(properties, prefix: 'text ');
327
  }
328 329
}

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

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

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

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