Merge pull request #1627 from abarth/material_list

Add a MaterialList
diff --git a/sky/packages/sky/lib/material.dart b/sky/packages/sky/lib/material.dart
index 51bcfaf..40c668c 100644
--- a/sky/packages/sky/lib/material.dart
+++ b/sky/packages/sky/lib/material.dart
@@ -9,6 +9,7 @@
 
 export 'src/material/card.dart';
 export 'src/material/checkbox.dart';
+export 'src/material/circle_avatar.dart';
 export 'src/material/colors.dart';
 export 'src/material/constants.dart';
 export 'src/material/date_picker.dart';
@@ -20,14 +21,17 @@
 export 'src/material/edges.dart';
 export 'src/material/flat_button.dart';
 export 'src/material/floating_action_button.dart';
-export 'src/material/icon_button.dart';
 export 'src/material/icon.dart';
+export 'src/material/icon_button.dart';
+export 'src/material/icon_theme.dart';
+export 'src/material/icon_theme_data.dart';
 export 'src/material/ink_well.dart';
 export 'src/material/input.dart';
 export 'src/material/list_item.dart';
 export 'src/material/material.dart';
 export 'src/material/material_app.dart';
 export 'src/material/material_button.dart';
+export 'src/material/material_list.dart';
 export 'src/material/popup_menu_item.dart';
 export 'src/material/popup_menu.dart';
 export 'src/material/progress_indicator.dart';
diff --git a/sky/packages/sky/lib/src/material/circle_avatar.dart b/sky/packages/sky/lib/src/material/circle_avatar.dart
new file mode 100644
index 0000000..7895b87
--- /dev/null
+++ b/sky/packages/sky/lib/src/material/circle_avatar.dart
@@ -0,0 +1,47 @@
+// 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 'package:flutter/painting.dart';
+import 'package:flutter/widgets.dart';
+
+import 'constants.dart';
+import 'theme.dart';
+import 'typography.dart';
+
+class CircleAvatar extends StatelessComponent {
+  CircleAvatar({
+    Key key,
+    this.label,
+    this.backgroundColor,
+    this.textTheme
+  }) : super(key: key);
+
+  final String label;
+  final Color backgroundColor;
+  final TextTheme textTheme;
+
+  Widget build(BuildContext context) {
+    Color color = backgroundColor;
+    TextStyle style = textTheme?.title;
+
+    if (color == null || style == null) {
+      ThemeData themeData = Theme.of(context);
+      color ??= themeData.primaryColor;
+      style ??= themeData.primaryTextTheme.title;
+    }
+
+    return new AnimatedContainer(
+      duration: kThemeChangeDuration,
+      decoration: new BoxDecoration(
+        backgroundColor: color,
+        shape: Shape.circle
+      ),
+      width: 40.0,
+      height: 40.0,
+      child: new Center(
+        child: new Text(label, style: style)
+      )
+    );
+  }
+}
diff --git a/sky/packages/sky/lib/src/material/constants.dart b/sky/packages/sky/lib/src/material/constants.dart
index 9b50a30..6824793 100644
--- a/sky/packages/sky/lib/src/material/constants.dart
+++ b/sky/packages/sky/lib/src/material/constants.dart
@@ -19,7 +19,11 @@
 // https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
 const double kListTitleHeight = 72.0;
 const double kListSubtitleHeight = 48.0;
-const double kListItemHeight = 72.0;
+
+const double kOneLineListItemHeight = 48.0;
+const double kOneLineListItemWithAvatarHeight = 56.0;
+const double kTwoLineListItemHeight = 72.0;
+const double kThreeLineListItemHeight = 88.0;
 
 const double kMaterialDrawerHeight = 140.0;
 const double kScrollbarSize = 10.0;
