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

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

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

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

/// 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
    @required this.message,
Ian Hickson's avatar
Ian Hickson committed
62
    @required this.textDirection,
63
    @required this.location,
64
    @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 71
       assert(location != null),
       assert(color != null),
       assert(textStyle != null);
72

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

Ian Hickson's avatar
Ian Hickson committed
76 77
  /// The directionality of the text.
  ///
78
  /// This value is used to disambiguate how to render bidirectional text. For
Ian Hickson's avatar
Ian Hickson committed
79 80 81
  /// 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]
82
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
83
  /// its left.
84
  ///
85 86
  /// See also [layoutDirection], which controls the interpretation of values in
  /// [location].
Ian Hickson's avatar
Ian Hickson committed
87 88
  final TextDirection textDirection;

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

92 93 94 95 96 97 98 99
  /// 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;

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

105 106 107
  /// The text style to use for the [message].
  ///
  /// Defaults to bold, white text.
108 109 110 111 112 113 114 115 116
  final TextStyle textStyle;

  bool _prepared = false;
  TextPainter _textPainter;
  Paint _paintShadow;
  Paint _paintBanner;

  void _prepare() {
    _paintShadow = new Paint()
117
      ..color = const Color(0x7F000000)
118
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
119 120 121 122 123
    _paintBanner = new Paint()
      ..color = color;
    _textPainter = new TextPainter(
      text: new TextSpan(style: textStyle, text: message),
      textAlign: TextAlign.center,
Ian Hickson's avatar
Ian Hickson committed
124
      textDirection: textDirection,
125 126 127 128 129 130 131 132
    );
    _prepared = true;
  }

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

  @override
144 145 146 147 148
  bool shouldRepaint(BannerPainter oldDelegate) {
    return message != oldDelegate.message
        || location != oldDelegate.location
        || color != oldDelegate.color
        || textStyle != oldDelegate.textStyle;
149
  }
150 151

  @override
152
  bool hitTest(Offset position) => false;
153 154

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

  double _translationY(double height) {
187
    assert(location != null);
188
    switch (location) {
189 190
      case BannerLocation.bottomStart:
      case BannerLocation.bottomEnd:
191
        return height - _kBottomOffset;
192 193
      case BannerLocation.topStart:
      case BannerLocation.topEnd:
194 195
        return 0.0;
    }
pq's avatar
pq committed
196
    return null;
197 198 199
  }

  double get _rotation {
200
    assert(location != null);
201 202
    assert(layoutDirection != null);
    switch (layoutDirection) {
203 204 205 206
      case TextDirection.rtl:
        switch (location) {
          case BannerLocation.bottomStart:
          case BannerLocation.topEnd:
207
            return -math.pi / 4.0;
208 209
          case BannerLocation.bottomEnd:
          case BannerLocation.topStart:
210
            return math.pi / 4.0;
211 212 213 214 215 216
        }
        break;
      case TextDirection.ltr:
        switch (location) {
          case BannerLocation.bottomStart:
          case BannerLocation.topEnd:
217
            return math.pi / 4.0;
218 219
          case BannerLocation.bottomEnd:
          case BannerLocation.topStart:
220
            return -math.pi / 4.0;
221 222
        }
        break;
223
    }
pq's avatar
pq committed
224
    return null;
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 243
    Key key,
    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.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
  ///
  /// See also [layoutDirection], which controls the interpretation of the
  /// [location].
Ian Hickson's avatar
Ian Hickson committed
277 278
  final TextDirection textDirection;

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

282 283 284 285 286 287 288 289 290 291
  /// 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;

292 293 294 295 296 297
  /// The color of the banner.
  final Color color;

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

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

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

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

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

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

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