diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 36dc4e538b..03f035d786 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -9,9 +9,9 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'asset_vendor.dart'; +import 'banner.dart'; import 'basic.dart'; import 'binding.dart'; -import 'checked_mode_banner.dart'; import 'framework.dart'; import 'locale_query.dart'; import 'media_query.dart'; diff --git a/packages/flutter/lib/src/widgets/banner.dart b/packages/flutter/lib/src/widgets/banner.dart new file mode 100644 index 0000000000..d53f30df1e --- /dev/null +++ b/packages/flutter/lib/src/widgets/banner.dart @@ -0,0 +1,143 @@ +// 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; + +import 'basic.dart'; +import 'framework.dart'; + +enum BannerLocation { topRight, topLeft, bottomRight, bottomLeft } + +class BannerPainter extends CustomPainter { + const BannerPainter({ + this.message, + this.location + }); + + final String message; + final BannerLocation location; + + static const Color kColor = const Color(0xA0B71C1C); + static const double kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards + static const double kHeight = 12.0; // height of banner + static const double kBottomOffset = kOffset + 0.707 * kHeight; // offset plus sqrt(2)/2 * banner height + static const Offset kTextAlign = const Offset(0.0, -3.0); // offset to move text up + static const double kFontSize = kHeight * 0.85; + static const double kShadowBlur = 4.0; // shadow blur sigma + static final Rect kRect = new Rect.fromLTWH(-kOffset, kOffset - kHeight, kOffset * 2.0, kHeight); + static const TextStyle kTextStyles = const TextStyle( + color: const Color(0xFFFFFFFF), + fontSize: kFontSize, + fontWeight: FontWeight.w900, + textAlign: TextAlign.center + ); + + @override + void paint(Canvas canvas, Size size) { + final Paint paintShadow = new Paint() + ..color = const Color(0x7F000000) + ..maskFilter = new MaskFilter.blur(BlurStyle.normal, kShadowBlur); + final Paint paintBanner = new Paint() + ..color = kColor; + canvas + ..translate(_translationX(size.width), _translationY(size.height)) + ..rotate(_rotation) + ..drawRect(kRect, paintShadow) + ..drawRect(kRect, paintBanner); + + final TextPainter textPainter = new TextPainter() + ..text = new TextSpan(style: kTextStyles, text: message) + ..maxWidth = kOffset * 2.0 + ..maxHeight = kHeight + ..layout(); + + textPainter.paint(canvas, kRect.topLeft.toOffset() + kTextAlign); + } + + @override + bool shouldRepaint(BannerPainter oldPainter) => false; + + @override + bool hitTest(Point position) => false; + + double _translationX(double width) { + switch (location) { + case BannerLocation.bottomRight: + return width - kBottomOffset; + case BannerLocation.topRight: + return width; + case BannerLocation.bottomLeft: + return kBottomOffset; + case BannerLocation.topLeft: + return 0.0; + } + } + + double _translationY(double height) { + switch (location) { + case BannerLocation.bottomRight: + case BannerLocation.bottomLeft: + return height - kBottomOffset; + case BannerLocation.topRight: + case BannerLocation.topLeft: + return 0.0; + } + } + + double get _rotation { + switch (location) { + case BannerLocation.bottomLeft: + case BannerLocation.topRight: + return math.PI / 4.0; + case BannerLocation.bottomRight: + case BannerLocation.topLeft: + return -math.PI / 4.0; + } + } +} + +class Banner extends StatelessWidget { + Banner({ + Key key, + this.child, + this.message, + this.location + }) : super(key: key); + + final Widget child; + final String message; + final BannerLocation location; + + @override + Widget build(BuildContext context) { + return new CustomPaint( + foregroundPainter: new BannerPainter(message: message, location: location), + child: child + ); + } +} + +/// Displays a banner saying "SLOW MODE" when running in checked mode. +/// Does nothing in release mode. +class CheckedModeBanner extends StatelessWidget { + CheckedModeBanner({ + Key key, + this.child + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + Widget result = child; + assert(() { + result = new Banner( + child: result, + message: 'SLOW MODE', + location: BannerLocation.topRight); + return true; + }); + return result; + } +} diff --git a/packages/flutter/lib/src/widgets/checked_mode_banner.dart b/packages/flutter/lib/src/widgets/checked_mode_banner.dart deleted file mode 100644 index e577bf6b98..0000000000 --- a/packages/flutter/lib/src/widgets/checked_mode_banner.dart +++ /dev/null @@ -1,78 +0,0 @@ -// 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; - -import 'basic.dart'; -import 'framework.dart'; - -class _CheckedModeBannerPainter extends CustomPainter { - const _CheckedModeBannerPainter(); - - static const Color kColor = const Color(0xA0B71C1C); - static const double kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards from the top right corner - static const double kHeight = 12.0; // height of banner - static const Offset kTextAlign = const Offset(0.0, -3.0); // offset to move text up - static const double kFontSize = kHeight * 0.85; - static const double kShadowBlur = 4.0; // shadow blur sigma - static final Rect kRect = new Rect.fromLTWH(-kOffset, kOffset-kHeight, kOffset * 2.0, kHeight); - static const TextStyle kTextStyles = const TextStyle( - color: const Color(0xFFFFFFFF), - fontSize: kFontSize, - fontWeight: FontWeight.w900, - textAlign: TextAlign.center - ); - - static final TextPainter textPainter = new TextPainter() - ..text = new TextSpan(style: kTextStyles, text: 'SLOW MODE') - ..maxWidth = kOffset * 2.0 - ..maxHeight = kHeight - ..layout(); - - @override - void paint(Canvas canvas, Size size) { - final Paint paintShadow = new Paint() - ..color = const Color(0x7F000000) - ..maskFilter = new MaskFilter.blur(BlurStyle.normal, kShadowBlur); - final Paint paintBanner = new Paint() - ..color = kColor; - canvas - ..translate(size.width, 0.0) - ..rotate(math.PI/4) - ..drawRect(kRect, paintShadow) - ..drawRect(kRect, paintBanner); - textPainter.paint(canvas, kRect.topLeft.toOffset() + kTextAlign); - } - - @override - bool shouldRepaint(_CheckedModeBannerPainter oldPainter) => false; - - @override - bool hitTest(Point position) => false; -} - -/// Displays a banner saying "CHECKED" when running in checked mode. -/// Does nothing in release mode. -class CheckedModeBanner extends StatelessWidget { - CheckedModeBanner({ - Key key, - this.child - }) : super(key: key); - - /// The widget below this widget in the tree. - final Widget child; - - @override - Widget build(BuildContext context) { - Widget result = child; - assert(() { - result = new CustomPaint( - foregroundPainter: const _CheckedModeBannerPainter(), - child: result - ); - return true; - }); - return result; - } -} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index e722227715..c569f83128 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -8,9 +8,9 @@ library widgets; export 'src/widgets/app.dart'; export 'src/widgets/asset_vendor.dart'; export 'src/widgets/auto_layout.dart'; +export 'src/widgets/banner.dart'; export 'src/widgets/basic.dart'; export 'src/widgets/binding.dart'; -export 'src/widgets/checked_mode_banner.dart'; export 'src/widgets/child_view.dart'; export 'src/widgets/dismissable.dart'; export 'src/widgets/drag_target.dart'; diff --git a/packages/flutter/test/widget/banner_test.dart b/packages/flutter/test/widget/banner_test.dart new file mode 100644 index 0000000000..417886c12d --- /dev/null +++ b/packages/flutter/test/widget/banner_test.dart @@ -0,0 +1,123 @@ +// 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; + +import 'package:flutter/widgets.dart'; +import 'package:test/test.dart'; + +class TestCanvas implements Canvas { + final List invocations = []; + + @override + void noSuchMethod(Invocation invocation) { + invocations.add(invocation); + } +} + +void main() { + test('A Banner with a location of topLeft paints in the top left', () { + BannerPainter bannerPainter = new BannerPainter( + message:"foo", + location: BannerLocation.topLeft + ); + + TestCanvas canvas = new TestCanvas(); + + bannerPainter.paint(canvas, new Size(1000.0, 1000.0)); + + Invocation translateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #translate; + }); + + expect(translateCommand, isNotNull); + expect(translateCommand.positionalArguments[0], lessThan(100.0)); + expect(translateCommand.positionalArguments[1], lessThan(100.0)); + + Invocation rotateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #rotate; + }); + + expect(rotateCommand, isNotNull); + expect(rotateCommand.positionalArguments[0], equals(-math.PI / 4.0)); + }); + + test('A Banner with a location of topRight paints in the top right', () { + BannerPainter bannerPainter = new BannerPainter( + message:"foo", + location: BannerLocation.topRight + ); + + TestCanvas canvas = new TestCanvas(); + + bannerPainter.paint(canvas, new Size(1000.0, 1000.0)); + + Invocation translateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #translate; + }); + + expect(translateCommand, isNotNull); + expect(translateCommand.positionalArguments[0], greaterThan(900.0)); + expect(translateCommand.positionalArguments[1], lessThan(100.0)); + + Invocation rotateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #rotate; + }); + + expect(rotateCommand, isNotNull); + expect(rotateCommand.positionalArguments[0], equals(math.PI / 4.0)); + }); + + test('A Banner with a location of bottomLeft paints in the bottom left', () { + BannerPainter bannerPainter = new BannerPainter( + message:"foo", + location: BannerLocation.bottomLeft + ); + + TestCanvas canvas = new TestCanvas(); + + bannerPainter.paint(canvas, new Size(1000.0, 1000.0)); + + Invocation translateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #translate; + }); + + expect(translateCommand, isNotNull); + expect(translateCommand.positionalArguments[0], lessThan(100.0)); + expect(translateCommand.positionalArguments[1], greaterThan(900.0)); + + Invocation rotateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #rotate; + }); + + expect(rotateCommand, isNotNull); + expect(rotateCommand.positionalArguments[0], equals(math.PI / 4.0)); + }); + + test('A Banner with a location of bottomRight paints in the bottom right', () { + BannerPainter bannerPainter = new BannerPainter( + message:"foo", + location: BannerLocation.bottomRight + ); + + TestCanvas canvas = new TestCanvas(); + + bannerPainter.paint(canvas, new Size(1000.0, 1000.0)); + + Invocation translateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #translate; + }); + + expect(translateCommand, isNotNull); + expect(translateCommand.positionalArguments[0], greaterThan(900.0)); + expect(translateCommand.positionalArguments[1], greaterThan(900.0)); + + Invocation rotateCommand = canvas.invocations.firstWhere((Invocation invocation) { + return invocation.memberName == #rotate; + }); + + expect(rotateCommand, isNotNull); + expect(rotateCommand.positionalArguments[0], equals(-math.PI / 4.0)); + }); +}