Skip to content

Generalized TabBar selected tab indicator #14741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 21, 2018

Conversation

HansMuller
Copy link
Contributor

@HansMuller HansMuller commented Feb 16, 2018

A TabBar's selected tab indicator can now be defined with a Decoration, using indicator. The new UnderlineTabIndicator (a Decoration) defines the default underline-style selected tab indicator.

The selected tab indicator's width can be defined by the overall width of the tab, with indicatorSize: TabBarIndicatorSize.tab, or just the width of the tab's label with indicatorSize: TabBarIndicatorSize.label.

Here are some examples that use indicator and indicatorSize.

indicator: new ShapeDecoration(
  shape: new RoundedRectangleBorder(
    borderRadius: new BorderRadius.circular(8.0),
    side: new BorderSide(
      color: Colors.white,
      width: 2.0,
    ),
  ) + // Inset the white border by 6.0
  new RoundedRectangleBorder(
    side: new BorderSide(
      color: Colors.transparent,
      width: 6.0,
    ),
  ),
),

custom_indicator1

This example sets indicatorSize and configures UnderlineIndicatorShape's insets:

indicatorSize: TabBarIndicatorSize.label,
indicator: const UnderlineTabIndicator(
  insets: const EdgeInsets.only(bottom: 12.0),
),

custom_indicator2

This PR includes an incompatible change: the Tab Widget's width is no longer padded horizontally. It's height is still fixed depending on what it contains (text or icon or both) and it's still centered vertically. TabBar still horizontally centers tab widgets within the space they're allocated.

Tests that assume that Tab's widgets will expand to fill the available width, for example tests that check TabBar Tab bounds, will have to be updated.

This change was made to enable computing a tab's intrinsic width for TabIndicatorSize.label. It should only affect apps that used Tab widgets outside of the TabBar or tests that check Tab bounds.

