Skip to content

Instantly share code, notes, and snippets.

@jogboms
Last active May 30, 2022 21:00
Show Gist options
  • Save jogboms/1c969075f50b6155ff7b7aab9b1e171a to your computer and use it in GitHub Desktop.
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) {
return 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,
BoxConstraints.tight(Constants.knobTrackSize),
);
positionChild(
SliderChildSlot.knobTrack,
Offset.zero.shift(Constants.knobToTrackPadding),
);
layoutChild(
SliderChildSlot.knob,
BoxConstraints.tight(Constants.knobSize),
);
positionChild(
SliderChildSlot.knob,
Offset(knobLeftPosition, 0),
);
}
@override
bool shouldRelayout(SliderLayoutDelegate oldDelegate) => knobLeftPosition != oldDelegate.knobLeftPosition;
}
class Constants {
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