Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Last active April 29, 2020 05:49
Show Gist options
  • Select an option

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

Select an option

Save HansMuller/ef14543f27c9708a3ffa0f021e8c7817 to your computer and use it in GitHub Desktop.
/*
This example defines a widget called "Destination" that displays two
widgets in a column: an "icon" and a "label". The Destination widget's
height is the height of the icon + the height of the label multiplied
by the Destination's "progress" parameter, a value between 0 and 1.
When progress is zero, the Destination's height is the same as the
icon's height, and when progress is 1.0 the Destination's height is the
sum of the icon and label's height
Tapping on the demo animates the Destination's progress value. The
Destination widget is clipped, so that only the part of the label exposed
by the progress parameter appears.
*/
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
enum _DestinationSlot {
icon,
label,
}
class Destination extends RenderObjectWidget {
const Destination({ Key key, this.icon, this.label, this.progress }) : super(key: key);
final Widget icon;
final Widget label;
final double progress;
@override
_DestinationElement createElement() => _DestinationElement(this);
@override
_RenderDestination createRenderObject(BuildContext context) {
return _RenderDestination(progress: progress);
}
@override
void updateRenderObject(BuildContext context, _RenderDestination renderObject) {
renderObject.progress = progress;
}
}
class _DestinationElement extends RenderObjectElement {
_DestinationElement(Destination widget) : super(widget);
final Map<_DestinationSlot, Element> slotToChild = <_DestinationSlot, Element>{};
final Map<Element, _DestinationSlot> childToSlot = <Element, _DestinationSlot>{};
@override
Destination get widget => super.widget as Destination;
@override
_RenderDestination get renderObject => super.renderObject as _RenderDestination;
@override
void visitChildren(ElementVisitor visitor) {
slotToChild.values.forEach(visitor);
}
@override
void forgetChild(Element child) {
assert(slotToChild.values.contains(child));
assert(childToSlot.keys.contains(child));
final _DestinationSlot slot = childToSlot[child];
childToSlot.remove(child);
slotToChild.remove(slot);
super.forgetChild(child);
}
void _mountChild(Widget widget, _DestinationSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
slotToChild.remove(slot);
childToSlot.remove(oldChild);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_mountChild(widget.icon, _DestinationSlot.icon);
_mountChild(widget.label, _DestinationSlot.label);
}
void _updateChild(Widget widget, _DestinationSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void update(Destination newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_updateChild(widget.icon, _DestinationSlot.icon);
_updateChild(widget.label, _DestinationSlot.label);
}
void _updateRenderObject(RenderBox child, _DestinationSlot slot) {
switch (slot) {
case _DestinationSlot.icon:
renderObject.icon = child;
break;
case _DestinationSlot.label:
renderObject.label = child;
break;
}
}
@override
void insertChildRenderObject(RenderObject child, dynamic slotValue) {
assert(child is RenderBox);
assert(slotValue is _DestinationSlot);
final _DestinationSlot slot = slotValue as _DestinationSlot;
_updateRenderObject(child as RenderBox, slot);
assert(renderObject.childToSlot.keys.contains(child));
assert(renderObject.slotToChild.keys.contains(slot));
}
@override
void removeChildRenderObject(RenderObject child) {
assert(child is RenderBox);
assert(renderObject.childToSlot.keys.contains(child));
_updateRenderObject(null, renderObject.childToSlot[child]);
assert(!renderObject.childToSlot.keys.contains(child));
assert(!renderObject.slotToChild.keys.contains(slot));
}
@override
void moveChildRenderObject(RenderObject child, dynamic slotValue) {
assert(false, 'not reachable');
}
}
class _RenderDestination extends RenderBox {
_RenderDestination({ @required double progress }) : assert(progress != null), _progress = progress;
final Map<_DestinationSlot, RenderBox> slotToChild = <_DestinationSlot, RenderBox>{};
final Map<RenderBox, _DestinationSlot> childToSlot = <RenderBox, _DestinationSlot>{};
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _DestinationSlot slot) {
if (oldChild != null) {
dropChild(oldChild);
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
childToSlot[newChild] = slot;
slotToChild[slot] = newChild;
adoptChild(newChild);
}
return newChild;
}
RenderBox _icon;
RenderBox get icon => _icon;
set icon(RenderBox value) {
_icon = _updateChild(_icon, value, _DestinationSlot.icon);
}
RenderBox _label;
RenderBox get label => _label;
set label(RenderBox value) {
_label = _updateChild(_label, value, _DestinationSlot.label);
}
double get progress => _progress;
double _progress;
set progress(double value) {
assert(value != null);
if (_progress == value)
return;
_progress = value;
markNeedsLayout();
}
// The returned list is ordered for hit testing.
Iterable<RenderBox> get _children sync* {
if (icon != null)
yield icon;
if (label != null)
yield label;
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (final RenderBox child in _children)
child.attach(owner);
}
@override
void detach() {
super.detach();
for (final RenderBox child in _children)
child.detach();
}
@override
void redepthChildren() {
_children.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
_children.forEach(visitor);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
void add(RenderBox child, String name) {
if (child != null)
value.add(child.toDiagnosticsNode(name: name));
}
add(icon, 'icon');
add(label, 'label');
return value;
}
@override
bool get sizedByParent => false;
static double _minWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
}
static double _maxWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicWidth(double height) {
final double iconWidth = icon == null ? 0 : icon.getMinIntrinsicWidth(height);
final double labelWidth = label == null ? 0 : label.getMinIntrinsicWidth(height);
return math.max(iconWidth, labelWidth);
}
@override
double computeMaxIntrinsicWidth(double height) {
final double iconWidth = icon == null ? 0 : icon.getMaxIntrinsicWidth(height);
final double labelWidth = label == null ? 0 : label.getMaxIntrinsicWidth(height);
return math.max(iconWidth, labelWidth);
}
@override
double computeMinIntrinsicHeight(double width) {
final double iconHeight = icon == null ? 0 : icon.getMaxIntrinsicHeight(width);
final double labelHeight = label == null ? 0 : label.getMaxIntrinsicHeight(width);
return iconHeight + labelHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
return computeMinIntrinsicHeight(width);
}
static Size _layoutBox(RenderBox box, BoxConstraints constraints) {
if (box == null)
return Size.zero;
box.layout(constraints, parentUsesSize: true);
return box.size;
}
static void _positionBox(RenderBox box, Offset offset) {
final BoxParentData parentData = box.parentData as BoxParentData;
parentData.offset = offset;
}
@override
void performLayout() {
final Size iconSize = _layoutBox(icon, constraints);
final Size labelSize = _layoutBox(label, constraints);
final double width = math.max(iconSize.width, labelSize.width);
final double height = iconSize.height + labelSize.height * progress;
_positionBox(icon, Offset((width - iconSize.width) / 2, 0));
_positionBox(label, Offset((width - labelSize.width) / 2, iconSize.height));
size = constraints.constrain(Size(width, height));
assert(size.width == constraints.constrainWidth(width));
assert(size.height == constraints.constrainHeight(height));
}
@override
void paint(PaintingContext context, Offset offset) {
void doPaint(RenderBox child) {
if (child != null) {
final BoxParentData parentData = child.parentData as BoxParentData;
context.paintChild(child, parentData.offset + offset);
}
}
doPaint(icon);
doPaint(label);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(BoxHitTestResult result, { @required Offset position }) {
assert(position != null);
for (final RenderBox child in _children) {
final BoxParentData parentData = child.parentData as BoxParentData;
final bool isHit = result.addWithPaintOffset(
offset: parentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - parentData.offset);
return child.hitTest(result, position: transformed);
},
);
if (isHit)
return true;
}
return false;
}
}
class AnimationDemo extends StatefulWidget {
@override
_AnimationDemoState createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Scaffold(
body: Center(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.display2,
child: Container(
color: Colors.grey.withOpacity(0.15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Above'),
Container(
color: Colors.blue.withOpacity(0.15),
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return ClipRect(
child: Destination(
icon: Icon(Icons.android, size: 48),
label: Text('Hello World'), // Try 'Hello\nWorld' instead
progress: _controller.value,
),
);
},
),
),
Text('Below'),
],
),
),
),
),
),
);
}
}
void main() {
runApp(MaterialApp(title: 'Reveal Transition Demo', home: AnimationDemo()));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment