Skip to content

Commit

Permalink
Implement Expander (bdlukaa#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa committed Nov 12, 2021
1 parent 08efc1b commit 0f31823
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 55 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Date format: DD/MM/YYYY
- Add possibility to disable acrylic by wrapping it in a `DisableAcrylic` ([#89](https://github.com/bdlukaa/fluent_ui/issues/89))
- Fix `onReaorder null exception` ([#88](https://github.com/bdlukaa/fluent_ui/issues/88))
- Implement `InfoBadge`
- Implement `Expander` ([#85](https://github.com/bdlukaa/fluent_ui/issues/85))
- Default `inputMouseCursor` is now `MouseCursor.defer`

## [3.4.0] - Flexibility - [22/10/2021]

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Unofficial implementation of Fluent UI for [Flutter](flutter.dev). It's written
- [Widgets](#widgets)
- [Tooltip](#tooltip)
- [Content Dialog](#content-dialog)
- [Expander](#expander)
- [Flyout](#flyout)
- **TODO** [Teaching tip]()
- [Acrylic](#acrylic)
Expand Down Expand Up @@ -1096,6 +1097,43 @@ showDialog(
![Delete File Dialog](https://docs.microsoft.com/en-us/windows/apps/design/controls/images/dialogs/dialog_rs2_delete_file.png)\
![Subscribe to App Service Dialog](https://docs.microsoft.com/en-us/windows/apps/design/controls/images/dialogs/dialog_rs2_three_button_default.png)\

## Expander

Expander lets you show or hide less important content that's related to a piece of primary content that's always visible. Items contained in the `header` are always visible. The user can expand and collapse the `content` area, where secondary content is displayed, by interacting with the header. When the content area is expanded, it pushes other UI elements out of the way; it does not overlay other UI. The Expander can expand upwards or downwards.

Both the `header` and `content` areas can contain any content, from simple text to complex UI layouts. For example, you can use the control to show additional options for an item.

![](https://docs.microsoft.com/en-us/windows/apps/design/controls/images/expander-default.gif)

Use an Expander when some primary content should always be visible, but related secondary content may be hidden until needed. This UI is commonly used when display space is limited and when information or options can be grouped together. Hiding the secondary content until it's needed can also help to focus the user on the most important parts of your app.

Here's an example of how to create an expander:

```dart
Expander(
header: const Text('This thext is in header'),
content: const Text('This is the content'),
direction: ExpanderDirection.down, // (optional). Defaults to ExpanderDirection.down
initiallyExpanded: false, // (false). Defaults to false
),
```

Open and close the expander programatically:

```dart
final _expanderKey = GlobalKey<ExpanderState>();
Expander(
header: const Text('This thext is in header'),
content: const Text('This is the content'),
),
// Call this function to close the expander
void close() {
_expanderKey.currentState?.open = false;
}
```

## Flyout

A flyout is a light dismiss container that can show arbitrary UI as its content. Flyouts can contain other flyouts or context menus to create a nested experience.
Expand Down
113 changes: 63 additions & 50 deletions example/lib/screens/others.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Others extends StatefulWidget {
}

class _OthersState extends State<Others> {
final _expanderKey = GlobalKey<ExpanderState>();

final otherController = ScrollController();

int currentIndex = 0;
Expand Down Expand Up @@ -56,59 +58,70 @@ class _OthersState extends State<Others> {
),
controller: otherController,
children: [
...List.generate(InfoBarSeverity.values.length, (index) {
final severity = InfoBarSeverity.values[index];
final titles = [
'Long title',
'Short title',
];
final descs = [
'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book',
'Short desc',
];
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: InfoBar(
title: Text(titles[index.isEven ? 0 : 1]),
content: Text(descs[index.isEven ? 0 : 1]),
isLong: InfoBarSeverity.values.indexOf(severity).isEven,
severity: severity,
action: () {
if (index == 0) {
return Tooltip(
message: 'This is a tooltip',
child: Button(
child: const Text('Hover this button to see a tooltip'),
onPressed: () {
print('pressed button with tooltip');
},
Mica(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Expander(
key: _expanderKey,
header: const Text('Info bars'),
content: Column(
children:
List.generate(InfoBarSeverity.values.length, (index) {
final severity = InfoBarSeverity.values[index];
final titles = [
'Long title',
'Short title',
];
final descs = [
'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book',
'Short desc',
];
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: InfoBar(
title: Text(titles[index.isEven ? 0 : 1]),
content: Text(descs[index.isEven ? 0 : 1]),
isLong: InfoBarSeverity.values.indexOf(severity).isEven,
severity: severity,
action: () {
if (index == 0) {
return Tooltip(
message: 'This is a tooltip',
child: Button(
child: const Text(
'Hover this button to see a tooltip'),
onPressed: () {
print('pressed button with tooltip');
},
),
);
} else {
if (index == 3) {
return Flyout(
controller: flyoutController,
contentWidth: 450,
content: const FlyoutContent(
child: Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'),
),
child: Button(
child: const Text('Open flyout'),
onPressed: () {
flyoutController.open = true;
},
),
);
}
}
}(),
onClose: () => _expanderKey.currentState?.open = false,
),
);
} else {
if (index == 3) {
return Flyout(
controller: flyoutController,
contentWidth: 450,
content: const FlyoutContent(
child: Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'),
),
child: Button(
child: const Text('Open flyout'),
onPressed: () {
flyoutController.open = true;
},
),
);
}
}
}(),
onClose: () {
print('closed');
},
}),
),
),
);
}),
),
),
Wrap(children: [
const ListTile(
title: Text('ListTile Title'),
Expand Down
1 change: 1 addition & 0 deletions lib/fluent_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export 'src/controls/navigation/tab_view.dart';
export 'src/controls/surfaces/calendar/calendar_view.dart';
export 'src/controls/surfaces/bottom_sheet.dart';
export 'src/controls/surfaces/dialog.dart';
export 'src/controls/surfaces/expander.dart';
export 'src/controls/surfaces/flyout/flyout.dart';
export 'src/controls/surfaces/info_bar.dart';
export 'src/controls/surfaces/list_tile.dart';
Expand Down
195 changes: 195 additions & 0 deletions lib/src/controls/surfaces/expander.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import 'package:fluent_ui/fluent_ui.dart';

/// The expander direction
enum ExpanderDirection {
/// Whether the [Expander] expands down
down,
/// Whether the [Expander] expands up
up,
}

/// The [Expander] control lets you show or hide less important content
/// that's related to a piece of primary content that's always visible.
/// Items contained in the Header are always visible. The user can expand
/// and collapse the Content area, where secondary content is displayed,
/// by interacting with the header. When the content area is expanded,
/// it pushes other UI elements out of the way; it does not overlay other
/// UI. The Expander can expand upwards or downwards.
///
/// Both the Header and Content areas can contain any content, from simple
/// text to complex UI layouts. For example, you can use the control to show
/// additional options for an item.
///
/// ![Expander Preview](https://docs.microsoft.com/en-us/windows/apps/design/controls/images/expander-default.gif)
///
/// See also:
///
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/expander>
class Expander extends StatefulWidget {
/// Creates an expander
const Expander({
Key? key,
required this.header,
required this.content,
this.icon,
this.animationCurve,
this.animationDuration,
this.direction = ExpanderDirection.down,
this.initiallyExpanded = false,
this.onStateChanged,
}) : super(key: key);

/// The expander header
///
/// Usually a [Text]
final Widget header;

/// The expander content
///
/// You can use complex, interactive UI as the content of the
/// Expander, including nested Expander controls in the content
/// of a parent Expander as shown here.
///
/// ![Expander Nested Content](https://docs.microsoft.com/en-us/windows/apps/design/controls/images/expander-nested.png)
final Widget content;

/// The icon of the toggle button.
final Widget? icon;

/// The expand-collapse animation duration. If null, defaults to
/// [FluentTheme.fastAnimationDuration]
final Duration? animationDuration;

/// The expand-collapse animation curve. If null, defaults to
/// [FluentTheme.animationCurve]
final Curve? animationCurve;

/// The expand direction. Defaults to [ExpanderDirection.down]
final ExpanderDirection direction;

/// Whether the [Expander] is initially expanded. Defaults to `false`
final bool initiallyExpanded;

/// A callback called when the current state is changed. `true` when
/// open and `false` when closed.
final ValueChanged<bool>? onStateChanged;

@override
ExpanderState createState() => ExpanderState();
}

class ExpanderState extends State<Expander>
with SingleTickerProviderStateMixin {
late ThemeData theme;

late bool _open;
bool get open => _open;
set open(bool value) {
if (_open != value) _handlePressed();
}

late AnimationController _controller;

@override
void initState() {
super.initState();
_open = widget.initiallyExpanded;
_controller = AnimationController(
vsync: this,
duration: widget.animationDuration ?? const Duration(milliseconds: 150),
);
}

void _handlePressed() {
if (open) {
_controller.animateTo(
0.0,
duration: widget.animationDuration ?? theme.fastAnimationDuration,
curve: widget.animationCurve ?? theme.animationCurve,
);
_open = false;
} else {
_controller.animateTo(
1.0,
duration: widget.animationDuration ?? theme.fastAnimationDuration,
curve: widget.animationCurve ?? theme.animationCurve,
);
_open = true;
}
widget.onStateChanged?.call(open);
if (mounted) setState(() {});
}

bool get _isDown => widget.direction == ExpanderDirection.down;

@override
Widget build(BuildContext context) {
assert(debugCheckHasFluentTheme(context));
theme = FluentTheme.of(context);
final children = [
HoverButton(
onPressed: _handlePressed,
builder: (context, states) {
return Container(
height: 48.0,
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
border: Border.all(width: 0.25),
borderRadius: BorderRadius.vertical(
top: const Radius.circular(4.0),
bottom: Radius.circular(open ? 0.0 : 4.0),
),
),
padding: const EdgeInsets.only(left: 16.0),
alignment: Alignment.centerLeft,
child: Row(children: [
Expanded(child: widget.header),
Container(
margin: const EdgeInsets.only(
left: 20.0,
right: 8.0,
top: 8.0,
bottom: 8.0,
),
padding: const EdgeInsets.symmetric(horizontal: 10.0),
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(theme, states),
borderRadius: BorderRadius.circular(4.0),
),
alignment: Alignment.center,
child: widget.icon ??
RotationTransition(
turns: Tween<double>(begin: 0, end: 0.5)
.animate(_controller),
child: Icon(
_isDown
? FluentIcons.chevron_down
: FluentIcons.chevron_up,
size: 10,
),
),
),
]),
);
},
),
SizeTransition(
sizeFactor: _controller,
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
border: Border.all(width: 0.25),
color: theme.acrylicBackgroundColor,
borderRadius:
const BorderRadius.vertical(bottom: Radius.circular(4.0)),
),
child: widget.content,
),
),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: _isDown ? children : children.reversed.toList(),
);
}
}
Loading

0 comments on commit 0f31823

Please sign in to comment.