Commit 955b3e21 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

A Flutter logo widget. (#5382)

Instead of a PNG, the Flutter gallery widget is now drawn in code.

There's now a FlutterLogoDecoration class that paints the flutter logo
anywhere you can use a Decoration (e.g. AnimatedContainer).

There's now a FlutterLogo class that honors the IconTheme.

The About dialog box API now takes a Widget for the applicationIcon,
instead of an ImageProvider. It uses IconTheme to make the icon the
right size instead of using an Image widget.

Add padding, duration, and curve properties to the DrawerHeader.
Make the child of a DrawerHeader optional.

Clean up UserAccuntsDrawerHeader a bit.

Add some useful properties and methods to EdgeInsets.

Add some debug logic to RenderDecoratedBox to catch unpaired
save/restore calls when possible.

Make GestureDetector fill its parent if it has no children. Fixes
https://github.com/flutter/flutter/issues/5380
parent e2d0917e
......@@ -2,6 +2,8 @@
// 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
......@@ -16,6 +18,69 @@ class LinkTextSpan extends TextSpan {
);
}
class GalleryDrawerHeader extends StatefulWidget {
const GalleryDrawerHeader({ Key key }) : super(key: key);
@override
_GalleryDrawerHeaderState createState() => new _GalleryDrawerHeaderState();
}
class _GalleryDrawerHeaderState extends State<GalleryDrawerHeader> {
bool _logoHasName = true;
bool _logoHorizontal = true;
Map<int, Color> _swatch = Colors.blue;
@override
Widget build(BuildContext context) {
final double systemTopPadding = MediaQuery.of(context).padding.top;
return new DrawerHeader(
decoration: new FlutterLogoDecoration(
margin: new EdgeInsets.fromLTRB(12.0, 12.0 + systemTopPadding, 12.0, 12.0),
style: _logoHasName ? _logoHorizontal ? FlutterLogoStyle.horizontal
: FlutterLogoStyle.stacked
: FlutterLogoStyle.markOnly,
swatch: _swatch,
),
duration: const Duration(milliseconds: 750),
child: new GestureDetector(
onLongPress: () {
setState(() {
_logoHorizontal = !_logoHorizontal;
if (!_logoHasName)
_logoHasName = true;
});
},
onTap: () {
setState(() {
_logoHasName = !_logoHasName;
});
},
onDoubleTap: () {
setState(() {
final List<Map<int, Color>> options = <Map<int, Color>>[];
if (_swatch != Colors.blue)
options.addAll(<Map<int, Color>>[Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue]);
if (_swatch != Colors.amber)
options.addAll(<Map<int, Color>>[Colors.amber, Colors.amber, Colors.amber]);
if (_swatch != Colors.red)
options.addAll(<Map<int, Color>>[Colors.red, Colors.red, Colors.red]);
if (_swatch != Colors.indigo)
options.addAll(<Map<int, Color>>[Colors.indigo, Colors.indigo, Colors.indigo]);
if (_swatch != Colors.pink)
options.addAll(<Map<int, Color>>[Colors.pink]);
if (_swatch != Colors.purple)
options.addAll(<Map<int, Color>>[Colors.purple]);
if (_swatch != Colors.cyan)
options.addAll(<Map<int, Color>>[Colors.cyan]);
_swatch = options[new math.Random().nextInt(options.length)];
});
}
)
);
}
}
class GalleryDrawer extends StatelessWidget {
GalleryDrawer({
Key key,
......@@ -41,24 +106,14 @@ class GalleryDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
TextStyle aboutTextStyle = themeData.textTheme.body2;
TextStyle aboutLinkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor);
final ThemeData themeData = Theme.of(context);
final TextStyle aboutTextStyle = themeData.textTheme.body2;
final TextStyle aboutLinkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor);
return new Drawer(
child: new Block(
children: <Widget>[
new DrawerHeader(
child: new Center(
child: new Padding(
padding: const EdgeInsets.all(16.0),
child: new Image.asset(
'packages/flutter_gallery_assets/drawer_logo.png',
fit: ImageFit.contain
)
)
)
),
new GalleryDrawerHeader(),
new DrawerItem(
icon: new Icon(Icons.brightness_5),
onPressed: () { onThemeChanged(true); },
......@@ -119,8 +174,9 @@ class GalleryDrawer extends StatelessWidget {
)
),
new AboutDrawerItem(
icon: new FlutterLogo(),
applicationVersion: '2016 Q3 Preview',
applicationIcon: new AssetImage('packages/flutter_gallery_assets/about_logo.png'),
applicationIcon: new FlutterLogo(),
applicationLegalese: '© 2016 The Chromium Authors',
aboutBoxChildren: <Widget>[
new Padding(
......
......@@ -37,6 +37,7 @@ export 'src/material/drop_down.dart';
export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart';
export 'src/material/floating_action_button.dart';
export 'src/material/flutter_logo.dart';
export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart';
export 'src/material/icon.dart';
......
......@@ -24,6 +24,7 @@ export 'src/painting/decoration.dart';
export 'src/painting/image_fit.dart';
export 'src/painting/edge_insets.dart';
export 'src/painting/fractional_offset.dart';
export 'src/painting/flutter_logo.dart';
export 'src/painting/text_editing.dart';
export 'src/painting/text_painter.dart';
export 'src/painting/text_span.dart';
......
......@@ -4,7 +4,6 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
......@@ -14,6 +13,8 @@ import 'dialog.dart';
import 'drawer_item.dart';
import 'flat_button.dart';
import 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'page.dart';
import 'progress_indicator.dart';
import 'scaffold.dart';
......@@ -84,9 +85,12 @@ class AboutDrawerItem extends StatelessWidget {
///
/// By default no icon is shown.
///
/// Typically this will be an [ImageIcon] widget. It should honor the
/// [IconTheme]'s [IconThemeData.size].
///
/// This is not necessarily the same as the icon shown on the drawer item
/// itself, which is controlled by the [icon] property.
final ImageProvider applicationIcon;
final Widget applicationIcon;
/// A string to show in small print in the [AboutDialog].
///
......@@ -137,7 +141,7 @@ void showAboutDialog({
@required BuildContext context,
String applicationName,
String applicationVersion,
ImageProvider applicationIcon,
Widget applicationIcon,
String applicationLegalese,
List<Widget> children
}) {
......@@ -168,7 +172,7 @@ void showLicensePage({
@required BuildContext context,
String applicationName,
String applicationVersion,
ImageProvider applicationIcon,
Widget applicationIcon,
String applicationLegalese
}) {
// TODO(ianh): remove pop once https://github.com/flutter/flutter/issues/4667 is fixed
......@@ -220,7 +224,10 @@ class AboutDialog extends StatelessWidget {
/// The icon to show next to the application name.
///
/// By default no icon is shown.
final ImageProvider applicationIcon;
///
/// Typically this will be an [ImageIcon] widget. It should honor the
/// [IconTheme]'s [IconThemeData.size].
final Widget applicationIcon;
/// A string to show in small print.
///
......@@ -241,15 +248,10 @@ class AboutDialog extends StatelessWidget {
Widget build(BuildContext context) {
final String name = applicationName ?? _defaultApplicationName(context);
final String version = applicationVersion ?? _defaultApplicationVersion(context);
final ImageProvider icon = applicationIcon ?? _defaultApplicationIcon(context);
final Widget icon = applicationIcon ?? _defaultApplicationIcon(context);
List<Widget> body = <Widget>[];
if (icon != null) {
body.add(new Image(
image: icon,
width: 48.0,
height: 48.0
));
}
if (icon != null)
body.add(new IconTheme(data: new IconThemeData(size: 48.0), child: icon));
body.add(new Flexible(
child: new Padding(
padding: new EdgeInsets.symmetric(horizontal: 24.0),
......@@ -454,7 +456,7 @@ String _defaultApplicationVersion(BuildContext context) {
return '';
}
ImageProvider _defaultApplicationIcon(BuildContext context) {
Widget _defaultApplicationIcon(BuildContext context) {
// TODO(ianh): Get this from the embedder somehow.
return null;
}
......@@ -10,8 +10,8 @@ import 'theme.dart';
const double _kDrawerHeaderHeight = 160.0 + 1.0; // bottom edge
/// The top-most region of a material design drawer. The header's [child]
/// widget is placed inside of a [Container] whose [decoration] can be passed as
/// an argument.
/// widget, if any, is placed inside a [Container] whose [decoration] can be
/// passed as an argument, inset by the given [padding].
///
/// Part of the material design [Drawer].
///
......@@ -20,9 +20,10 @@ const double _kDrawerHeaderHeight = 160.0 + 1.0; // bottom edge
/// See also:
///
/// * [Drawer]
/// * [UserAccountsDrawerHeader], a variant of [DrawerHeader] that is
/// specialized for showing user accounts.
/// * [DrawerItem]
/// * <https://www.google.com/design/spec/patterns/navigation-drawer.html>
class DrawerHeader extends StatelessWidget {
/// Creates a material design drawer header.
///
......@@ -30,15 +31,35 @@ class DrawerHeader extends StatelessWidget {
const DrawerHeader({
Key key,
this.decoration,
this.padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
this.duration: const Duration(milliseconds: 250),
this.curve: Curves.fastOutSlowIn,
this.child
}) : super(key: key);
/// Decoration for the main drawer header [Container]; useful for applying
/// backgrounds.
final BoxDecoration decoration;
///
/// This decoration will extend under the system status bar.
///
/// If this is changed, it will be animated according to [duration] and [curve].
final Decoration decoration;
/// The padding by which to inset [child].
///
/// The [DrawerHeader] additionally offsets the child by the height of the
/// system status bar.
///
/// If the child is null, the padding has no effect.
final EdgeInsets padding;
/// The duration for animations of the [decoration].
final Duration duration;
/// The curve for animations of the [decoration].
final Curve curve;
/// A widget that extends behind the system status bar and is placed inside a
/// [Container].
/// A widget to be placed inside the drawer header, inset by the [padding].
final Widget child;
@override
......@@ -56,15 +77,12 @@ class DrawerHeader extends StatelessWidget {
)
)
),
child: new Container(
padding: new EdgeInsets.only(
top: 16.0 + statusBarHeight,
left: 16.0,
right: 16.0,
bottom: 8.0
),
child: new AnimatedContainer(
padding: padding + new EdgeInsets.only(top: statusBarHeight),
decoration: decoration,
child: new DefaultTextStyle(
duration: duration,
curve: curve,
child: child == null ? null : new DefaultTextStyle(
style: Theme.of(context).textTheme.body2,
child: child
)
......
// Copyright 2016 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 'package:flutter/widgets.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'colors.dart';
/// The Flutter logo, in widget form. This widget respects the [IconTheme].
///
/// See also:
///
/// * [IconTheme], which provides ambient configuration for icons.
/// * [Icon], for showing icons the Material design icon library.
/// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
class FlutterLogo extends StatelessWidget {
/// Creates a widget that paints the Flutter logo.
///
/// The [size] and [color] default to the value given by the current [IconTheme].
const FlutterLogo({
Key key,
this.size,
this.swatch: Colors.blue,
this.style: FlutterLogoStyle.markOnly,
this.duration: const Duration(milliseconds: 750),
}) : super(key: key);
/// The size of the logo in logical pixels.
///
/// The logo will be fit into a square this size.
///
/// Defaults to the current [IconTheme] size, if any. If there is no
/// [IconTheme], or it does not specify an explicit size, then it defaults to
/// 24.0.
final double size;
/// The colors to use to paint the logo. This map should contain at least two
/// values, one for 400 and one for 900.
///
/// If possible, the default should be used. It corresponds to the
/// [Colors.blue] swatch.
///
/// If for some reason that color scheme is impractical, the [Colors.amber],
/// [Colors.red], or [Colors.indigo] swatches can be used. These are Flutter's
/// secondary colors.
///
/// In extreme cases where none of those four color schemes will work,
/// [Colors.pink], [Colors.purple], or [Colors.cyan] swatches can be used.
/// These are Flutter's tertiary colors.
final Map<int, Color> swatch;
/// Whether and where to draw the "Flutter" text. By default, only the logo
/// itself is drawn.
final FlutterLogoStyle style;
/// The length of time for the animation if the [style] or [swatch] properties
/// are changed.
final Duration duration;
@override
Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context).fallback();
final double iconSize = size ?? iconTheme.size;
return new AnimatedContainer(
width: iconSize,
height: iconSize,
duration: duration,
decoration: new FlutterLogoDecoration(
swatch: swatch,
style: style,
),
);
}
}
......@@ -22,9 +22,8 @@ import 'debug.dart';
/// See also:
///
/// * [Drawer]
/// * [DrawerItem]
/// * [DrawerHeader], for a drawer header that doesn't show user acounts
/// * <https://www.google.com/design/spec/patterns/navigation-drawer.html>
class UserAccountsDrawerHeader extends StatefulWidget {
/// Creates a material design drawer header.
///
......@@ -43,41 +42,37 @@ class UserAccountsDrawerHeader extends StatefulWidget {
/// section is pressed.
final VoidCallback onDetailsPressed;
/// Decoration for the main drawer header container useful for applying
/// backgrounds.
final BoxDecoration decoration;
/// The background to show in the drawer header.
final Decoration decoration;
/// A widget placed in the upper-left corner representing the current
/// account picture. Normally a [CircleAvatar].
final Widget currentAccountPicture;
/// A list of widgets that represent the user's accounts. Up to three of them
/// are arranged in a row in the header's upper-right corner. Normally a list
/// A list of widgets that represent the user's accounts. Up to three of will
/// be arranged in a row in the header's upper-right corner. Normally a list
/// of [CircleAvatar] widgets.
final List<Widget> otherAccountsPictures;
/// A widget placed on the top row of the account details representing
/// account name.
/// A widget placed on the top row of the account details representing the
/// account's name.
final Widget accountName;
/// A widget placed on the bottom row of the account details representing
/// account email.
/// A widget placed on the bottom row of the account details representing the
/// account's e-mail address.
final Widget accountEmail;
@override
_UserAccountsDrawerHeaderState createState() =>
new _UserAccountsDrawerHeaderState();
_UserAccountsDrawerHeaderState createState() => new _UserAccountsDrawerHeaderState();
}
class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
/// Saves whether the account dropdown is open or not.
bool isOpen = false;
bool _isOpen = false;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final List<Widget> otherAccountsPictures = config.otherAccountsPictures ??
<Widget>[];
final List<Widget> otherAccountsPictures = config.otherAccountsPictures ?? <Widget>[];
return new DrawerHeader(
decoration: config.decoration,
child: new Column(
......@@ -117,7 +112,7 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
child: new InkWell(
onTap: () {
setState(() {
isOpen = !isOpen;
_isOpen = !_isOpen;
});
if (config.onDetailsPressed != null)
config.onDetailsPressed();
......@@ -146,8 +141,7 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
child: new Align(
alignment: FractionalOffset.centerRight,
child: new Icon(
isOpen ? Icons.arrow_drop_up :
Icons.arrow_drop_down,
_isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
color: Colors.white
)
)
......
......@@ -114,6 +114,10 @@ abstract class BoxPainter {
/// However, when its size changes, it could continue using those
/// same instances, since the previous resources would no longer be
/// relevant and thus losing them would not be an issue.
///
/// Implementations should paint their decorations on the canvas in a
/// rectangle whose top left corner is at the given `offset` and whose size is
/// given by `configuration.size`.
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration);
/// Callback that is invoked if an asynchronously-loading resource used by the
......
......@@ -56,6 +56,22 @@ class EdgeInsets {
/// The offset from the bottom.
final double bottom;
/// An Offset describing the vector from the top left of a rectangle to the
/// top left of that rectangle inset by this object.
Offset get topLeft => new Offset(left, top);
/// An Offset describing the vector from the top right of a rectangle to the
/// top right of that rectangle inset by this object.
Offset get topRight => new Offset(-right, top);
/// An Offset describing the vector from the bottom left of a rectangle to the
/// bottom left of that rectangle inset by this object.
Offset get bottomLeft => new Offset(left, -bottom);
/// An Offset describing the vector from the bottom right of a rectangle to the
/// bottom right of that rectangle inset by this object.
Offset get bottomRight => new Offset(-right, -bottom);
/// Whether every dimension is non-negative.
bool get isNonNegative => left >= 0.0 && top >= 0.0 && right >= 0.0 && bottom >= 0.0;
......@@ -88,10 +104,57 @@ class EdgeInsets {
/// rect is moved left by [left], the top edge of the rect is moved up by
/// [top], the right edge of the rect is moved right by [right], and the
/// bottom edge of the rect is moved down by [bottom].
///
/// See also:
///
/// * [inflateSize], to inflate a [Size] rather than a [Rect].
/// * [deflateRect], to deflate a [Rect] rather than inflating it.
Rect inflateRect(Rect rect) {
return new Rect.fromLTRB(rect.left - left, rect.top - top, rect.right + right, rect.bottom + bottom);
}
/// Returns a new rect that is smaller than the given rect in each direction by
/// the amount of inset in each direction. Specifically, the left edge of the
/// rect is moved right by [left], the top edge of the rect is moved down by
/// [top], the right edge of the rect is moved left by [right], and the
/// bottom edge of the rect is moved up by [bottom].
///
/// If the argument's [Rect.size] is smaller than [collapsedSize], then the
/// resulting rectangle will have negative dimensions.
///
/// See also:
///
/// * [deflateSize], to deflate a [Size] rather than a [Rect].
/// * [inflateRect], to inflate a [Rect] rather than deflating it.
Rect deflateRect(Rect rect) {
return new Rect.fromLTRB(rect.left + left, rect.top + top, rect.right - right, rect.bottom - bottom);
}
/// Returns a new size that is bigger than the given size by the amount of
/// inset in the horizontal and vertical directions.
///
/// See also:
///
/// * [inflateRect], to inflate a [Rect] rather than a [Size].
/// * [deflateSize], to deflate a [Size] rather than inflating it.
Size inflateSize(Size size) {
return new Size(size.width + horizontal, size.height + vertical);
}
/// Returns a new size that is smaller than the given size by the amount of
/// inset in the horizontal and vertical directions.
///
/// If the argument is smaller than [collapsedSize], then the resulting size
/// will have negative dimensions.
///
/// See also:
///
/// * [deflateRect], to deflate a [Rect] rather than a [Size].
/// * [inflateSize], to inflate a [Size] rather than deflating it.
Size deflateSize(Size size) {
return new Size(size.width - horizontal, size.height - vertical);
}
/// Returns the difference between two EdgeInsets.
EdgeInsets operator -(EdgeInsets other) {
return new EdgeInsets.fromLTRB(
......
This diff is collapsed.
......@@ -1168,7 +1168,27 @@ class RenderDecoratedBox extends RenderProxyBox {
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
int debugSaveCount;
assert(() {
debugSaveCount = context.canvas.getSaveCount();
return true;
});
_painter.paint(context.canvas, offset, filledConfiguration);
assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) {
throw new FlutterError(
'${_decoration.runtimeType} painter had mismatching save and restore calls.\n'
'Before painting the decoration, the canvas save count was $debugSaveCount. '
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'Every call to save() or saveLayer() must be matched by a call to restore().\n'
'The decoration was:\n'
'${decoration.toString(" ")}\n'
'The painter was:\n'
' $_painter'
);
}
return true;
});
if (decoration.isComplex)
context.setIsComplexHint();
}
......
......@@ -21,7 +21,7 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si
return new ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
// TODO(ianh): provide the locale,
// TODO(ianh): provide the locale
size: size,
platform: Platform.operatingSystem
);
......
......@@ -215,6 +215,8 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
/// different parameters to [Container]. For more complex animations, you'll
/// likely want to use a subclass of [Transition] or use an
/// [AnimationController] yourself.
///
/// Properties that are null are not animated.
class AnimatedContainer extends ImplicitlyAnimatedWidget {
/// Creates a container that animates its parameters implicitly.
///
......
......@@ -7,7 +7,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('UserAccuntsDrawerHeader test', (WidgetTester tester) async {
testWidgets('UserAccountsDrawerHeader test', (WidgetTester tester) async {
final Key avatarA = new Key('A');
final Key avatarC = new Key('C');
final Key avatarD = new Key('D');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment