| // Copyright 2014 The Flutter 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 'dart:ui' as ui show Gradient, TextBox, lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'alignment.dart'; |
| import 'basic_types.dart'; |
| import 'box_fit.dart'; |
| import 'colors.dart'; |
| import 'decoration.dart'; |
| import 'edge_insets.dart'; |
| import 'image_provider.dart'; |
| import 'text_painter.dart'; |
| import 'text_span.dart'; |
| import 'text_style.dart'; |
| |
| /// Possible ways to draw Flutter's logo. |
| enum FlutterLogoStyle { |
| /// Show only Flutter's logo, not the "Flutter" label. |
| /// |
| /// This is the default behavior for [FlutterLogoDecoration] objects. |
| markOnly, |
| |
| /// Show Flutter's logo on the left, and the "Flutter" label to its right. |
| horizontal, |
| |
| /// Show Flutter's logo above the "Flutter" label. |
| stacked, |
| } |
| |
| /// An immutable description of how to paint Flutter's logo. |
| class FlutterLogoDecoration extends Decoration { |
| /// Creates a decoration that knows how to paint Flutter's logo. |
| /// |
| /// The [style] controls whether and where to draw the "Flutter" label. If one |
| /// is shown, the [textColor] controls the color of the label. |
| /// |
| /// The [textColor], [style], and [margin] arguments must not be null. |
| const FlutterLogoDecoration({ |
| this.textColor = const Color(0xFF757575), |
| this.style = FlutterLogoStyle.markOnly, |
| this.margin = EdgeInsets.zero, |
| }) : assert(textColor != null), |
| assert(style != null), |
| assert(margin != null), |
| _position = identical(style, FlutterLogoStyle.markOnly) ? 0.0 : identical(style, FlutterLogoStyle.horizontal) ? 1.0 : -1.0, |
| _opacity = 1.0; |
| |
| const FlutterLogoDecoration._(this.textColor, this.style, this.margin, this._position, this._opacity); |
| |
| /// The color used to paint the "Flutter" text on the logo, if [style] is |
| /// [FlutterLogoStyle.horizontal] or [FlutterLogoStyle.stacked]. |
| /// |
| /// If possible, the default (a medium grey) should be used against a white |
| /// background. |
| final Color textColor; |
| |
| /// Whether and where to draw the "Flutter" text. By default, only the logo |
| /// itself is drawn. |
| // This property isn't actually used when painting. It's only really used to |
| // set the internal _position property. |
| final FlutterLogoStyle style; |
| |
| /// How far to inset the logo from the edge of the container. |
| final EdgeInsets margin; |
| |
| // The following are set when lerping, to represent states that can't be |
| // represented by the constructor. |
| final double _position; // -1.0 for stacked, 1.0 for horizontal, 0.0 for no logo |
| final double _opacity; // 0.0 .. 1.0 |
| |
| bool get _inTransition => _opacity != 1.0 || (_position != -1.0 && _position != 0.0 && _position != 1.0); |
| |
| @override |
| bool debugAssertIsValid() { |
| assert( |
| textColor != null |
| && style != null |
| && margin != null |
| && _position != null |
| && _position.isFinite |
| && _opacity != null |
| && _opacity >= 0.0 |
| && _opacity <= 1.0, |
| ); |
| return true; |
| } |
| |
| @override |
| bool get isComplex => !_inTransition; |
| |
| /// Linearly interpolate between two Flutter logo descriptions. |
| /// |
| /// Interpolates both the color and the style in a continuous fashion. |
| /// |
| /// If both values are null, this returns null. Otherwise, it returns a |
| /// non-null value. If one of the values is null, then the result is obtained |
| /// by scaling the other value's opacity and [margin]. |
| /// |
| /// {@macro dart.ui.shadow.lerp} |
| /// |
| /// See also: |
| /// |
| /// * [Decoration.lerp], which interpolates between arbitrary decorations. |
| static FlutterLogoDecoration? lerp(FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) { |
| assert(t != null); |
| assert(a == null || a.debugAssertIsValid()); |
| assert(b == null || b.debugAssertIsValid()); |
| if (a == null && b == null) { |
| return null; |
| } |
| if (a == null) { |
| return FlutterLogoDecoration._( |
| b!.textColor, |
| b.style, |
| b.margin * t, |
| b._position, |
| b._opacity * clampDouble(t, 0.0, 1.0), |
| ); |
| } |
| if (b == null) { |
| return FlutterLogoDecoration._( |
| a.textColor, |
| a.style, |
| a.margin * t, |
| a._position, |
| a._opacity * clampDouble(1.0 - t, 0.0, 1.0), |
| ); |
| } |
| if (t == 0.0) { |
| return a; |
| } |
| if (t == 1.0) { |
| return b; |
| } |
| return FlutterLogoDecoration._( |
| Color.lerp(a.textColor, b.textColor, t)!, |
| t < 0.5 ? a.style : b.style, |
| EdgeInsets.lerp(a.margin, b.margin, t)!, |
| a._position + (b._position - a._position) * t, |
| clampDouble(a._opacity + (b._opacity - a._opacity) * t, 0.0, 1.0), |
| ); |
| } |
| |
| @override |
| FlutterLogoDecoration? lerpFrom(Decoration? a, double t) { |
| assert(debugAssertIsValid()); |
| if (a == null || a is FlutterLogoDecoration) { |
| assert(a == null || a.debugAssertIsValid()); |
| return FlutterLogoDecoration.lerp(a as FlutterLogoDecoration?, this, t); |
| } |
| return super.lerpFrom(a, t) as FlutterLogoDecoration?; |
| } |
| |
| @override |
| FlutterLogoDecoration? lerpTo(Decoration? b, double t) { |
| assert(debugAssertIsValid()); |
| if (b == null || b is FlutterLogoDecoration) { |
| assert(b == null || b.debugAssertIsValid()); |
| return FlutterLogoDecoration.lerp(this, b as FlutterLogoDecoration?, t); |
| } |
| return super.lerpTo(b, t) as FlutterLogoDecoration?; |
| } |
| |
| @override |
| // TODO(ianh): better hit testing |
| bool hitTest(Size size, Offset position, { TextDirection? textDirection }) => true; |
| |
| @override |
| BoxPainter createBoxPainter([ VoidCallback? onChanged ]) { |
| assert(debugAssertIsValid()); |
| return _FlutterLogoPainter(this); |
| } |
| |
| @override |
| Path getClipPath(Rect rect, TextDirection textDirection) { |
| return Path()..addRect(rect); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| assert(debugAssertIsValid()); |
| if (identical(this, other)) { |
| return true; |
| } |
| return other is FlutterLogoDecoration |
| && other.textColor == textColor |
| && other._position == _position |
| && other._opacity == _opacity; |
| } |
| |
| @override |
| int get hashCode { |
| assert(debugAssertIsValid()); |
| return Object.hash( |
| textColor, |
| _position, |
| _opacity, |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(ColorProperty('textColor', textColor)); |
| properties.add(EnumProperty<FlutterLogoStyle>('style', style)); |
| if (_inTransition) { |
| properties.add(DiagnosticsNode.message('transition ${debugFormatDouble(_position)}:${debugFormatDouble(_opacity)}')); |
| } |
| } |
| } |
| |
| |
| /// An object that paints a [BoxDecoration] into a canvas. |
| class _FlutterLogoPainter extends BoxPainter { |
| _FlutterLogoPainter(this._config) |
| : assert(_config != null), |
| assert(_config.debugAssertIsValid()), |
| super(null) { |
| _prepareText(); |
| } |
| |
| final FlutterLogoDecoration _config; |
| |
| // these are configured assuming a font size of 100.0. |
| // TODO(dnfield): Figure out how to dispose this https://github.com/flutter/flutter/issues/110601 |
| late TextPainter _textPainter; |
| late Rect _textBoundingRect; |
| |
| void _prepareText() { |
| const String kLabel = 'Flutter'; |
| _textPainter = TextPainter( |
| text: TextSpan( |
| text: kLabel, |
| style: TextStyle( |
| color: _config.textColor, |
| fontFamily: 'Roboto', |
| fontSize: 100.0 * 350.0 / 247.0, // 247 is the height of the F when the fontSize is 350, assuming device pixel ratio 1.0 |
| fontWeight: FontWeight.w300, |
| textBaseline: TextBaseline.alphabetic, |
| ), |
| ), |
| textDirection: TextDirection.ltr, |
| ); |
| _textPainter.layout(); |
| final ui.TextBox textSize = _textPainter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: kLabel.length)).single; |
| _textBoundingRect = Rect.fromLTRB(textSize.left, textSize.top, textSize.right, textSize.bottom); |
| } |
| |
| // This class contains a lot of magic numbers. They were derived from the |
| // values in the SVG files exported from the original artwork source. |
| |
| void _paintLogo(Canvas canvas, Rect rect) { |
| // Our points are in a coordinate space that's 166 pixels wide and 202 pixels high. |
| // First, transform the rectangle so that our coordinate space is a square 202 pixels |
| // to a side, with the top left at the origin. |
| canvas.save(); |
| canvas.translate(rect.left, rect.top); |
| canvas.scale(rect.width / 202.0, rect.height / 202.0); |
| // Next, offset it some more so that the 166 horizontal pixels are centered |
| // in that square (as opposed to being on the left side of it). This means |
| // that if we draw in the rectangle from 0,0 to 166,202, we are drawing in |
| // the center of the given rect. |
| canvas.translate((202.0 - 166.0) / 2.0, 0.0); |
| |
| // Set up the styles. |
| final Paint lightPaint = Paint() |
| ..color = const Color(0xFF54C5F8); |
| final Paint mediumPaint = Paint() |
| ..color = const Color(0xFF29B6F6); |
| final Paint darkPaint = Paint() |
| ..color = const Color(0xFF01579B); |
| |
| final ui.Gradient triangleGradient = ui.Gradient.linear( |
| const Offset(87.2623 + 37.9092, 28.8384 + 123.4389), |
| const Offset(42.9205 + 37.9092, 35.0952 + 123.4389), |
| <Color>[ |
| const Color(0x001A237E), |
| const Color(0x661A237E), |
| ], |
| ); |
| final Paint trianglePaint = Paint() |
| ..shader = triangleGradient; |
| |
| // Draw the basic shape. |
| final Path topBeam = Path() |
| ..moveTo(37.7, 128.9) |
| ..lineTo(9.8, 101.0) |
| ..lineTo(100.4, 10.4) |
| ..lineTo(156.2, 10.4); |
| canvas.drawPath(topBeam, lightPaint); |
| |
| final Path middleBeam = Path() |
| ..moveTo(156.2, 94.0) |
| ..lineTo(100.4, 94.0) |
| ..lineTo(78.5, 115.9) |
| ..lineTo(106.4, 143.8); |
| canvas.drawPath(middleBeam, lightPaint); |
| |
| final Path bottomBeam = Path() |
| ..moveTo(79.5, 170.7) |
| ..lineTo(100.4, 191.6) |
| ..lineTo(156.2, 191.6) |
| ..lineTo(107.4, 142.8); |
| canvas.drawPath(bottomBeam, darkPaint); |
| |
| // The overlap between middle and bottom beam. |
| canvas.save(); |
| canvas.transform(Float64List.fromList(const <double>[ |
| // careful, this is in _column_-major order |
| 0.7071, -0.7071, 0.0, 0.0, |
| 0.7071, 0.7071, 0.0, 0.0, |
| 0.0, 0.0, 1.0, 0.0, |
| -77.697, 98.057, 0.0, 1.0, |
| ])); |
| canvas.drawRect(const Rect.fromLTWH(59.8, 123.1, 39.4, 39.4), mediumPaint); |
| canvas.restore(); |
| |
| // The gradients below the middle beam on top of the bottom beam. |
| final Path triangle = Path() |
| ..moveTo(79.5, 170.7) |
| ..lineTo(120.9, 156.4) |
| ..lineTo(107.4, 142.8); |
| canvas.drawPath(triangle, trianglePaint); |
| |
| canvas.restore(); |
| } |
| |
| @override |
| void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { |
| offset += _config.margin.topLeft; |
| final Size canvasSize = _config.margin.deflateSize(configuration.size!); |
| if (canvasSize.isEmpty) { |
| return; |
| } |
| final Size logoSize; |
| if (_config._position > 0.0) { |
| // horizontal style |
| logoSize = const Size(820.0, 232.0); |
| } else if (_config._position < 0.0) { |
| // stacked style |
| logoSize = const Size(252.0, 306.0); |
| } else { |
| // only the mark |
| logoSize = const Size(202.0, 202.0); |
| } |
| final FittedSizes fittedSize = applyBoxFit(BoxFit.contain, logoSize, canvasSize); |
| assert(fittedSize.source == logoSize); |
| final Rect rect = Alignment.center.inscribe(fittedSize.destination, offset & canvasSize); |
| final double centerSquareHeight = canvasSize.shortestSide; |
| final Rect centerSquare = Rect.fromLTWH( |
| offset.dx + (canvasSize.width - centerSquareHeight) / 2.0, |
| offset.dy + (canvasSize.height - centerSquareHeight) / 2.0, |
| centerSquareHeight, |
| centerSquareHeight, |
| ); |
| |
| final Rect logoTargetSquare; |
| if (_config._position > 0.0) { |
| // horizontal style |
| logoTargetSquare = Rect.fromLTWH(rect.left, rect.top, rect.height, rect.height); |
| } else if (_config._position < 0.0) { |
| // stacked style |
| final double logoHeight = rect.height * 191.0 / 306.0; |
| logoTargetSquare = Rect.fromLTWH( |
| rect.left + (rect.width - logoHeight) / 2.0, |
| rect.top, |
| logoHeight, |
| logoHeight, |
| ); |
| } else { |
| // only the mark |
| logoTargetSquare = centerSquare; |
| } |
| final Rect logoSquare = Rect.lerp(centerSquare, logoTargetSquare, _config._position.abs())!; |
| |
| if (_config._opacity < 1.0) { |
| canvas.saveLayer( |
| offset & canvasSize, |
| Paint() |
| ..colorFilter = ColorFilter.mode( |
| const Color(0xFFFFFFFF).withOpacity(_config._opacity), |
| BlendMode.modulate, |
| ), |
| ); |
| } |
| if (_config._position != 0.0) { |
| if (_config._position > 0.0) { |
| // horizontal style |
| final double fontSize = 2.0 / 3.0 * logoSquare.height * (1 - (10.4 * 2.0) / 202.0); |
| final double scale = fontSize / 100.0; |
| final double finalLeftTextPosition = // position of text in rest position |
| (256.4 / 820.0) * rect.width - // 256.4 is the distance from the left edge to the left of the F when the whole logo is 820.0 wide |
| (32.0 / 350.0) * fontSize; // 32 is the distance from the text bounding box edge to the left edge of the F when the font size is 350 |
| final double initialLeftTextPosition = // position of text when just starting the animation |
| rect.width / 2.0 - _textBoundingRect.width * scale; |
| final Offset textOffset = Offset( |
| rect.left + ui.lerpDouble(initialLeftTextPosition, finalLeftTextPosition, _config._position)!, |
| rect.top + (rect.height - _textBoundingRect.height * scale) / 2.0, |
| ); |
| canvas.save(); |
| if (_config._position < 1.0) { |
| final Offset center = logoSquare.center; |
| final Path path = Path() |
| ..moveTo(center.dx, center.dy) |
| ..lineTo(center.dx + rect.width, center.dy - rect.width) |
| ..lineTo(center.dx + rect.width, center.dy + rect.width) |
| ..close(); |
| canvas.clipPath(path); |
| } |
| canvas.translate(textOffset.dx, textOffset.dy); |
| canvas.scale(scale, scale); |
| _textPainter.paint(canvas, Offset.zero); |
| canvas.restore(); |
| } else if (_config._position < 0.0) { |
| // stacked style |
| final double fontSize = 0.35 * logoTargetSquare.height * (1 - (10.4 * 2.0) / 202.0); |
| final double scale = fontSize / 100.0; |
| if (_config._position > -1.0) { |
| // This limits what the drawRect call below is going to blend with. |
| canvas.saveLayer(_textBoundingRect, Paint()); |
| } else { |
| canvas.save(); |
| } |
| canvas.translate( |
| logoTargetSquare.center.dx - (_textBoundingRect.width * scale / 2.0), |
| logoTargetSquare.bottom, |
| ); |
| canvas.scale(scale, scale); |
| _textPainter.paint(canvas, Offset.zero); |
| if (_config._position > -1.0) { |
| canvas.drawRect(_textBoundingRect.inflate(_textBoundingRect.width * 0.5), Paint() |
| ..blendMode = BlendMode.modulate |
| ..shader = ui.Gradient.linear( |
| Offset(_textBoundingRect.width * -0.5, 0.0), |
| Offset(_textBoundingRect.width * 1.5, 0.0), |
| <Color>[const Color(0xFFFFFFFF), const Color(0xFFFFFFFF), const Color(0x00FFFFFF), const Color(0x00FFFFFF)], |
| <double>[ 0.0, math.max(0.0, _config._position.abs() - 0.1), math.min(_config._position.abs() + 0.1, 1.0), 1.0 ], |
| ), |
| ); |
| } |
| canvas.restore(); |
| } |
| } |
| _paintLogo(canvas, logoSquare); |
| if (_config._opacity < 1.0) { |
| canvas.restore(); |
| } |
| } |
| } |