Skip to content

Commit

Permalink
Introduce a vertical/horizontal split layout (#1261)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveShuckerow committed Oct 29, 2019
1 parent 549b225 commit 4384ed3
Show file tree
Hide file tree
Showing 2 changed files with 445 additions and 0 deletions.
176 changes: 176 additions & 0 deletions packages/devtools_app/lib/src/flutter/split.dart
@@ -0,0 +1,176 @@
// 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.

import 'dart:math';

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(djshuckerow): introduce support for a minimum fraction a child is allowed.
class Split extends StatefulWidget {
/// Builds a split oriented along [axis].
const Split({
Key key,
@required this.axis,
@required this.firstChild,
@required this.secondChild,
double initialFirstFraction,
}) : initialFirstFraction = initialFirstFraction ?? 0.5,
assert(axis != null),
assert(firstChild != null),
assert(secondChild != null),
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 key passed to the divider between [firstChild] and [secondChild].
///
/// Visible to grab it in tests.
@visibleForTesting
Key get dividerKey => Key('$this dividerKey');

/// The size of the divider between [firstChild] and [secondChild] in
/// logical pixels (dp, not px).
static const double dividerMainAxisSize = 10.0;

@override
State<StatefulWidget> createState() => _SplitState();
}

class _SplitState extends State<Split> {
double firstFraction;
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 crossAxisSize = isHorizontal ? height : width;
const halfDivider = Split.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/flutter/issues/43747): use an icon.
// The material icon for a drag handle is not currently available.
// For now, draw an indicator that is 3 lines running in the direction
// of the main axis, like a hamburger menu.
// TODO(https://github.com/flutter/devtools/issues/1265): update mouse
// to indicate that this is resizable.
final dragIndicator = Flex(
direction: isHorizontal ? Axis.vertical : Axis.horizontal,
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < min(crossAxisSize / 6.0, 3).floor(); i++)
Padding(
padding: EdgeInsets.symmetric(
vertical: isHorizontal ? 2.0 : 0.0,
horizontal: isHorizontal ? 0.0 : 2.0,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).indicatorColor,
borderRadius: BorderRadius.circular(Split.dividerMainAxisSize),
),
child: SizedBox(
height: isHorizontal ? 2.0 : Split.dividerMainAxisSize - 2.0,
width: isHorizontal ? Split.dividerMainAxisSize - 2.0 : 2.0,
),
),
),
],
);

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,
child: SizedBox(
width: isHorizontal ? Split.dividerMainAxisSize : width,
height: isHorizontal ? height : Split.dividerMainAxisSize,
child: Center(
child: dragIndicator,
),
),
),
SizedBox(
width: isHorizontal ? secondSize : width,
height: isHorizontal ? height : secondSize,
child: widget.secondChild,
),
];
return Flex(direction: widget.axis, children: children);
}
}

0 comments on commit 4384ed3

Please sign in to comment.