Skip to content

Instantly share code, notes, and snippets.

@wesleybliss
Created July 7, 2022 04:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wesleybliss/c23a8732dcf8d3523db6697946376738 to your computer and use it in GitHub Desktop.
Save wesleybliss/c23a8732dcf8d3523db6697946376738 to your computer and use it in GitHub Desktop.
Resizable split view widget
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 2022-07-06 Updated to null safety
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// A widget that takes two children, lays them out along [axis], and allows
/// the user to resize them.
///
/// The user can customize the amount of space allocated to each child by
/// dragging a divider between them.
///
/// [initialFirstFraction] defines how much space to give the [firstChild]
/// when first building this widget. [secondChild] will take the remaining
/// space.
///
/// The user can drag the widget with key [dividerKey] to change
/// the space allocated between [firstChild] and [secondChild].
// TODO: introduce support for a minimum fraction a child
// is allowed.
class SplitView extends StatefulWidget {
/// Builds a split oriented along [axis].
SplitView({
Key? key,
required this.axis,
required this.firstChild,
required this.secondChild,
this.initialFirstFraction = 0.5,
this.dividerMainAxisSize = 10.0,
this.dividerColor = Colors.black,
Widget? dividerIcon,
this.dividerIconColor = Colors.white,
this.dividerShowIcon = true,
}) : dividerIcon = dividerIcon ??
Icon(
Icons.drag_indicator,
color: dividerIconColor,
),
super(key: key);
/// The main axis the children will lay out on.
///
/// If [Axis.horizontal], the children will be placed in a [Row]
/// and they will be horizontally resizable.
///
/// If [Axis.vertical], the children will be placed in a [Column]
/// and they will be vertically resizable.
///
/// Cannot be null.
final Axis axis;
/// The child that will be laid out first along [axis].
final Widget firstChild;
/// The child that will be laid out last along [axis].
final Widget secondChild;
/// The fraction of the layout to allocate to [firstChild].
///
/// [secondChild] will receive a fraction of `1 - initialFirstFraction`.
final double initialFirstFraction;
/// The size of the divider between [firstChild] and [secondChild] in
/// logical pixels (dp, not px).
final double dividerMainAxisSize;
/// The background color of the divider
final Color dividerColor;
/// Icon centered in the middle of the divider
final Widget dividerIcon;
/// Color for the default divider icon
final Color dividerIconColor;
/// If the divider icon should be visible
final bool dividerShowIcon;
/// The key passed to the divider between [firstChild] and [secondChild].
///
/// Visible to grab it in tests.
@visibleForTesting
Key get dividerKey => Key('$this dividerKey');
static Axis axisFor(BuildContext context, double horizontalAspectRatio) {
final screenSize = MediaQuery.of(context).size;
final aspectRatio = screenSize.width / screenSize.height;
if (aspectRatio >= horizontalAspectRatio) {
return Axis.horizontal;
}
return Axis.vertical;
}
@override
State<StatefulWidget> createState() => _SplitViewState();
}
class _SplitViewState extends State<SplitView> {
double firstFraction = 0.0;
double get secondFraction => 1 - firstFraction;
bool get isHorizontal => widget.axis == Axis.horizontal;
@override
void initState() {
super.initState();
firstFraction = widget.initialFirstFraction;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: _buildLayout);
}
Widget _buildLayout(BuildContext context, BoxConstraints constraints) {
final width = constraints.maxWidth;
final height = constraints.maxHeight;
final axisSize = isHorizontal ? width : height;
final halfDivider = widget.dividerMainAxisSize / 2.0;
// Determine what fraction to give each child, including enough space to
// display the divider.
double firstSize = axisSize * firstFraction;
double secondSize = axisSize * secondFraction;
// Clamp the sizes to be sure there is enough space for the dividers.
firstSize = firstSize.clamp(halfDivider, axisSize - halfDivider);
secondSize = secondSize.clamp(halfDivider, axisSize - halfDivider);
// Remove space from each child to place the divider in the middle.
firstSize = firstSize - halfDivider;
secondSize = secondSize - halfDivider;
void updateSpacing(DragUpdateDetails dragDetails) {
final delta = isHorizontal ? dragDetails.delta.dx : dragDetails.delta.dy;
final fractionalDelta = delta / axisSize;
setState(() {
// Update the fraction of space consumed by the children,
// being sure not to allocate any negative space.
firstFraction += fractionalDelta;
firstFraction = firstFraction.clamp(0.0, 1.0);
});
}
// TODO(https://github.com/flutter/devtools/issues/1265): update mouse
// to indicate that this is resizable.
final dragIcon = widget.dividerShowIcon
? Align(
alignment: const Alignment(1.0, 1.0),
child: OverflowBox(
minWidth: 0.0,
maxWidth: 100.0,
minHeight: 0.0,
maxHeight: 50.0,
child: widget.dividerIcon,
),
)
: null;
final dragIndicator = MouseRegion(
cursor: isHorizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
child: Container(
width: isHorizontal ? widget.dividerMainAxisSize : width,
height: isHorizontal ? height : widget.dividerMainAxisSize,
color: widget.dividerColor,
child: dragIcon,
),
);
final children = [
SizedBox(
width: isHorizontal ? firstSize : width,
height: isHorizontal ? height : firstSize,
child: widget.firstChild,
),
GestureDetector(
key: widget.dividerKey,
behavior: HitTestBehavior.translucent,
onHorizontalDragUpdate: isHorizontal ? updateSpacing : null,
onVerticalDragUpdate: isHorizontal ? null : updateSpacing,
// DartStartBehavior.down is needed to keep the mouse pointer stuck to
// the drag bar. There still appears to be a few frame lag before the
// drag action triggers which isn't ideal but isn't a launch blocker.
dragStartBehavior: DragStartBehavior.down,
child: SizedBox(
width: isHorizontal ? widget.dividerMainAxisSize : width,
height: isHorizontal ? height : widget.dividerMainAxisSize,
child: Center(
child: dragIndicator,
),
),
),
SizedBox(
width: isHorizontal ? secondSize : width,
height: isHorizontal ? height : secondSize,
child: widget.secondChild,
),
];
return Flex(direction: widget.axis, children: children);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment