Skip to content

Commit

Permalink
Introduce Expandable Widget
Browse files Browse the repository at this point in the history
  • Loading branch information
dev2-nomo committed Nov 23, 2023
1 parent de6e2b3 commit 130de77
Show file tree
Hide file tree
Showing 11 changed files with 545 additions and 8 deletions.
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ linter:
prefer_int_literals: false
sort_constructors_first: false
lines_longer_than_80_chars: false
avoid_positional_boolean_parameters: false


3 changes: 2 additions & 1 deletion example/lib/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:example/main.dart';
import 'package:example/sections/button_section.dart';
import 'package:example/sections/card_section.dart';
import 'package:example/sections/dialogs/dialog_wrapper.dart';
import 'package:example/sections/expandable_section.dart';
import 'package:example/sections/loading_section.dart';
import 'package:example/sections/modal_sheet_section.dart';
import 'package:example/sections/text_section.dart';
Expand Down Expand Up @@ -148,7 +149,7 @@ final routes = [
MenuPageRouteInfo(
name: "/expandable",
title: "Expandable",
page: DialogWrapper(),
page: ExpandableSection(),
),
MenuPageRouteInfo(
name: "/tile",
Expand Down
95 changes: 95 additions & 0 deletions example/lib/sections/expandable_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:nomo_ui_kit/components/app/routebody/nomo_route_body.dart';
import 'package:nomo_ui_kit/components/expandable/expandable.dart';
import 'package:nomo_ui_kit/components/outline_container/nomo_outline_container.dart';
import 'package:nomo_ui_kit/components/text/nomo_text.dart';
import 'package:nomo_ui_kit/theme/nomo_theme.dart';
import 'package:nomo_ui_kit/utils/layout_extensions.dart';

class ExpandableSection extends StatelessWidget {
const ExpandableSection({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return NomoRouteBody(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NomoText(
"Expandable",
style: context.typography.h3,
),
8.vSpacing,
NomoText(
"The Expandable Widget can be used to expand things",
style: context.typography.b1,
),
32.vSpacing,
NomoOutlineContainer(
child: Column(
children: [
Expandable(
title: Text(
"Simple Expandable",
style: context.typography.h1,
),
children: [
Container(
height: 64,
color: Colors.red,
),
Container(
height: 64,
color: Colors.white,
),
Container(
height: 64,
color: Colors.red,
)
],
),
Expandable(
title: Text(
"Nested Expandable",
style: context.typography.h1,
),
children: [
Expandable(
margin: const EdgeInsets.symmetric(horizontal: 12),
title: Text(
"Child Expandable",
style: context.typography.h1,
),
children: [
Container(
height: 64,
color: Colors.red,
),
Container(
height: 64,
color: Colors.white,
),
Container(
height: 64,
color: Colors.blue,
)
],
),
Container(
height: 64,
color: Colors.white,
),
Container(
height: 64,
color: Colors.red,
)
],
),
],
),
),
],
),
);
}
}
1 change: 0 additions & 1 deletion example/lib/sections/loading_section.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:nomo_ui_kit/components/app/app.dart';
import 'package:nomo_ui_kit/components/layout/dynamic_row/dynamic_row.dart';
import 'package:nomo_ui_kit/components/loading/loading.dart';
Expand Down
13 changes: 7 additions & 6 deletions lib/components/buttons/base/nomo_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ class _NomoButtonState extends State<NomoButton> with SingleTickerProviderStateM

@override
Widget build(BuildContext context) {
final borderRadius = switch (widget.borderRadius) {
final BorderRadiusGeometry borderRadius => borderRadius.resolve(Directionality.of(context)),
null when widget.shape == BoxShape.circle => BorderRadius.circular(1E3),
null => null,
};

return Padding(
padding: widget.margin ?? EdgeInsets.zero,
child: SizedBox(
Expand Down Expand Up @@ -160,12 +166,7 @@ class _NomoButtonState extends State<NomoButton> with SingleTickerProviderStateM
onExit: (_) => _controller.reverse(),
child: InkWell(
onTap: widget.onPressed,
borderRadius: widget.borderRadius?.resolve(Directionality.of(context)).ifElse(
widget.shape != BoxShape.circle,
other: BorderRadius.circular(
max(widget.width ?? 0, widget.height ?? 0),
),
),
borderRadius: borderRadius,
hoverColor: widget.backgroundColor
?.darken(0.05)
.ifElse(
Expand Down
215 changes: 215 additions & 0 deletions lib/components/expandable/expandable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import 'dart:math' show pi;
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:nomo_ui_generator/annotations.dart';
import 'package:nomo_ui_kit/components/buttons/base/nomo_button.dart';
import 'package:nomo_ui_kit/theme/nomo_theme.dart';

part 'expandable.theme_data.g.dart';

const _kExpand = Icons.arrow_forward_ios_rounded;
const _kDuration = Duration(milliseconds: 300);
const _kCurve = Curves.easeInOut;

@NomoComponentThemeData('expandableTheme')
class Expandable extends StatefulWidget {
final Widget title;
final List<Widget> children;
final IconData expandIcon;
final Duration duration;
final Curve curve;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? padding;
final BoxDecoration? decoration;

/// If the [expansionNotifier] is defined [initiallyExpanded] is ignored
/// If the [expansionNotifier] is not defined the state will be handled internally
final ValueNotifier<bool>? expansionNotifier;

final void Function()? onLongPress;
final void Function(bool isExpanded)? onExpansionChanged;
final bool initiallyExpanded;

/// Styles
@NomoSizingField(28.0)
final double iconSize;
@NomoColorField(EdgeInsets.symmetric(horizontal: 8.0, vertical: 4))
final EdgeInsetsGeometry titlePadding;
@NomoColorField(EdgeInsets.symmetric(vertical: 4.0))
final EdgeInsetsGeometry childrenPadding;
@NomoColorField(BorderRadius.all(Radius.circular(12)))
final BorderRadius? borderRadius;
@NomoColorField<Color?>(null)
final Color? highlightColor;
@NomoColorField<Color?>(null)
final Color? focusColor;
@NomoColorField<Color?>(null)
final Color? splashColor;
@NomoColorField<Color?>(null)
final Color? hoverColor;

const Expandable({
required this.title,
required this.children,
super.key,
this.decoration,
this.padding,
this.curve = _kCurve,
this.duration = _kDuration,
this.expandIcon = _kExpand,
this.margin,
this.expansionNotifier,
this.iconSize = 24.0,
this.initiallyExpanded = false,
this.onLongPress,
this.titlePadding = const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
this.childrenPadding = const EdgeInsets.symmetric(vertical: 4.0),
this.onExpansionChanged,
this.borderRadius = const BorderRadius.all(Radius.circular(12)),
this.focusColor,
this.highlightColor,
this.hoverColor,
this.splashColor,
});

@override
State<Expandable> createState() => _ExpandableState();
}

class _ExpandableState extends State<Expandable> with TickerProviderStateMixin {
late final AnimationController controller;
late final Animation<double> turnAnimation;
late final Animation<double> heightFactorAnimation;
late final ValueNotifier<bool> stateNotifier;
late ExpandableThemeData theme;

@override
void initState() {
stateNotifier = widget.expansionNotifier ?? ValueNotifier(widget.initiallyExpanded)
..addListener(onStateChanged);

controller = AnimationController(
vsync: this,
duration: widget.duration,
value: stateNotifier.value ? 1 : 0,
);

turnAnimation = Tween(begin: pi / 2, end: 1.5 * pi).animate(
CurvedAnimation(
parent: controller,
curve: widget.curve,
),
);
heightFactorAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: widget.curve,
),
);

super.initState();
}

@override
void didChangeDependencies() {
theme = getFromContext(context, widget);
super.didChangeDependencies();
}

@override
void dispose() {
controller.dispose();
stateNotifier
..removeListener(onStateChanged)
..dispose();
super.dispose();
}

void onStateChanged() {
final isExpanded = stateNotifier.value;
if (isExpanded) {
controller.forward();
} else {
controller.reverse();
}
widget.onExpansionChanged?.call(isExpanded);
}

void onTap() {
stateNotifier.value = !stateNotifier.value;
}

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: stateNotifier,
builder: (context, value, _) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final hideChildren = value && controller.isDismissed;
return Container(
margin: widget.margin ?? EdgeInsets.zero,
decoration: widget.decoration,
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
onLongPress: widget.onLongPress,
highlightColor: theme.highlightColor,
splashColor: theme.splashColor,
hoverColor: theme.hoverColor,
focusColor: theme.focusColor,
borderRadius: widget.borderRadius,
child: Padding(
padding: theme.titlePadding,
child: Row(
children: [
Expanded(child: widget.title),
NomoButton(
onPressed: onTap,
shape: BoxShape.circle,
padding: const EdgeInsets.all(12),
child: Transform.rotate(
angle: turnAnimation.value,
child: Icon(
widget.expandIcon,
size: theme.iconSize,
),
),
),
],
),
),
),
),
ClipRect(
child: Align(
heightFactor: heightFactorAnimation.value,
child: Offstage(
offstage: hideChildren,
child: Padding(
padding: theme.childrenPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: widget.children,
),
),
),
),
)
],
),
);
},
);
},
);
}
}
Loading

0 comments on commit 130de77

Please sign in to comment.