/// tab's boundary in terms of its (centered) widget, [TabIndicatorSize.label],
/// or the the entire tab, [TabIndicatorSize.tab].
class DefaultTabIndicator extends ShapeBorder {
/// Create a underline style tab indicator.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an

/// The [TabBar.indicatorSize] property can be used to define the
/// tab's boundary in terms of its (centered) widget, [TabIndicatorSize.label],
/// or the the entire tab, [TabIndicatorSize.tab].
class DefaultTabIndicator extends ShapeBorder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would call this an UnderlineTabIndicator, so that if we ever want to change the default we're not stuck with a weird class name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

///
/// The [TabBar.indicatorSize] property can be used to define the
/// tab's boundary in terms of its (centered) widget, [TabIndicatorSize.label],
/// or the the entire tab, [TabIndicatorSize.tab].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraneous double space


@override
EdgeInsetsGeometry get dimensions {
return padding.subtract(new EdgeInsets.only(bottom: borderSide.width));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this normally mean it returns negative values?

I'm not sure what this is trying to do.

assert(textDirection != null);
final Rect indicator = padding.resolve(textDirection).deflateRect(rect);
return new Rect.fromLTWH(
indicator.left,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indent by 2, not 4


@override
bool operator ==(dynamic other) {
if (identical(this, other))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can drop the identical check, the rest of the method is cheap enough

this.padding: EdgeInsets.zero,
}) : assert(borderSide != null), assert(padding != null);

/// The color and weight of the line drawn below the selected tab.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and elsewhere in this class: consider reframing the documentation to describe this just as a shape, rather than as an indicator-specific feature.

@Hixie
Copy link
Contributor

Hixie commented Feb 16, 2018

I know I told you to go down the ShapeBorder route, but in looking at this I think actually the Decoration route would be better.

Don't worry, ShapeBorder and Decoration are basically the same thing. But Decoration fits your use case better (e.g. it doesn't have opinions about inner and outer paths). ShapeBorder would make more sense if this was going to actually clip something. Decoration is more about just drawing a box or whatever, which is what you're doing here.

General approach looks good.

@HansMuller
Copy link
Contributor Author

TabBar's indicator (was indicatorShape) is now a Decoration.

@HansMuller HansMuller changed the title Generalized TabBar selected tab indicator [WIP] Generalized TabBar selected tab indicator Feb 20, 2018
import 'colors.dart';

/// Used with [TabBar.indicator] to draw a horizontal line below the
/// selected tab
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: period

/// defines the line's color and weight.
///
/// The [TabBar.indicatorSize] property can be used to define the
/// tab's boundary in terms of its (centered) widget, [TabIndicatorSize.label],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we say "the indicator's bounding rectangle" instead of "the tab's boundary"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

///
/// The [TabBar.indicatorSize] property can be used to define the
/// tab's boundary in terms of its (centered) widget, [TabIndicatorSize.label],
/// or the the entire tab, [TabIndicatorSize.tab].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra the

/// The color and weight of the line drawn below the selected tab.
final BorderSide borderSide;

/// Locates the [borderSide] style selected tab underline relative to the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this sentence, maybe (as you suggested) omit the "[borderSide] style selected tab" part?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

/// the tab widget itself.
final Decoration indicator;

/// The location of the selected tab indicator is defined relative to the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might make it easier to read if we start with a summary sentence. (maybe something like "Defines the sizing behavior of the tab indicator"?)

/// tab.
tab,

/// The tab's bounds are only as wide as (centered) tab widget itself.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ...the (centered) tab widget

@@ -488,14 +529,16 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicatorColor,
this.indicatorWeight: 2.0,
this.indicatorPadding: EdgeInsets.zero,
this.indicator, // TBD indicator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we say something about the nullability of this in the constructor's doc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added a sentence about indicatorColor et al being ignored if indicator is specified.

// When that happens, automatic transitions of the theme will likely look
// ugly as the indicator color suddenly snaps to white at one end, but it's
// not clear how to avoid that any further.
if (color == Material.of(context).color)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just not do anything? I would think it's the developer's responsibility to not use same background/foreground colors. I don't think we can just decide he is making a mistake (maybe he wants the same color), especially as we don't have a good resolution here. (Also, what happens if the material color is white?)

If we leave this here, we should probably document this behavior in the relevant places (TabBar.indicatorColor and Theme.indicatorColor)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is old code that I left alone for the sake of backwards compatibility. I'd be happy to document or revise it in a separate PR

@@ -636,12 +742,24 @@ class _TabBarState extends State<TabBar> {
@override
void didUpdateWidget(TabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller)
if (widget.controller != oldWidget.controller ||
widget.indicatorColor != oldWidget.indicatorColor ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like _updateTabController() short circuits if newController == _controller, so the new or conditions here doesn't do anything no?

Should we have a test that changes one of these parameters (e.g indicatorColor) and verifies that the change worked?

@@ -591,6 +665,37 @@ class _TabBarState extends State<TabBar> {
_IndicatorPainter _indicatorPainter;
int _currentIndex;
double _tabStripWidth;
List<GlobalKey> _tabKeys;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you add a comment saying what is this used for?

Copy link
Contributor Author

@HansMuller HansMuller Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes (added where it's first used)

@amirh
Copy link
Contributor

amirh commented Feb 20, 2018

I did not review the tests

Copy link
Contributor

@amirh amirh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@HansMuller HansMuller merged commit 24efb55 into flutter:master Feb 21, 2018
@tvolkert
Copy link
Contributor

This change made the selected tab indicator a little bit wider than it was previously, which turned up in our internal pixel diffing tests. Was this intentional?

/cc @jason-simmons

@HansMuller
Copy link
Contributor Author

Yes, and I should have mentioned that here. The original tab indicator painter had a 1/2 pixel error.

@tvolkert
Copy link
Contributor

Actually it looks like this caused a regression in certain cases where the selected tab changes, and the font of the tab is updated to reflect the selection change, but the selected tab indicator doesn't move.

before_after

DaveShuckerow pushed a commit to DaveShuckerow/flutter that referenced this pull request May 14, 2018
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants