Skip to content

Commit

Permalink
ref: Move divider into ResizableChild (#77)
Browse files Browse the repository at this point in the history
* Move divider into ResizableChild

* Add toString, equality, and hashCode overrides

* Clean up divider space calc

* Add divider to child toString

* Update README

* Remove int cast
  • Loading branch information
andyhorn authored Dec 21, 2024
1 parent 7a3c94c commit 117bbce
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 159 deletions.
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Each example also comes with an embedded source-code view, so you don't have to
## Features

- `ResizableContainer`s are fully nestable and support LTR _and_ RTL layouts
- Customize the length, thickness, alignment, and color of the divider(s) between children and the cursor displayed when hovering
- Customize the look and feel of the divider(s) between children
- Respond to user interactions with `onHoverEnter` and `onHoverExit` for web/desktop and `onTapDown` and `onTapUp` for mobile
- Programmatically set the sizes of the children through a `ResizableController`
- Respond to changes in the sizes of the resizable children by listening to the `ResizableController`
Expand Down Expand Up @@ -101,33 +101,38 @@ onTap: () => controller.setSizes(const [

### ResizableChild

To add widgets to your container, you must provide a `List<ResizableChild>`, each of which contain the child `Widget` and an optional `ResizableSize`.
To add widgets to your container, you must provide a `List<ResizableChild>`, each of which contain the child `Widget`, an optional `ResizableDivider`, and an optional `ResizableSize`.

```dart
children: [
if (showNavBar) ...[
const ResizableChild(
size: ResizableSize.expand(max: 350),
child: NavBarWidget(),
const ResizableChild(
divider: ResizableDivider(
thickness: 2,
color: Colors.blue,
),
],
size: ResizableSize.expand(max: 350),
child: NavBarWidget(),
),
const ResizableChild(
divider: ResizableDivider(
thickness: 2,
padding: 3,
),
child: BodyWidget(),
),
if (showSidePanel) ...[
const ResizableChild(
size: ResizableSize.ratio(0.25, min: 100),
child: SidePanelWidget(),
),
],
const ResizableChild(
size: ResizableSize.ratio(0.25, min: 100),
child: SidePanelWidget(),
),
],
```

In the example above, there are three `Widget`s added to the screen, two of which can be hidden based on state.
In the example above, the first two children have a custom `ResizableDivider` (read more about dividers in the [ResizableDivider](#resizabledivider) section). If no divider is set, a default one will be used. The divider provided to a child will be used between itself and the _next_ child in the list - the divider of the last child will not be used.

The first child, containing the `NavBarWidget`, has a maximum size of 350px.
The second child, containing the `BodyWidget`, is set to automatically expand to fill the available space via the default `ResizableSize.expand()` value.
The third child, containing the `SidePanelWidget`, is set to a ratio of 0.75 with a minimum size of 100px.
Each child also provides a custom size configuration:
* The first child, containing the `NavBarWidget`, has a maximum size of 350px.
* The second child, containing the `BodyWidget`, is set to automatically expand to fill the available space via the default `ResizableSize.expand()` value.
* The third child, containing the `SidePanelWidget`, is set to a ratio of 0.75 with a minimum size of 100px.

The `size` parameter gives a directive of how to size the child during the initial layout, resizing, and screen size changes. See the [Resizable Size](#resizable-size) section below for more information.

Expand Down Expand Up @@ -259,7 +264,7 @@ In this scenario, the first child would be given 2/3 of the total available spac

Use the `ResizableDivider` class to customize the look and feel of the dividers between each of a container's children.

You can customize the `thickness`, `length`, `crossAxisAlignment`, `mainAxisAlignment`, and `color` of the divider, as well as display a custom mouse cursor on hover. You can also provide callbacks for the `onHoverEnter` and `onHoverExit` (web) and `onTapDown` and `onTapUp` (mobile) events to respond to user interactions.
You can customize the `thickness`, `length`, `crossAxisAlignment`, `mainAxisAlignment`, and `color` of the divider, as well as display a custom mouse cursor on hover and respond to `onHoverEnter` and `onHoverExit` (web) and `onTapDown` and `onTapUp` (mobile) events.

```dart
divider: ResizableDivider(
Expand All @@ -285,7 +290,7 @@ If the divider's length is less than the total available space, you can use the

![Cross-Axis Alignment](./doc/screenshot_cross_axis_start.png?raw=true 'Cross-Axis Alignment')

By adding a `padding` value, additional (empty) space will be added around/alongside the divider. The `mainAxisAlignment` property can then be used to control its position within this space on the main axis. For example, a vertical divider set to `MainAxisAlignment.start` will be positioned at the very left edge of its available space.
By adding a `padding` value, additional (empty) space will be added around/alongside the divider. The `mainAxisAlignment` property can then be used to control its position within this space on the main axis. For example, a vertical divider set to `MainAxisAlignment.start` will be positioned at the very left edge of the available space for a vertical divider.

![Main-Axis Alignment](./doc/screenshot_main_axis_start.png?raw=true 'Main-Axis Alignment')

Expand Down
28 changes: 14 additions & 14 deletions example/lib/screens/divider/custom_divider_example_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,22 @@ class _CustomDividerExampleScreenState
Expanded(
child: ResizableContainer(
direction: Axis.horizontal,
divider: ResizableDivider(
color: hovered
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.inversePrimary,
thickness: thickness,
padding: padding,
crossAxisAlignment: crossAxisAlignment,
mainAxisAlignment: mainAxisAlignment,
length: ResizableSize.ratio(length),
onHoverEnter: () => setState(() => hovered = true),
onHoverExit: () => setState(() => hovered = false),
onTapDown: () => setState(() => hovered = true),
onTapUp: () => setState(() => hovered = false),
),
children: [
ResizableChild(
divider: ResizableDivider(
color: hovered
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.inversePrimary,
thickness: thickness,
padding: padding,
crossAxisAlignment: crossAxisAlignment,
mainAxisAlignment: mainAxisAlignment,
length: ResizableSize.ratio(length),
onHoverEnter: () => setState(() => hovered = true),
onHoverExit: () => setState(() => hovered = false),
onTapDown: () => setState(() => hovered = true),
onTapUp: () => setState(() => hovered = false),
),
child: ColoredBox(
color: Theme.of(context).colorScheme.primaryContainer,
child: const Center(child: Text('Left')),
Expand Down
34 changes: 11 additions & 23 deletions lib/src/layout/resizable_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_resizable_container/flutter_resizable_container.dart';
import 'package:flutter_resizable_container/src/extensions/iterable_ext.dart';
import 'package:flutter_resizable_container/src/layout/resizable_layout_direction.dart';
import 'package:flutter_resizable_container/src/resizable_size.dart';

Expand All @@ -15,14 +16,12 @@ class ResizableLayout extends MultiChildRenderObjectWidget {
super.key,
required super.children,
required this.direction,
required this.divider,
required this.onComplete,
required this.sizes,
required this.resizableChildren,
});

final Axis direction;
final ResizableDivider divider;
final ValueChanged<List<double>> onComplete;
final List<ResizableSize> sizes;
final List<ResizableChild> resizableChildren;
Expand All @@ -31,7 +30,6 @@ class ResizableLayout extends MultiChildRenderObjectWidget {
RenderObject createRenderObject(BuildContext context) {
return ResizableLayoutRenderObject(
layoutDirection: ResizableLayoutDirection.forAxis(direction),
divider: divider,
sizes: sizes,
onComplete: onComplete,
resizableChildren: resizableChildren,
Expand All @@ -45,7 +43,6 @@ class ResizableLayout extends MultiChildRenderObjectWidget {
) {
renderObject
..layoutDirection = ResizableLayoutDirection.forAxis(direction)
..divider = divider
..sizes = sizes
..onComplete = onComplete
..resizableChildren = resizableChildren;
Expand All @@ -56,25 +53,21 @@ class ResizableLayoutRenderObject extends RenderBox
with _ContainerMixin, _DefaultsMixin {
ResizableLayoutRenderObject({
required ResizableLayoutDirection layoutDirection,
required ResizableDivider divider,
required List<ResizableSize> sizes,
required ValueChanged<List<double>> onComplete,
required List<ResizableChild> resizableChildren,
}) : _layoutDirection = layoutDirection,
_divider = divider,
_sizes = sizes,
_onComplete = onComplete,
_resizableChildren = resizableChildren;

ResizableLayoutDirection _layoutDirection;
ResizableDivider _divider;
List<ResizableSize> _sizes;
ValueChanged<List<double>> _onComplete;
List<ResizableChild> _resizableChildren;
double _currentPosition = 0.0;

ResizableLayoutDirection get layoutDirection => _layoutDirection;
ResizableDivider get divider => _divider;
List<ResizableSize> get sizes => _sizes;
ValueChanged<List<double>> get onComplete => _onComplete;
List<ResizableChild> get resizableChildren => _resizableChildren;
Expand All @@ -88,15 +81,6 @@ class ResizableLayoutRenderObject extends RenderBox
markNeedsLayout();
}

set divider(ResizableDivider divider) {
if (_divider == divider) {
return;
}

_divider = divider;
markNeedsLayout();
}

set sizes(List<ResizableSize> sizes) {
if (listEquals(_sizes, sizes)) {
return;
Expand Down Expand Up @@ -135,7 +119,6 @@ class ResizableLayoutRenderObject extends RenderBox

final children = getChildrenAsList();
final dividerSpace = _getDividerSpace();
final dividerConstraints = _getDividerConstraints();
final pixelSpace = _getPixelsSpace();
final shrinkSpace = _getShrinkSpace(children);
final availableRatioSpace = _getAvailableRatioSpace(
Expand Down Expand Up @@ -176,7 +159,10 @@ class ResizableLayoutRenderObject extends RenderBox

if (i < childCount - 1) {
final divider = children[i + 1];
final dividerSize = _layoutChild(divider, dividerConstraints);
final dividerSize = _layoutChild(
divider,
_getDividerConstraints(resizableChildren[i ~/ 2].divider),
);
finalSizes.add(dividerSize);
}
}
Expand Down Expand Up @@ -221,12 +207,14 @@ class ResizableLayoutRenderObject extends RenderBox
}

double _getDividerSpace() {
final dividerThickness = divider.thickness + divider.padding;
final dividerCount = childCount ~/ 2;
return dividerThickness * dividerCount;
return resizableChildren
.take(resizableChildren.length - 1)
.map((child) => child.divider.thickness + child.divider.padding)
.sum((x) => x)
.toDouble();
}

BoxConstraints _getDividerConstraints() {
BoxConstraints _getDividerConstraints(ResizableDivider divider) {
return BoxConstraints.tight(
layoutDirection.getSize(divider.thickness + divider.padding, constraints),
);
Expand Down
21 changes: 17 additions & 4 deletions lib/src/resizable_child.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_resizable_container/src/resizable_size.dart';
import 'package:flutter_resizable_container/flutter_resizable_container.dart';

/// Controls the sizing parameters for the [child] Widget.
class ResizableChild {
/// Create a new instance of the [ResizableChild] class.
const ResizableChild({
required this.child,
this.size = const ResizableSize.expand(),
this.divider = const ResizableDivider(),
});

/// The size of the corresponding widget. May use a ratio of the
Expand All @@ -22,21 +23,33 @@ class ResizableChild {
///
/// // Auto-fill available space
/// size: const ResizableSize.expand(),
///
/// // Conform to the child's intrinsic size
/// size: const ResizableSize.shrink(),
/// ```
final ResizableSize size;

/// The child [Widget]
/// The child [Widget] to be displayed.
final Widget child;

/// The divider configuration to be used after this child.
///
/// If not provided, the default divider will be used.
///
/// If this is the last child, the divider will not be used.
final ResizableDivider divider;

@override
String toString() => 'ResizableChildData(size: $size, child: $child)';
String toString() =>
'ResizableChildData(size: $size, child: $child, divider: $divider)';

@override
operator ==(Object other) =>
other is ResizableChild &&
other.size == size &&
other.divider == divider &&
other.child.runtimeType == child.runtimeType;

@override
int get hashCode => Object.hash(size, child);
int get hashCode => Object.hash(size, child, divider);
}
23 changes: 10 additions & 13 deletions lib/src/resizable_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ class ResizableContainer extends StatefulWidget {
required this.children,
required this.direction,
this.controller,
ResizableDivider? divider,
}) : divider = divider ?? const ResizableDivider();
});

/// A list of resizable [ResizableChild] containing the child [Widget]s and
/// their sizing configuration.
Expand All @@ -34,9 +33,6 @@ class ResizableContainer extends StatefulWidget {
/// The direction along which the child widgets will be laid and resized.
final Axis direction;

/// Configuration values for the dividing space/line between this container's [children].
final ResizableDivider divider;

@override
State<ResizableContainer> createState() => _ResizableContainerState();
}
Expand All @@ -57,13 +53,12 @@ class _ResizableContainerState extends State<ResizableContainer> {
void didUpdateWidget(covariant ResizableContainer oldWidget) {
final didChildrenChange = !listEquals(oldWidget.children, widget.children);
final didDirectionChange = oldWidget.direction != widget.direction;
final didDividerChange = oldWidget.divider != widget.divider;

if (didChildrenChange) {
controller.setChildren(widget.children);
}

if (didChildrenChange || didDirectionChange || didDividerChange) {
if (didChildrenChange || didDirectionChange) {
manager.setNeedsLayout();
}

Expand Down Expand Up @@ -99,14 +94,13 @@ class _ResizableContainerState extends State<ResizableContainer> {
});
},
sizes: controller.sizes,
divider: widget.divider,
resizableChildren: widget.children,
children: [
for (var i = 0; i < widget.children.length; i++) ...[
widget.children[i].child,
if (i < widget.children.length - 1) ...[
ResizableContainerDivider.placeholder(
config: widget.divider,
config: widget.children[i].divider,
direction: widget.direction,
),
],
Expand Down Expand Up @@ -144,7 +138,7 @@ class _ResizableContainerState extends State<ResizableContainer> {
),
if (i < widget.children.length - 1) ...[
ResizableContainerDivider(
config: widget.divider,
config: widget.children[i].divider,
direction: widget.direction,
onResizeUpdate: (delta) => manager.adjustChildSize(
index: i,
Expand All @@ -164,9 +158,12 @@ class _ResizableContainerState extends State<ResizableContainer> {

double _getAvailableSpace(BoxConstraints constraints) {
final totalSpace = constraints.maxForDirection(widget.direction);
final numDividers = widget.children.length - 1;
final dividerSpace = numDividers * widget.divider.thickness +
numDividers * widget.divider.padding;
final dividerSpace = widget.children
.take(widget.children.length - 1)
.map((child) => child.divider)
.map((divider) => divider.thickness + divider.padding)
.sum((x) => x);

return totalSpace - dividerSpace;
}

Expand Down
Loading

0 comments on commit 117bbce

Please sign in to comment.