diff --git a/sky/packages/sky/lib/src/material/date_picker.dart b/sky/packages/sky/lib/src/material/date_picker.dart
index e9d5759..c2bdc57 100644
--- a/sky/packages/sky/lib/src/material/date_picker.dart
+++ b/sky/packages/sky/lib/src/material/date_picker.dart
@@ -122,17 +122,15 @@
 
   Widget build(BuildContext context) {
     ThemeData theme = Theme.of(context);
-    TextTheme headerTheme;
+    TextTheme headerTheme = theme.primaryTextTheme;
     Color dayColor;
     Color yearColor;
     switch(theme.primaryColorBrightness) {
       case ThemeBrightness.light:
-        headerTheme = Typography.black;
         dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54;
         yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54;
         break;
       case ThemeBrightness.dark:
-        headerTheme = Typography.white;
         dayColor = mode == DatePickerMode.day ? Colors.white : Colors.white70;
         yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70;
         break;
diff --git a/sky/packages/sky/lib/src/material/floating_action_button.dart b/sky/packages/sky/lib/src/material/floating_action_button.dart
index b7883a7..1a6d7d3 100644
--- a/sky/packages/sky/lib/src/material/floating_action_button.dart
+++ b/sky/packages/sky/lib/src/material/floating_action_button.dart
@@ -4,7 +4,8 @@
 
 import 'package:flutter/widgets.dart';
 
-import 'icon.dart';
+import 'icon_theme.dart';
+import 'icon_theme_data.dart';
 import 'ink_well.dart';
 import 'material.dart';
 import 'theme.dart';
diff --git a/sky/packages/sky/lib/src/material/icon.dart b/sky/packages/sky/lib/src/material/icon.dart
index 46b46ab..0257564 100644
--- a/sky/packages/sky/lib/src/material/icon.dart
+++ b/sky/packages/sky/lib/src/material/icon.dart
@@ -8,50 +8,8 @@
 import 'package:flutter/widgets.dart';
 
 import 'theme.dart';
-
-enum IconThemeColor { white, black }
-
-class IconThemeData {
-  const IconThemeData({ this.color });
-  final IconThemeColor color;
-
-  bool operator ==(dynamic other) {
-    if (other is! IconThemeData)
-      return false;
-    final IconThemeData typedOther = other;
-    return color == typedOther;
-  }
-
-  int get hashCode => color.hashCode;
-
-  String toString() => '$color';
-}
-
-class IconTheme extends InheritedWidget {
-
-  IconTheme({
-    Key key,
-    this.data,
-    Widget child
-  }) : super(key: key, child: child) {
-    assert(data != null);
-    assert(child != null);
-  }
-
-  final IconThemeData data;
-
-  static IconThemeData of(BuildContext context) {
-    IconTheme result = context.inheritedWidgetOfType(IconTheme);
-    return result?.data;
-  }
-
-  bool updateShouldNotify(IconTheme old) => data != old.data;
-
-  void debugFillDescription(List<String> description) {
-    super.debugFillDescription(description);
-    description.add('$data');
-  }
-}
+import 'icon_theme.dart';
+import 'icon_theme_data.dart';
 
 AssetBundle _initIconBundle() {
   if (rootBundle != null)
diff --git a/sky/packages/sky/lib/src/material/icon_button.dart b/sky/packages/sky/lib/src/material/icon_button.dart
index 4ea19a3..f7dd289 100644
--- a/sky/packages/sky/lib/src/material/icon_button.dart
+++ b/sky/packages/sky/lib/src/material/icon_button.dart
@@ -7,6 +7,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'icon.dart';
+import 'icon_theme_data.dart';
 
 class IconButton extends StatelessComponent {
   const IconButton({
diff --git a/sky/packages/sky/lib/src/material/icon_theme.dart b/sky/packages/sky/lib/src/material/icon_theme.dart
new file mode 100644
index 0000000..38723ff
--- /dev/null
+++ b/sky/packages/sky/lib/src/material/icon_theme.dart
@@ -0,0 +1,32 @@
+// 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 'package:flutter/widgets.dart';
+
+import 'icon_theme_data.dart';
+
+class IconTheme extends InheritedWidget {
+  IconTheme({
+    Key key,
+    this.data,
+    Widget child
+  }) : super(key: key, child: child) {
+    assert(data != null);
+    assert(child != null);
+  }
+
+  final IconThemeData data;
+
+  static IconThemeData of(BuildContext context) {
+    IconTheme result = context.inheritedWidgetOfType(IconTheme);
+    return result?.data;
+  }
+
+  bool updateShouldNotify(IconTheme old) => data != old.data;
+
+  void debugFillDescription(List<String> description) {
+  super.debugFillDescription(description);
+    description.add('$data');
+  }
+}
diff --git a/sky/packages/sky/lib/src/material/icon_theme_data.dart b/sky/packages/sky/lib/src/material/icon_theme_data.dart
new file mode 100644
index 0000000..9ba71ab
--- /dev/null
+++ b/sky/packages/sky/lib/src/material/icon_theme_data.dart
@@ -0,0 +1,21 @@
+// 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.
+
+enum IconThemeColor { white, black }
+
+class IconThemeData {
+  const IconThemeData({ this.color });
+  final IconThemeColor color;
+
+  bool operator ==(dynamic other) {
+    if (other is! IconThemeData)
+      return false;
+    final IconThemeData typedOther = other;
+    return color == typedOther;
+  }
+
+  int get hashCode => color.hashCode;
+
+  String toString() => '$color';
+}
diff --git a/sky/packages/sky/lib/src/material/list_item.dart b/sky/packages/sky/lib/src/material/list_item.dart
index 9f59a7a..0352f30 100644
--- a/sky/packages/sky/lib/src/material/list_item.dart
+++ b/sky/packages/sky/lib/src/material/list_item.dart
@@ -5,7 +5,6 @@
 import 'package:flutter/widgets.dart';
 
 import 'ink_well.dart';
-import 'constants.dart';
 
 class ListItem extends StatelessComponent {
   ListItem({
@@ -42,14 +41,12 @@
 
     if (right != null) {
       children.add(new Container(
-        margin: new EdgeDims.only(left: 8.0),
-        width: 40.0,
+        margin: new EdgeDims.only(left: 16.0),
         child: right
       ));
     }
 
-    return new Container(
-      height: kListItemHeight,
+    return new Padding(
       padding: const EdgeDims.symmetric(horizontal: 16.0),
       child: new InkWell(
         onTap: onTap,
diff --git a/sky/packages/sky/lib/src/material/material_list.dart b/sky/packages/sky/lib/src/material/material_list.dart
new file mode 100644
index 0000000..105a8b7
--- /dev/null
+++ b/sky/packages/sky/lib/src/material/material_list.dart
@@ -0,0 +1,64 @@
+// 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 'package:flutter/widgets.dart';
+
+import 'constants.dart';
+import 'scrollbar_painter.dart';
+
+enum MaterialListType {
+  oneLine,
+  oneLineWithAvatar,
+  twoLine,
+  threeLine
+}
+
+Map<MaterialListType, double> _kItemExtent = const <MaterialListType, double>{
+  MaterialListType.oneLine: kOneLineListItemHeight,
+  MaterialListType.oneLineWithAvatar: kOneLineListItemWithAvatarHeight,
+  MaterialListType.twoLine: kTwoLineListItemHeight,
+  MaterialListType.threeLine: kThreeLineListItemHeight,
+};
+
+class MaterialList<T> extends StatefulComponent {
+  MaterialList({
+    Key key,
+    this.initialScrollOffset,
+    this.onScroll,
+    this.items,
+    this.itemBuilder,
+    this.type: MaterialListType.twoLine
+  }) : super(key: key);
+
+  final double initialScrollOffset;
+  final ScrollListener onScroll;
+  final List<T> items;
+  final ItemBuilder<T> itemBuilder;
+  final MaterialListType type;
+
+  _MaterialListState<T> createState() => new _MaterialListState<T>();
+}
+
+class _MaterialListState<T> extends State<MaterialList<T>> {
+
+  void initState() {
+    super.initState();
+    _scrollbarPainter = new ScrollbarPainter();
+  }
+
+  ScrollbarPainter _scrollbarPainter;
+
+  Widget build(BuildContext context) {
+    return new ScrollableList<T>(
+      initialScrollOffset: config.initialScrollOffset,
+      scrollDirection: ScrollDirection.vertical,
+      onScroll: config.onScroll,
+      items: config.items,
+      itemBuilder: config.itemBuilder,
+      itemExtent: _kItemExtent[config.type],
+      padding: const EdgeDims.symmetric(vertical: 8.0),
+      scrollableListPainter: _scrollbarPainter
+    );
+  }
+}
diff --git a/sky/packages/sky/lib/src/material/tabs.dart b/sky/packages/sky/lib/src/material/tabs.dart
index 3a9d40e..798f336 100644
--- a/sky/packages/sky/lib/src/material/tabs.dart
+++ b/sky/packages/sky/lib/src/material/tabs.dart
@@ -13,9 +13,10 @@
 import 'colors.dart';
 import 'constants.dart';
 import 'icon.dart';
+import 'icon_theme.dart';
+import 'icon_theme_data.dart';
 import 'ink_well.dart';
 import 'theme.dart';
-import 'typography.dart';
 
 typedef void TabSelectedIndexChanged(int selectedIndex);
 typedef void TabLayoutChanged(Size size, List<double> widths);
@@ -509,18 +510,8 @@
       indicatorColor = Colors.white;
     }
 
-    TextStyle textStyle;
-    IconThemeColor iconThemeColor;
-    switch (themeData.primaryColorBrightness) {
-      case ThemeBrightness.light:
-        textStyle = Typography.black.body1;
-        iconThemeColor = IconThemeColor.black;
-        break;
-      case ThemeBrightness.dark:
-        textStyle = Typography.white.body1;
-        iconThemeColor = IconThemeColor.white;
-        break;
-    }
+    TextStyle textStyle = themeData.primaryTextTheme.body1;
+    IconThemeData iconTheme = themeData.primaryIconTheme;
 
     List<Widget> tabs = <Widget>[];
     bool textAndIcons = false;
@@ -532,7 +523,7 @@
     }
 
     Widget content = new IconTheme(
-      data: new IconThemeData(color: iconThemeColor),
+      data: iconTheme,
       child: new DefaultTextStyle(
         style: textStyle,
         child: new BuilderTransition(
diff --git a/sky/packages/sky/lib/src/material/theme_data.dart b/sky/packages/sky/lib/src/material/theme_data.dart
index 3b789d0..8e79255 100644
--- a/sky/packages/sky/lib/src/material/theme_data.dart
+++ b/sky/packages/sky/lib/src/material/theme_data.dart
@@ -4,8 +4,9 @@
 
 import 'dart:ui' show Color;
 
-import 'typography.dart';
 import 'colors.dart';
+import 'icon_theme_data.dart';
+import 'typography.dart';
 
 enum ThemeBrightness { dark, light }
 
@@ -82,6 +83,19 @@
   /// icons placed on top of the primary color (e.g. toolbar text).
   final ThemeBrightness primaryColorBrightness;
 
+  /// A text theme that contrasts with the primary color.
+  TextTheme get primaryTextTheme {
+    if (primaryColorBrightness == ThemeBrightness.dark)
+      return Typography.white;
+    return Typography.black;
+  }
+
+  IconThemeData get primaryIconTheme {
+    if (primaryColorBrightness == ThemeBrightness.dark)
+      return const IconThemeData(color: IconThemeColor.white);
+    return const IconThemeData(color: IconThemeColor.black);
+  }
+
   /// The foreground color for widgets (knobs, text, etc)
   Color get accentColor => _accentColor;
   Color _accentColor;
diff --git a/sky/packages/sky/lib/src/material/tool_bar.dart b/sky/packages/sky/lib/src/material/tool_bar.dart
index b2f5daa..c4ac519 100644
--- a/sky/packages/sky/lib/src/material/tool_bar.dart
+++ b/sky/packages/sky/lib/src/material/tool_bar.dart
@@ -5,7 +5,8 @@
 import 'package:flutter/widgets.dart';
 
 import 'constants.dart';
-import 'icon.dart';
+import 'icon_theme.dart';
+import 'icon_theme_data.dart';
 import 'shadows.dart';
 import 'theme.dart';
 import 'typography.dart';
@@ -17,7 +18,8 @@
     this.center,
     this.right,
     this.level: 2,
-    this.backgroundColor
+    this.backgroundColor,
+    this.textTheme
   }) : super(key: key);
 
   final Widget left;
@@ -25,22 +27,22 @@
   final List<Widget> right;
   final int level;
   final Color backgroundColor;
+  final TextTheme textTheme;
 
   Widget build(BuildContext context) {
     Color color = backgroundColor;
     IconThemeData iconThemeData;
-    TextStyle centerStyle = Typography.white.title;
-    TextStyle sideStyle = Typography.white.body1;
-    if (color == null) {
+    TextStyle centerStyle = textTheme?.title;
+    TextStyle sideStyle = textTheme?.body1;
+
+    if (color == null || iconThemeData == null || textTheme == null) {
       ThemeData themeData = Theme.of(context);
-      color = themeData.primaryColor;
-      if (themeData.primaryColorBrightness == ThemeBrightness.light) {
-        centerStyle = Typography.black.title;
-        sideStyle = Typography.black.body2;
-        iconThemeData = const IconThemeData(color: IconThemeColor.black);
-      } else {
-        iconThemeData = const IconThemeData(color: IconThemeColor.white);
-      }
+      color ??= themeData.primaryColor;
+      iconThemeData ??= themeData.primaryIconTheme;
+
+      TextTheme primaryTextTheme = themeData.primaryTextTheme;
+      centerStyle ??= primaryTextTheme.title;
+      sideStyle ??= primaryTextTheme.body2;
     }
 
     List<Widget> children = new List<Widget>();