Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created November 3, 2022 20:46
Show Gist options
  • Select an option

  • Save HansMuller/8fd61db8c4ea690f19ee4dea006055cd to your computer and use it in GitHub Desktop.

Select an option

Save HansMuller/8fd61db8c4ea690f19ee4dea006055cd to your computer and use it in GitHub Desktop.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
typedef _NextChild = RenderBox? Function(RenderBox child);
class _SplitViewParentData extends ContainerBoxParentData<RenderBox> {
bool? isActiveSplitter;
double? weight;
double minExtent = 0;
double maxExtent = double.infinity;
double inputPadding = 0;
MouseCursor cursor = MouseCursor.defer;
@override
String toString() => '${super.toString()}; weight=$weight';
}
class SplitConstraints extends ParentDataWidget<_SplitViewParentData> {
const SplitConstraints({
Key? key,
this.weight,
this.minExtent = 0,
this.maxExtent = double.infinity,
this.inputPadding = 0,
this.cursor = MouseCursor.defer,
required Widget child,
}) : assert(weight == null || weight >= 0),
assert(minExtent >= 0),
assert(maxExtent >= minExtent),
super(key: key, child: child);
final double? weight;
final double minExtent;
final double maxExtent;
final double inputPadding;
final MouseCursor cursor;
@override
void applyParentData(RenderObject renderObject) {
final _SplitViewParentData parentData = renderObject.parentData! as _SplitViewParentData;
bool needsLayout = false;
bool needsPaint = false;
if (parentData.weight != weight) {
parentData.weight = weight;
}
if (parentData.minExtent != minExtent) {
parentData.minExtent = minExtent;
needsLayout = true;
}
if (parentData.maxExtent != maxExtent) {
parentData.maxExtent = maxExtent;
needsLayout = true;
}
if (parentData.inputPadding != inputPadding) {
parentData.inputPadding = inputPadding;
}
if (parentData.cursor != cursor) {
parentData.cursor = cursor;
}
if (needsLayout || needsPaint) {
final AbstractNode? targetParent = renderObject.parent;
if (targetParent is RenderObject) {
if (needsLayout) {
targetParent.markNeedsLayout();
} else {
targetParent.markNeedsPaint();
}
}
}
}
@override
Type get debugTypicalAncestorWidgetClass => SplitConstraints;
}
class _SplitViewRenderWidget<T> extends MultiChildRenderObjectWidget {
_SplitViewRenderWidget({
Key? key,
required this.axis,
required List<Widget> children,
}) : super(key: key, children: children);
final Axis axis;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSplitView<T>(
textDirection: Directionality.of(context),
axis: axis,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSplitView<T> renderObject) {
renderObject
..textDirection = Directionality.of(context)
..axis = axis;
}
}
class _RenderSplitView<T> extends RenderBox with
ContainerRenderObjectMixin<RenderBox, _SplitViewParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _SplitViewParentData>
implements MouseTrackerAnnotation {
_RenderSplitView({
required TextDirection textDirection,
required Axis axis,
}) : _textDirection = textDirection,
_axis = axis
{
initDragRecognizer(axis);
}
Size? initialSize;
late DragGestureRecognizer dragRecognizer;
bool dragUnderway = false;
late Offset dragAnchor;
late Offset dragPosition;
bool splitLayoutNeeded = false;
late Offset splitAnchor;
late Offset splitOffset;
@override
MouseCursor get cursor => _cursor;
MouseCursor _cursor = MouseCursor.defer;
set cursor(MouseCursor value) {
if (_cursor != value) {
_cursor = value;
// A repaint is needed in order to trigger a device update of
// [MouseTracker] so that this new value can be found.
markNeedsPaint();
}
}
@override
PointerEnterEventListener? onEnter;
@override
PointerExitEventListener? onExit;
@override
bool get validForMouseTracker => _validForMouseTracker;
bool _validForMouseTracker = true;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_validForMouseTracker = true;
}
@override
void detach() {
// It's possible that the renderObject be detached during mouse events
// dispatching, set the [MouseTrackerAnnotation.validForMouseTracker] false to prevent
// the callbacks from being called.
_validForMouseTracker = false;
super.detach();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
markNeedsLayout();
}
Axis get axis => _axis;
Axis _axis;
set axis(Axis value) {
if (_axis == value) {
return;
}
_axis = value;
initialSize = null;
initDragRecognizer(_axis);
markNeedsLayout();
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _SplitViewParentData) {
child.parentData = _SplitViewParentData();
}
}
double? getChildWeight(RenderBox child) => (child.parentData! as _SplitViewParentData).weight;
double getChildMinExtent(RenderBox child) => (child.parentData! as _SplitViewParentData).minExtent;
double getChildMaxExtent(RenderBox child) => (child.parentData! as _SplitViewParentData).maxExtent;
Offset getChildOffset(RenderBox child)=> (child.parentData! as _SplitViewParentData).offset;
void setChildOffset(RenderBox child, Offset offset) {
(child.parentData! as _SplitViewParentData).offset = offset;
}
BoxConstraints getChildConstraints(RenderBox child) {
final _SplitViewParentData parentData = child.parentData! as _SplitViewParentData;
switch (axis) {
case Axis.horizontal:
final double minWidth = math.min(constraints.minWidth, parentData.minExtent);
final double maxWidth = math.min(constraints.maxWidth, parentData.maxExtent);
return constraints
.tighten(height: constraints.maxHeight)
.copyWith(minWidth: minWidth, maxWidth: maxWidth);
case Axis.vertical:
final double minHeight = math.min(constraints.minHeight, parentData.minExtent);
final double maxHeight = math.min(constraints.maxHeight, parentData.maxExtent);
return constraints
.tighten(width: constraints.maxWidth)
.copyWith(minHeight: minHeight, maxHeight: maxHeight);
}
}
RenderBox? splitterAt(Offset position) {
RenderBox? child = firstChild;
RenderBox? splitter;
double splitterDistance = double.infinity; // input-padded splitters can overlap
int index = 0;
while (child != null) {
final bool isSplitter = index.isOdd;
index += 1;
if (isSplitter) {
final _SplitViewParentData parentData = child.parentData! as _SplitViewParentData;
final double inputPadding = parentData.inputPadding;
switch (axis) {
case Axis.horizontal:
final double childX = parentData.offset.dx;
if (position.dx >= childX - inputPadding && position.dx < childX + child.size.width + inputPadding) {
double distance = (position.dx - (childX + child.size.width / 2)).abs();
if (distance < splitterDistance) {
splitterDistance = distance;
splitter = child;
}
}
break;
case Axis.vertical:
final double childY = parentData.offset.dy;
if (position.dy >= childY - inputPadding && position.dy < childY + child.size.height + inputPadding) {
double distance = (position.dy - (childY + child.size.height / 2)).abs();
if (distance < splitterDistance) {
splitterDistance = distance;
splitter = child;
}
}
break;
}
}
child = childAfter(child);
}
return splitter;
}
RenderBox? getActiveSplitter() {
RenderBox? child = firstChild;
while (child != null) {
final _SplitViewParentData parentData = child.parentData! as _SplitViewParentData;
if (parentData.isActiveSplitter == true) {
return child;
}
child = childAfter(child);
}
return null;
}
void setActiveSplitter(RenderBox splitter) {
RenderBox? child = firstChild;
while (child != null) {
final _SplitViewParentData parentData = child.parentData! as _SplitViewParentData;
parentData.isActiveSplitter = child == splitter;
child = childAfter(child);
}
}
void clearActiveSplitter() {
RenderBox? child = firstChild;
while (child != null) {
final _SplitViewParentData parentData = child.parentData! as _SplitViewParentData;
parentData.isActiveSplitter = false;
child = childAfter(child);
}
}
@override
bool hitTestSelf(Offset position) => splitterAt(position) != null;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
if (splitterAt(position) == null) {
return defaultHitTestChildren(result, position: position);
}
return false;
}
void initDragRecognizer(Axis axis) {
switch (axis) {
case Axis.horizontal:
dragRecognizer = HorizontalDragGestureRecognizer();
break;
case Axis.vertical:
dragRecognizer = HorizontalDragGestureRecognizer();
break;
}
dragRecognizer
..dragStartBehavior = DragStartBehavior.down
..onStart = handleDragStart
..onUpdate = handleDragUpdate
..onEnd = handleDragEnd
..onCancel = handleDragCancel;
}
void handleDragStart(DragStartDetails details) {
final RenderBox? splitter = splitterAt(details.localPosition);
if (splitter == null) {
dragUnderway = false;
} else {
dragUnderway = true;
setActiveSplitter(splitter);
splitAnchor = getChildOffset(splitter);
splitOffset = splitAnchor;
dragAnchor = details.localPosition;
}
}
void handleDragUpdate(DragUpdateDetails details) {
if (dragUnderway) {
splitOffset = splitAnchor + details.localPosition - dragAnchor;
splitLayoutNeeded = true;
markNeedsLayout();
}
}
void handleDragEnd(DragEndDetails details) {
dragUnderway = false;
}
void handleDragCancel() {
dragUnderway = false;
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent) {
dragRecognizer.addPointer(event);
}
if (event is PointerHoverEvent && !event.down) {
final RenderBox? splitter = splitterAt(event.localPosition);
if (splitter != null) {
final _SplitViewParentData parentData = splitter.parentData! as _SplitViewParentData;
cursor = parentData.cursor;
}
}
}
double redistributeUnderflow(double underflow, RenderBox? startChild, _NextChild nextChild) {
void updateHorizontalLayout() {
double x = 0;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
if (underflow != 0) {
final double? weight = getChildWeight(child);
if (weight != null) {
final BoxConstraints childConstraints = getChildConstraints(child);
final double childWidth = child.size.width;
child.layout(childConstraints.tighten(width: child.size.width + underflow), parentUsesSize: true);
underflow -= child.size.width - childWidth;
}
}
setChildOffset(child, Offset(x, 0));
x += child.size.width;
}
}
void updateVerticalLayout() {
double y = 0;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
if (underflow != 0) {
final double? weight = getChildWeight(child);
if (weight != null) {
final BoxConstraints childConstraints = getChildConstraints(child);
final double childHeight = child.size.height;
child.layout(childConstraints.tighten(height: child.size.height + underflow), parentUsesSize: true);
underflow -= child.size.height - childHeight;
}
}
setChildOffset(child, Offset(0, y));
y += child.size.height;
}
}
switch (axis) {
case Axis.horizontal:
updateHorizontalLayout();
break;
case Axis.vertical:
updateVerticalLayout();
break;
}
return underflow;
}
// Layout the renderer's children based on their weights the first
// time this renderer is laid out, or when its axis changes.
Size initialLayout(RenderBox? startChild, _NextChild nextChild) {
// Layout each unweighted child and compute the available "extent"
// - the width or height along the main axis - that remains when
// it's reduced by the extents of the unweighted children. The
// extent of the remaining children be proportional to their weight.
double totalWeight = 0;
late double availableExtent;
switch (axis) {
case Axis.horizontal:
availableExtent = constraints.maxWidth;
break;
case Axis.vertical:
availableExtent = constraints.maxHeight;
break;
}
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
final double? weight = getChildWeight(child);
if (weight != null) {
totalWeight += weight;
} else {
BoxConstraints childConstraints = getChildConstraints(child);
child.layout(childConstraints, parentUsesSize: true);
switch (axis) {
case Axis.horizontal:
availableExtent -= child.size.width;
break;
case Axis.vertical:
availableExtent -= child.size.height;
break;
}
}
}
// Layout each weighted child with its width or height constrained to
// its share of the available extent.
double width = 0;
double height = 0;
switch (axis) {
case Axis.horizontal:
height = constraints.maxHeight;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
final double? weight = getChildWeight(child);
if (weight != null) {
BoxConstraints childConstraints = getChildConstraints(child);
final double weightedWidth = availableExtent * weight / totalWeight; // TBD availableExtent == 0
BoxConstraints weightedConstraints = childConstraints.tighten(width: weightedWidth);
child.layout(weightedConstraints, parentUsesSize: true);
}
setChildOffset(child, Offset(width, 0));
width += child.size.width;
}
double underflow = constraints.biggest.width - width;
if (underflow != 0) {
width -= redistributeUnderflow(underflow, startChild, nextChild);
}
break;
case Axis.vertical:
width = constraints.maxWidth;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
final double? weight = getChildWeight(child);
if (weight != null) {
BoxConstraints childConstraints = getChildConstraints(child);
final double weightedHeight = availableExtent * weight / totalWeight; // TBD availableExtent == 0
BoxConstraints weightedConstraints = childConstraints.tighten(height: weightedHeight);
child.layout(weightedConstraints, parentUsesSize: true);
}
setChildOffset(child, Offset(0, height));
height += child.size.height;
}
double underflow = constraints.biggest.height - height;
if (underflow != 0) {
height -= redistributeUnderflow(underflow, startChild, nextChild);
}
break;
}
initialSize = constraints.constrain(Size(width, height));
return initialSize!;
}
// Layout the renderer's children when a splitter is moved.
Size splitLayout(RenderBox? startChild, _NextChild nextChild, _NextChild previousChild) {
final RenderBox activeSplitter = getActiveSplitter()!;
switch (axis) {
case Axis.horizontal:
final double splitExtent = previousChild(activeSplitter)!.size.width + nextChild(activeSplitter)!.size.width;
double width = 0;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
BoxConstraints childConstraints = getChildConstraints(child);
if (activeSplitter == nextChild(child)) {
final RenderBox next = nextChild(activeSplitter)!;
final double minWidth = math.max(getChildMinExtent(child), splitExtent - getChildMaxExtent(next));
final double maxWidth = math.min(getChildMaxExtent(child), splitExtent - getChildMinExtent(next));
childConstraints = childConstraints.tighten(
width: (splitOffset.dx - getChildOffset(child).dx).clamp(minWidth, maxWidth),
);
} else if (activeSplitter == previousChild(child)) {
final double splitterWidth = activeSplitter.size.width;
final RenderBox previous = previousChild(activeSplitter)!;
final double minWidth = math.max(getChildMinExtent(child), splitExtent - getChildMaxExtent(previous));
final double maxWidth = math.min(getChildMaxExtent(child), splitExtent - getChildMinExtent(previous));
childConstraints = childConstraints.tighten(
width: (getChildOffset(child).dx + child.size.width - splitOffset.dx - splitterWidth).clamp(minWidth, maxWidth),
);
} else {
childConstraints = childConstraints.tighten(
width: child.size.width,
);
}
child.layout(childConstraints, parentUsesSize: true);
setChildOffset(child, Offset(width, 0));
width += child.size.width;
}
break;
case Axis.vertical:
double height = 0;
final double splitExtent = previousChild(activeSplitter)!.size.height + nextChild(activeSplitter)!.size.height;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
BoxConstraints childConstraints = getChildConstraints(child);
if (activeSplitter == nextChild(child)) {
final RenderBox next = nextChild(activeSplitter)!;
final double minHeight = math.max(getChildMinExtent(child), splitExtent - getChildMaxExtent(next));
final double maxHeight = math.min(getChildMaxExtent(child), splitExtent - getChildMinExtent(next));
childConstraints = childConstraints.tighten(
height: (splitOffset.dy - getChildOffset(child).dy).clamp(minHeight, maxHeight),
);
} else if (activeSplitter == previousChild(child)) {
final double splitterHeight = activeSplitter.size.height;
final RenderBox previous = previousChild(activeSplitter)!;
final double minHeight = math.max(getChildMinExtent(child), splitExtent - getChildMaxExtent(previous));
final double maxHeight = math.min(getChildMaxExtent(child), splitExtent - getChildMinExtent(previous));
childConstraints = childConstraints.tighten(
height: (getChildOffset(child).dy + child.size.height - splitOffset.dy - splitterHeight).clamp(minHeight, maxHeight),
);
} else {
childConstraints = childConstraints.tighten(
height: child.size.height,
);
}
child.layout(childConstraints, parentUsesSize: true);
setChildOffset(child, Offset(0, height));
height += child.size.height;
}
break;
}
splitLayoutNeeded = false;
return constraints.biggest;
}
// Layout the renderer's children when its size constraints change.
// The additional space is distributed to the weighted children.
Size updateLayout(RenderBox? startChild, _NextChild nextChild) {
// Layout each unweighted child and compute the available "extent"
// - the width or height along the main axis - that remains when
// it's reduced by the extents of the unweighted children.
double totalWeight = 0;
for (RenderBox? child = startChild; child != null; ) {
final double? weight = getChildWeight(child);
if (weight != null) {
totalWeight += weight;
} else {
BoxConstraints childConstraints = getChildConstraints(child);
switch (axis) {
case Axis.horizontal:
child.layout(childConstraints.tighten(width: child.size.width), parentUsesSize: true);
break;
case Axis.vertical:
child.layout(childConstraints.tighten(height: child.size.height), parentUsesSize: true);
break;
}
}
child = nextChild(child);
}
void updateHorizontalLayout() {
final double deltaWidth = constraints.biggest.width - size.width;
double x = 0;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
final double? weight = getChildWeight(child);
final BoxConstraints childConstraints = getChildConstraints(child);
if (weight != null) {
final double updatedWidth = child.size.width + deltaWidth * weight / totalWeight;
child.layout(childConstraints.tighten(width: updatedWidth), parentUsesSize: true);
}
setChildOffset(child, Offset(x, 0));
x += child.size.width;
}
double underflow = constraints.biggest.width - x;
if (underflow != 0) {
underflow = redistributeUnderflow(underflow, startChild, nextChild);
}
}
void updateVerticalLayout() {
// Allocate deltaHeight to the weighted children in proportion to their weight.
final double deltaHeight = constraints.biggest.height - size.height;
double y = 0;
for (RenderBox? child = startChild; child != null; child = nextChild(child)) {
final double? weight = getChildWeight(child);
if (weight != null) {
final BoxConstraints childConstraints = getChildConstraints(child);
final double updatedHeight = child.size.height + deltaHeight * weight / totalWeight;
child.layout(childConstraints.tighten(height: updatedHeight), parentUsesSize: true);
}
setChildOffset(child, Offset(0, y));
y += child.size.height;
}
// If we were unable to allocate all of deltaHeight, then allocate the over/underflow
// to the first weighted child (children) that can accept it per their min,maxExtent
// constraints. In this case the allocations are not proportional the child's weight.
double underflow = constraints.biggest.height - y;
if (underflow != 0) {
underflow = redistributeUnderflow(underflow, startChild, nextChild);
}
}
switch (axis) {
case Axis.horizontal:
updateHorizontalLayout();
break;
case Axis.vertical:
updateVerticalLayout();
break;
}
return constraints.biggest;
}
@override
void performLayout() {
late final RenderBox? startChild;
late final _NextChild nextChild;
late final _NextChild previousChild;
// RTL order only applies to horizontal layouts
switch (textDirection) {
case TextDirection.ltr:
startChild = firstChild;
nextChild = childAfter;
previousChild = childBefore;
break;
case TextDirection.rtl:
switch(axis) {
case Axis.vertical:
startChild = firstChild;
nextChild = childAfter;
previousChild = childBefore;
break;
case Axis.horizontal:
startChild = lastChild;
nextChild = childBefore;
previousChild = childAfter;
break;
}
}
// If children were added (not laid out yet) then the layout can't
// be updated, so we force an initialLayout().
for (RenderBox? child = startChild; child != null && initialSize != null; child = nextChild(child)) {
if (!child.hasSize) {
initialSize = null;
}
}
if (initialSize == null) {
size = initialLayout(startChild, nextChild);
} else if (splitLayoutNeeded) {
size = splitLayout(startChild, nextChild, previousChild);
} else {
size = updateLayout(startChild, nextChild);
}
}
@override
double computeMinIntrinsicWidth(double height) {
double minWidth = 0.0;
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
final double minChildWidth = child.getMinIntrinsicWidth(height);
switch (axis) {
case Axis.horizontal:
minWidth += minChildWidth;
break;
case Axis.vertical:
minWidth = math.max(minWidth, minChildWidth);
break;
}
}
return minWidth;
}
@override
double computeMaxIntrinsicWidth(double height) {
double maxWidth = 0.0;
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
final double maxChildWidth = child.getMaxIntrinsicWidth(height);
switch (axis) {
case Axis.horizontal:
maxWidth += maxChildWidth;
break;
case Axis.vertical:
maxWidth = math.max(maxWidth, maxChildWidth);
break;
}
}
return maxWidth;
}
@override
double computeMinIntrinsicHeight(double width) {
double minHeight = 0.0;
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
final double minChildHeight = child.getMinIntrinsicHeight(width);
switch (axis) {
case Axis.horizontal:
minHeight += minChildHeight;
break;
case Axis.vertical:
minHeight = math.max(minHeight, minChildHeight);
break;
}
}
return minHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
double maxHeight = 0.0;
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
final double maxChildHeight = child.getMaxIntrinsicHeight(width);
switch (axis) {
case Axis.horizontal:
maxHeight += maxChildHeight;
break;
case Axis.vertical:
maxHeight = math.max(maxHeight, maxChildHeight);
break;
}
}
return maxHeight;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;
while (child != null) {
final _SplitViewParentData childParentData = child.parentData! as _SplitViewParentData;
context.paintChild(child, childParentData.offset + offset);
child = childAfter(child);
}
}
}
class SplitView extends StatelessWidget {
const SplitView({
Key? key,
this.axis = Axis.horizontal,
required this.children,
}) : super(key: key);
final Axis axis;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return _SplitViewRenderWidget(
axis: axis,
children: children,
);
}
}
// ------- DEMO -------
enum DemoId { splitView, nestedSplitViews }
class DemoItemLabel extends StatelessWidget {
const DemoItemLabel({ Key? key, this.index }) : super(key: key);
final int? index;
@override
Widget build(BuildContext context) {
return CircleAvatar(
minRadius: 4,
maxRadius: 20,
backgroundColor: Colors.grey.shade800,
foregroundColor: Colors.white,
child: Text(index == null ? '-' : '$index'),
);
}
}
class DemoItem extends StatefulWidget {
const DemoItem({
Key? key,
required this.index,
required this.color,
required this.isSelected,
required this.nestedSplitViews,
this.onTap,
}) : super(key: key);
final int index;
final Color color;
final bool isSelected;
final VoidCallback? onTap;
final bool nestedSplitViews;
@override
State<DemoItem> createState() => _DemoItemState();
}
class _DemoItemState extends State<DemoItem> with TickerProviderStateMixin {
late AnimationController colorOpacityController;
late AnimationController borderOpacityController;
late Animation<double> colorOpacity;
late Animation<double> borderOpacity;
@override
void initState() {
super.initState();
colorOpacityController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this
);
borderOpacityController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this
);
colorOpacity = colorOpacityController
.drive(CurveTween(curve: Curves.easeIn))
.drive(Tween<double>(begin: 0.5, end: 0.85));
borderOpacity = borderOpacityController
.drive(CurveTween(curve: Curves.easeIn))
.drive(Tween<double>(begin: 0, end: 1));
}
@override
void dispose() {
colorOpacityController.dispose();
borderOpacityController.dispose();
super.dispose();
}
@override
void didUpdateWidget(DemoItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isSelected != oldWidget.isSelected) {
if (widget.isSelected) {
colorOpacityController.forward();
} else {
colorOpacityController.reverse();
}
}
}
void handleEnter(PointerEnterEvent event) {
borderOpacityController.forward();
}
void handleExit(PointerExitEvent event) {
borderOpacityController.reverse();
}
Widget animatedBuild(BuildContext context, Widget? child) {
return DecoratedBox(
decoration: BoxDecoration(
color: widget.color.withOpacity(colorOpacity.value),
border: Border.all(color: Colors.grey.shade800.withOpacity(borderOpacity.value)),
),
child: child,
);
}
@override
Widget build(BuildContext context) {
return MouseRegion(
opaque: false,
onEnter: handleEnter,
onExit: handleExit,
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[
colorOpacityController.view,
borderOpacityController.view
]),
builder: animatedBuild,
child: Center(child: DemoItemLabel(index: widget.index)),
),
),
);
}
}
@immutable
class SplitConstraintsData {
const SplitConstraintsData({
this.weight = 1,
this.minExtent = 0,
this.maxExtent = double.infinity,
this.inputPadding = 0,
});
final double? weight;
final double minExtent;
final double maxExtent;
final double inputPadding;
SplitConstraintsData copyWith({
double? weight,
double? minExtent,
double? maxExtent,
double? inputPadding,
}) {
return SplitConstraintsData(
weight: weight ?? this.weight,
minExtent: minExtent ?? this.minExtent,
maxExtent: maxExtent ?? this.maxExtent,
inputPadding: inputPadding ?? this.inputPadding,
);
}
@override
String toString() {
String maxExtentString = maxExtent.isInfinite ? ' ${String.fromCharCode(0x221e)}' : '${maxExtent.floor()}';
return 'SplitConstraintsData[weight=${weight?.floor()}, minExtent=${minExtent.floor()}, maxExtent=$maxExtentString, inputPadding=${inputPadding.floor()}]';
}
}
class SelectedItemConstraintsView extends StatefulWidget {
const SelectedItemConstraintsView({
Key? key,
required this.index,
required this.axis,
required this.data,
required this.onDataChanged,
}) : super(key: key);
final int index;
final Axis axis;
final SplitConstraintsData data;
final ValueChanged<SplitConstraintsData> onDataChanged;
@override
State<SelectedItemConstraintsView> createState() => _SelectedItemConstraintsViewState();
}
class _SelectedItemConstraintsViewState extends State<SelectedItemConstraintsView> {
static const Widget spacer = SizedBox(width: 24);
late final TextEditingController minController;
late final TextEditingController maxController;
late final TextEditingController weightController;
String? minErrorText;
String? maxErrorText;
String? weightErrorText;
String get maxExtentString => widget.data.maxExtent.isInfinite ? String.fromCharCode(0x221e) : '${widget.data.maxExtent.floor()}';
String get weightString => widget.data.weight == null ? '<none>' : '${widget.data.weight!.floor()}';
@override
void initState() {
super.initState();
minController = TextEditingController(text: '${widget.data.minExtent.floor()}');
maxController = TextEditingController(text: maxExtentString);
weightController = TextEditingController(text: weightString);
}
@override
void dispose() {
minController.dispose();
maxController.dispose();
weightController.dispose();
super.dispose();
}
@override
void didUpdateWidget(SelectedItemConstraintsView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.index != oldWidget.index) {
minController.text = '${widget.data.minExtent.floor()}';
maxController.text = maxExtentString;
weightController.text = weightString;
}
}
@override
Widget build(BuildContext context) {
late final String dimension;
switch (widget.axis) {
case Axis.horizontal:
dimension = 'Width';
break;
case Axis.vertical:
dimension = 'Height';
break;
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
spacer,
SizedBox(
width: 96,
child: TextField(
controller: minController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Min $dimension',
errorText: minErrorText,
),
onSubmitted: (String? stringValue) {
if (stringValue != null) {
final double? value = double.tryParse(stringValue);
if (value != null && value >= 0 && value <= widget.data.maxExtent) {
if (minErrorText != null) {
setState(() { minErrorText = null; });
}
widget.onDataChanged(widget.data.copyWith(minExtent: value));
} else {
if (minErrorText == null) {
setState(() { minErrorText = 'invalid'; });
}
}
}
},
),
),
spacer,
SizedBox(
width: 96,
child: TextField(
controller: maxController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Max $dimension',
),
onSubmitted: (String? stringValue) {
if (stringValue != null) {
final double? value = double.tryParse(stringValue);
if (value != null && value >= widget.data.minExtent) {
if (maxErrorText != null) {
setState(() { maxErrorText = null; });
}
widget.onDataChanged(widget.data.copyWith(maxExtent: value));
} else {
if (maxErrorText == null) {
setState(() { maxErrorText = 'invalid'; });
}
}
}
},
),
),
],
);
}
}
class SelectedItemView extends StatefulWidget {
const SelectedItemView({
Key? key,
required this.itemCount,
required this.index,
required this.data,
required this.axis,
required this.textDirection,
required this.onItemCountChanged,
required this.onAxisChanged,
required this.onTextDirectionChanged,
required this.onDataChanged,
}) : super(key: key);
final int itemCount;
final int? index;
final SplitConstraintsData? data;
final Axis axis;
final TextDirection textDirection;
final ValueChanged<int?> onItemCountChanged;
final ValueChanged<Axis?> onAxisChanged;
final ValueChanged<TextDirection?> onTextDirectionChanged;
final ValueChanged<SplitConstraintsData> onDataChanged;
@override
State<SelectedItemView> createState() => _SelectedItemViewState();
}
class _SelectedItemViewState extends State<SelectedItemView> {
static const Widget spacer = SizedBox(width: 24);
@override
Widget build(BuildContext context) {
return Material(
elevation: 8,
child: Container(
color: Theme.of(context).colorScheme.background,
height: 128,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 24,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
DemoItemLabel(index: widget.index),
if (widget.index != null && widget.data != null)
SelectedItemConstraintsView(
index: widget.index!,
axis: widget.axis,
data: widget.data!,
onDataChanged: widget.onDataChanged,
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
DropdownButton<int>(
value: widget.itemCount,
onChanged: widget.onItemCountChanged,
items: <int>[2, 3, 5, 8].map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(' $value items'),
);
}).toList(),
),
spacer,
DropdownButton<TextDirection>(
value: widget.textDirection,
onChanged: widget.onTextDirectionChanged,
items: const <DropdownMenuItem<TextDirection>>[
DropdownMenuItem<TextDirection>(
value: TextDirection.ltr,
child: Text('LTR'),
),
DropdownMenuItem<TextDirection>(
value: TextDirection.rtl,
child: Text('RTL'),
),
],
),
spacer,
DropdownButton<Axis>(
value: widget.axis,
onChanged: widget.onAxisChanged,
items: const <DropdownMenuItem<Axis>>[
DropdownMenuItem<Axis>(
value: Axis.horizontal,
child: Text('Horizontal'),
),
DropdownMenuItem<Axis>(
value: Axis.vertical,
child: Text('Vertical'),
),
],
),
const SizedBox(width: 16),
],
),
),
],
),
),
);
}
}
class DemoSplitter extends StatelessWidget {
const DemoSplitter({
Key? key,
required this.axis,
}) : super(key: key);
final Axis axis;
@override
Widget build(BuildContext context) {
final MouseCursor cursor = axis == Axis.horizontal
? SystemMouseCursors.resizeLeftRight
: SystemMouseCursors.resizeUpDown;
return SplitConstraints(
inputPadding: 0,
cursor: cursor,
child: Container(
color: Theme.of(context).colorScheme.secondary.withOpacity(.75),
width: 16,
height: 16,
),
);
}
}
class Demo extends StatefulWidget {
const Demo({ Key? key }) : super(key: key);
@override
State<Demo> createState() => _DemoState();
}
class _DemoState extends State<Demo> {
int itemCount = 5;
bool nestedSplitViews = false;
int? selectedIndex;
Axis axis = Axis.horizontal;
TextDirection textDirection = TextDirection.ltr;
late List<SplitConstraintsData> splitConstraintsData;
void initSplitConstraintsData() {
splitConstraintsData = <SplitConstraintsData>[
for(int index = 0; index < itemCount; index += 1)
const SplitConstraintsData()
];
}
@override
void initState() {
super.initState();
initSplitConstraintsData();
}
void handleTap(int index) {
setState(() {
selectedIndex = index == selectedIndex ? null : index;
});
}
@override
Widget build(BuildContext context) {
Widget buildSplitter(int index) {
return DemoSplitter(
axis: axis,
);
}
Widget buildItem(int index) {
final SplitConstraintsData data = splitConstraintsData[index];
return SplitConstraints(
weight: data.weight,
minExtent: data.minExtent,
maxExtent: data.maxExtent,
inputPadding: data.inputPadding,
child: DemoItem(
index: index,
color: Color.lerp(Colors.green, Colors.indigo, index / itemCount)!,
isSelected: index == selectedIndex,
onTap: () { handleTap(index); },
nestedSplitViews: nestedSplitViews,
),
);
}
return Scaffold(
body: Column(
children: <Widget>[
SelectedItemView(
itemCount: itemCount,
index: selectedIndex,
data: selectedIndex == null ? null : splitConstraintsData[selectedIndex!],
axis: axis,
textDirection: textDirection,
onItemCountChanged: (int? newValue) {
if (newValue != null) {
setState(() {
itemCount = newValue;
selectedIndex = null;
initSplitConstraintsData();
});
}
},
onAxisChanged: (Axis? newValue) {
if (newValue != null) {
setState(() { axis = newValue; });
}
},
onTextDirectionChanged: (TextDirection? newValue) {
if (newValue != null) {
setState(() { textDirection = newValue; });
}
},
onDataChanged: (SplitConstraintsData newValue) {
setState(() {
splitConstraintsData[selectedIndex!] = newValue;
print('selectedIndex=$selectedIndex $newValue');
});
},
),
Expanded(
child: Directionality(
textDirection: textDirection,
child: SplitView(
axis: axis,
children: <Widget>[
for (int index = 0; index < itemCount * 2 - 1; index += 1)
index.isOdd ? buildSplitter(index) : buildItem(index ~/ 2)
],
),
),
),
],
),
);
}
}
class NestedSplitDemo extends StatefulWidget {
const NestedSplitDemo({ Key? key }) : super(key: key);
@override
State<NestedSplitDemo> createState() => _NestedSplitDemoState();
}
class _NestedSplitDemoState extends State<NestedSplitDemo> {
@override
Widget build(BuildContext context) {
return SplitView(
axis: Axis.horizontal,
children: <Widget>[
SplitConstraints(
weight: 1,
minExtent: 48,
child: Container(
color: Colors.orange.withOpacity(0.15),
child: SplitView(
axis: Axis.vertical,
children: <Widget>[
SplitConstraints(
weight: 1,
minExtent: 48,
child: Container(
color: Colors.orange.withOpacity(0.15),
child: const Placeholder(color: Colors.orange),
),
),
SplitConstraints(
inputPadding: 20,
cursor: SystemMouseCursors.resizeUpDown,
child: Container(
color: Colors.white,
width: 8,
height: 8,
),
),
SplitConstraints(
weight: 1,
minExtent: 48,
child: Container(
color: Colors.orange.withOpacity(0.15),
child: const Placeholder(color: Colors.orange),
),
),
],
),
),
),
SplitConstraints(
inputPadding: 20,
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(
color: Colors.white,
width: 8,
height: 8,
),
),
SplitConstraints(
weight: 1,
minExtent: 48,
child: Container(
color: Colors.indigo.withOpacity(0.15),
child: const Placeholder(color: Colors.indigo),
),
),
],
);
}
}
class DemoNavigator extends StatefulWidget {
const DemoNavigator({ Key? key }) : super(key: key);
@override
State<DemoNavigator> createState() => _DemoNavigatorState();
}
class _DemoNavigatorState extends State<DemoNavigator> {
DemoId demoId = DemoId.splitView;
@override
Widget build(BuildContext context) {
late final String title;
late final Widget demo;
switch(demoId) {
case DemoId.splitView:
title = 'SplitView Demo';
demo = const Demo();
break;
case DemoId.nestedSplitViews:
title = 'Nested SplitViews Demo';
demo = const NestedSplitDemo();
break;
}
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
PopupMenuButton(
onSelected: (DemoId id) {
setState(() { demoId = id; });
},
itemBuilder: (BuildContext context) {
return const [
PopupMenuItem<DemoId>(
value: DemoId.splitView,
child: Text('SplitView Demo'),
),
PopupMenuItem<DemoId>(
value: DemoId.nestedSplitViews,
child: Text('Nested SplitViews Demo'),
),
];
},
),
const SizedBox(width: 16),
],
),
body: demo,
);
}
}
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.blue),
home: const DemoNavigator(),
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment