From 0ff3df83eee823035decd15b45a1d21915132347 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 25 Jun 2015 14:25:33 -0700 Subject: [PATCH] Version 0 of TabLabel, Tab, and TabBar components There's is no support for animating the selected tab indicator, there isn't a TabNavigator container yet, overflow layout (tabs don't fit) isn't supported yet, etc. R=abarth@chromium.org, ianh@google.com Review URL: https://codereview.chromium.org/1205953002. --- examples/widgets/tabs.dart | 62 +++++++++ sdk/BUILD.gn | 1 + sdk/lib/rendering/block.dart | 9 +- sdk/lib/rendering/box.dart | 4 + sdk/lib/widgets/tabs.dart | 262 +++++++++++++++++++++++++++++++++++ 5 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 examples/widgets/tabs.dart create mode 100644 sdk/lib/widgets/tabs.dart diff --git a/examples/widgets/tabs.dart b/examples/widgets/tabs.dart new file mode 100644 index 00000000000..cb711845a74 --- /dev/null +++ b/examples/widgets/tabs.dart @@ -0,0 +1,62 @@ +// 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:sky/theme/colors.dart'; +import 'package:sky/theme/typography.dart'; +import 'package:sky/widgets/basic.dart'; +import 'package:sky/widgets/material.dart'; +import 'package:sky/widgets/scaffold.dart'; +import 'package:sky/widgets/tabs.dart'; +import 'package:sky/widgets/tool_bar.dart'; +import 'package:sky/widgets/widget.dart'; + +class TabbedNavigatorApp extends App { + static Iterable items = const ["ONE", "TWO", "FREE", "FOUR"]; + final List navigatorSelections = new List.filled(items.length, 0); + + Widget buildTabNavigator(Iterable labels, int navigatorIndex) { + TabBar tabBar = new TabBar( + labels: labels.toList(), + selectedIndex: navigatorSelections[navigatorIndex], + onChanged: (selectionIndex) { + setState(() { + navigatorSelections[navigatorIndex] = selectionIndex; + }); + } + ); + + return new Container(child: tabBar, margin: new EdgeDims.only(bottom: 16.0)); + } + + Widget build() { + Iterable textLabels = items + .map((s) => new TabLabel(text: "ITEM " + s)); + + Iterable iconLabels = items + .map((s) => new TabLabel(icon: 'action/search_white')); + + Iterable textAndIconLabels = items + .map((s) => new TabLabel(text: "ITEM " + s, icon: 'action/search_white')); + + var navigatorIndex = 0; + Iterable tabNavigators = [textLabels, iconLabels, textAndIconLabels] + .map((labels) => buildTabNavigator(labels, navigatorIndex++)); + + ToolBar toolbar = new ToolBar( + center: new Text('Tabbed Navigator', style: white.title), + backgroundColor: Blue[500]); + + return new Scaffold( + toolbar: toolbar, + body: new Material( + child: new Center(child: new Block(tabNavigators.toList())), + color: Grey[500] + ) + ); + } +} + +void main() { + runApp(new TabbedNavigatorApp()); +} diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index 61594c69357..c067360c3dc 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -119,6 +119,7 @@ dart_pkg("sdk") { "lib/widgets/scaffold.dart", "lib/widgets/scrollable.dart", "lib/widgets/switch.dart", + "lib/widgets/tabs.dart", "lib/widgets/theme.dart", "lib/widgets/toggleable.dart", "lib/widgets/tool_bar.dart", diff --git a/sdk/lib/rendering/block.dart b/sdk/lib/rendering/block.dart index 90373caf3f9..3ab31711dfe 100644 --- a/sdk/lib/rendering/block.dart +++ b/sdk/lib/rendering/block.dart @@ -28,11 +28,11 @@ class RenderBlock extends RenderBox with ContainerRenderObjectMixin new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth); + + BoxConstraints heightConstraints() => new BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); + final double minWidth; final double maxWidth; final double minHeight; diff --git a/sdk/lib/widgets/tabs.dart b/sdk/lib/widgets/tabs.dart new file mode 100644 index 00000000000..198e1b4818b --- /dev/null +++ b/sdk/lib/widgets/tabs.dart @@ -0,0 +1,262 @@ +// 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 'dart:sky' as sky; + +import 'package:sky/painting/text_style.dart'; +import 'package:sky/rendering/box.dart'; +import 'package:sky/rendering/object.dart'; +import 'package:sky/theme/colors.dart'; +import 'package:sky/widgets/basic.dart'; +import 'package:sky/widgets/icon.dart'; +import 'package:sky/widgets/ink_well.dart'; +import 'package:sky/widgets/widget.dart'; + +typedef void SelectedIndexChanged(int selectedIndex); + +const double _kTabHeight = 46.0; +const double _kTabIndicatorHeight = 2.0; +const double _kTabBarHeight = _kTabHeight + _kTabIndicatorHeight; +const double _kMinTabWidth = 72.0; + +class TabBarParentData extends BoxParentData with + ContainerParentDataMixin { } + +class RenderTabBar extends RenderBox with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + + int _selectedIndex; + int get selectedIndex => _selectedIndex; + void set selectedIndex(int value) { + if (_selectedIndex != value) { + _selectedIndex = value; + markNeedsPaint(); + } + } + + void setParentData(RenderBox child) { + if (child.parentData is! TabBarParentData) + child.parentData = new TabBarParentData(); + } + + double getMinIntrinsicWidth(BoxConstraints constraints) { + BoxConstraints widthConstraints = + new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight); + double maxWidth = 0.0; + int childCount = 0; + RenderBox child = firstChild; + while (child != null) { + maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints)); + ++childCount; + assert(child.parentData is TabBarParentData); + child = child.parentData.nextSibling; + } + return constraints.constrainWidth(maxWidth * childCount); + } + + double getMaxIntrinsicWidth(BoxConstraints constraints) { + BoxConstraints widthConstraints = + new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight); + double maxWidth = 0.0; + int childCount = 0; + RenderBox child = firstChild; + while (child != null) { + maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints)); + ++childCount; + assert(child.parentData is TabBarParentData); + child = child.parentData.nextSibling; + } + return constraints.constrainWidth(maxWidth * childCount); + } + + double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrainHeight(_kTabBarHeight); + + double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); + + double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints); + + // TODO(hansmuller): track this value in the parent rather than computing it. + int _childCount() { + int childCount = 0; + RenderBox child = firstChild; + while (child != null) { + ++childCount; + assert(child.parentData is TabBarParentData); + child = child.parentData.nextSibling; + } + return childCount; + } + + void performLayout() { + assert(constraints is BoxConstraints); + + size = constraints.constrain(new Size(constraints.maxWidth, _kTabBarHeight)); + assert(size.width < double.INFINITY); + assert(size.height < double.INFINITY); + + int childCount = _childCount(); + if (childCount == 0) + return; + + double tabWidth = size.width / childCount; + BoxConstraints tabConstraints = + new BoxConstraints.tightFor(width: tabWidth, height: size.height); + double x = 0.0; + RenderBox child = firstChild; + while (child != null) { + child.layout(tabConstraints); + assert(child.parentData is TabBarParentData); + child.parentData.position = new Point(x, 0.0); + x += tabWidth; + child = child.parentData.nextSibling; + } + } + + void hitTestChildren(HitTestResult result, { Point position }) { + defaultHitTestChildren(result, position: position); + } + + void _paintIndicator(RenderCanvas canvas, RenderBox selectedTab) { + var size = new Size(selectedTab.size.width, _kTabIndicatorHeight); + var point = new Point(selectedTab.parentData.position.x, _kTabHeight); + Rect rect = new Rect.fromPointAndSize(point, size); + // TODO(hansmuller): indicator color should be based on the theme. + canvas.drawRect(rect, new Paint()..color = White); + } + + void paint(RenderCanvas canvas) { + Rect rect = new Rect.fromSize(size); + canvas.drawRect(rect, new Paint()..color = Blue[500]); + + int index = 0; + RenderBox child = firstChild; + while (child != null) { + assert(child.parentData is TabBarParentData); + canvas.paintChild(child, child.parentData.position); + if (index++ == selectedIndex) + _paintIndicator(canvas, child); + child = child.parentData.nextSibling; + } + } +} + +class TabBarWrapper extends MultiChildRenderObjectWrapper { + TabBarWrapper(List children, this.selectedIndex, { String key }) + : super(key: key, children: children); + + final int selectedIndex; + + RenderTabBar get root => super.root; + RenderTabBar createNode() => new RenderTabBar(); + + void syncRenderObject(Widget old) { + super.syncRenderObject(old); + root.selectedIndex = selectedIndex; + } +} + +class TabLabel { + const TabLabel({ this.text, this.icon }); + + final String text; + final String icon; +} + +class Tab extends Component { + Tab({ + String key, + this.label, + this.selected: false + }) : super(key: key) { + assert(label.text != null || label.icon != null); + } + + final TabLabel label; + final bool selected; + + // TODO(hansmuller): use themes here. + static const TextStyle selectedStyle = const TextStyle(color: const Color(0xFFFFFFFF)); + static const TextStyle style = const TextStyle(color: const Color(0xB2FFFFFF)); + + Widget _buildLabelText() { + assert(label.text != null); + return new Text(label.text, style: style); + } + + Widget _buildLabelIcon() { + assert(label.icon != null); + return new Icon(type: label.icon, size: 24); + } + + Widget build() { + Widget labelContents; + if (label.icon == null) { + labelContents = _buildLabelText(); + } else if (label.text == null) { + labelContents = _buildLabelIcon(); + } else { + labelContents = new Flex( + [_buildLabelText(), _buildLabelIcon()], + justifyContent: FlexJustifyContent.center, + alignItems: FlexAlignItems.center, + direction: FlexDirection.vertical + ); + } + + Widget highlightedLabel = new Opacity( + child: labelContents, + opacity: selected ? 1.0 : 0.7 + ); + + Container centeredLabel = new Container( + child: new Center(child: highlightedLabel), + constraints: new BoxConstraints(minWidth: _kMinTabWidth) + ); + + return new InkWell(child: centeredLabel); + } +} + +class TabBar extends Component { + TabBar({ + String key, + this.labels, + this.selectedIndex: 0, + this.onChanged + }) : super(key: key); + + final List labels; + final int selectedIndex; + final SelectedIndexChanged onChanged; + + void _handleTap(int tabIndex) { + if (tabIndex != selectedIndex && onChanged != null) + onChanged(tabIndex); + } + + Widget _toTab(TabLabel label, int tabIndex) { + Tab tab = new Tab( + label: label, + selected: tabIndex == selectedIndex, + key: label.text == null ? label.icon : label.text + ); + return new Listener( + child: tab, + onGestureTap: (_) => _handleTap(tabIndex) + ); + } + + Widget build() { + assert(labels != null && labels.isNotEmpty); + List tabs = []; + for (int tabIndex = 0; tabIndex < labels.length; tabIndex++) { + tabs.add(_toTab(labels[tabIndex], tabIndex)); + } + return new TabBarWrapper(tabs, selectedIndex); + } +} + +