Skip to content

Instantly share code, notes, and snippets.

@jogboms
Last active January 20, 2026 06:19
Show Gist options
  • Select an option

  • Save jogboms/1c969075f50b6155ff7b7aab9b1e171a to your computer and use it in GitHub Desktop.

Select an option

Save jogboms/1c969075f50b6155ff7b7aab9b1e171a to your computer and use it in GitHub Desktop.
Flutter guild presentation - Multi-child layout demo
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: App()));
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Center(child: SliderWidget()));
}
class SliderWidget extends StatefulWidget {
const SliderWidget({super.key});
@override
State<SliderWidget> createState() => _SliderWidgetState();
}
class _SliderWidgetState extends State<SliderWidget> {
final GlobalKey _sliderKey = GlobalKey();
final GlobalKey _knobKey = GlobalKey();
final GlobalKey _knobTrackKey = GlobalKey();
double _knobLeftPosition = 0.0;
Color _knobColor = Constants.gradientColors.first;
@override
Widget build(BuildContext context) => SizedBox.fromSize(
key: _sliderKey,
size: Constants.size,
child: CustomMultiChildLayout(
delegate: SliderLayoutDelegate(knobLeftPosition: _knobLeftPosition),
children: [
LayoutId(
id: SliderChildSlot.knobTrack,
child: DecoratedBox(
key: _knobTrackKey,
decoration: BoxDecoration(
gradient: Constants.gradient,
borderRadius: Constants.borderRadius,
),
),
),
LayoutId(
id: SliderChildSlot.knob,
child: GestureDetector(
key: _knobKey,
onHorizontalDragStart: (_) => _onDragStart(),
onHorizontalDragUpdate: (details) => _onDragUpdate(details.primaryDelta!),
child: Material(
shape: CircleBorder(
side: BorderSide(
color: Constants.knobBackgroundColor,
width: Constants.knobToTrackPadding,
),
),
shadowColor: Constants.knobShadowColor,
color: _knobColor,
elevation: Constants.knobElevation,
),
),
),
],
),
);
double _dragLeftPosition = 0.0;
void _onDragStart() {
_dragLeftPosition = _knobKey.rect.left;
}
void _onDragUpdate(double delta) {
final sliderRect = _sliderKey.rect;
final knobTrackRect = _knobTrackKey.rect;
final lowerLimit = knobTrackRect.left - Constants.knobRadius;
final upperLimit = knobTrackRect.right - Constants.knobRadius;
final dragLeftPosition = (_dragLeftPosition + delta).clamp(lowerLimit, upperLimit);
if (_dragLeftPosition == dragLeftPosition) {
return;
}
setState(() {
_dragLeftPosition = dragLeftPosition;
_knobLeftPosition = _dragLeftPosition - sliderRect.left;
_knobColor = Constants.gradientColors.mix(lowerLimit, upperLimit, _dragLeftPosition);
});
}
}
enum SliderChildSlot { knob, knobTrack }
class SliderLayoutDelegate extends MultiChildLayoutDelegate {
SliderLayoutDelegate({required this.knobLeftPosition});
final double knobLeftPosition;
@override
void performLayout(Size size) {
layoutChild(SliderChildSlot.knobTrack, .tight(Constants.knobTrackSize));
positionChild(SliderChildSlot.knobTrack, Offset.zero.shift(Constants.knobToTrackPadding));
layoutChild(SliderChildSlot.knob, .tight(Constants.knobSize));
positionChild(SliderChildSlot.knob, Offset(knobLeftPosition, 0));
}
@override
bool shouldRelayout(SliderLayoutDelegate oldDelegate) =>
knobLeftPosition != oldDelegate.knobLeftPosition;
}
interface class Constants {
static const size = Size(720, 120);
static final knobDimension = size.height;
static final knobSize = Size.square(knobDimension);
static final knobRadius = knobDimension / 2;
static final knobToTrackPadding = knobDimension * .15;
static final knobTrackSize = Size(size.width - knobToTrackPadding * 2, size.height - knobToTrackPadding * 2);
static const knobBackgroundColor = Colors.white;
static const knobElevation = 8.0;
static const knobShadowColor = Colors.black87;
static final borderRadius = BorderRadius.circular(size.height / 2);
static const gradientColors = [Colors.blueAccent, Colors.purpleAccent];
static const gradient = LinearGradient(colors: gradientColors);
}
// https://stackoverflow.com/a/55088673/8236404
double Function(double input) interpolate({
double inputMin = 0,
double inputMax = 1,
double outputMin = 0,
double outputMax = 1,
}) {
assert(inputMin != inputMax || outputMin != outputMax);
final diff = (outputMax - outputMin) / (inputMax - inputMin);
return (input) => ((input - inputMin) * diff) + outputMin;
}
extension RenderBoxRectExtension<T extends State> on GlobalKey<T> {
Rect get rect {
final renderBox = currentContext!.findRenderObject() as RenderBox;
return renderBox.localToGlobal(Offset.zero) & renderBox.size;
}
}
extension ShiftExtension on Offset {
Offset shift(double delta) => translate(delta, delta);
}
extension MixGradientColor on List<Color> {
Color mix(double min, double max, double value) =>
Color.lerp(first, last, interpolate(inputMin: min, inputMax: max)(value))!;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment