Last active
July 2, 2020 11:04
-
-
Save blaugold/7811392b4c06836d844e48d97ec89cb0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
/// Overlays [overlay] over [anchor], when pushed. | |
/// | |
/// This route extends [PopupRoute], but if your implementing your own route | |
/// I suggest skimming the docs of [TransitionRoute], [ModalRoute] and | |
/// [PageRoute]. [PopupRoute] is just the most natural base class for a route | |
/// like this one. | |
class OverlayRoute<T> extends PopupRoute<T> with WidgetsBindingObserver { | |
OverlayRoute({ | |
@required this.anchor, | |
@required this.overlay, | |
}); | |
/// The context which contains the [RenderBox] which [overlay] will be | |
/// positioned on top of. | |
final BuildContext anchor; | |
/// The builder which builds the overlaid [Widget]. | |
final WidgetBuilder overlay; | |
/// We return false here to ensure that the source route, which contains | |
/// [anchor], is kept around. Otherwise [anchor] becomes unusable once this | |
/// route is pushed. | |
@override | |
bool get opaque => false; | |
/// The color of the barrier displayed between the source route and this | |
/// route. The barrier occupies the whole screen. | |
@override | |
Color get barrierColor => Colors.black26; | |
/// Returning `true` allows the user to pop this route by tapping on the | |
/// barrier. | |
@override | |
bool get barrierDismissible => true; | |
/// The semantics label for the barrier. This is necessary since the user | |
/// can interact with the barrier to pop the route. | |
@override | |
String get barrierLabel => 'Dismiss route'; | |
/// We don't animate the transition of this simple route. | |
@override | |
Duration get transitionDuration => Duration.zero; | |
/// This is the bounding [Rect] of the anchor in the coordinate space of | |
/// this route. | |
Rect _anchorRect; | |
void _updateAnchorRect() { | |
// [setState] is necessary to trigger a rebuild of [buildTransitions], | |
// where [_anchorRect] is used. | |
// [setState] and [buildTransitions] are analogous to [State.setState] | |
// and [State.build]. | |
setState(() { | |
_anchorRect = getAnchorRectInNavigator( | |
anchor.findRenderObject(), | |
navigator.context.findRenderObject(), | |
); | |
}); | |
} | |
/// This method comes from mixing in [WidgetsBindingObserver] and is | |
/// overridden to handle changes of the screen size, since these changes | |
/// can affect the layout of [anchor]. Try it out by resizing the window or | |
/// rotating the screen. | |
@override | |
void didChangeMetrics() { | |
_updateAnchorRect(); | |
} | |
@override | |
TickerFuture didPush() { | |
// This starts notifications of [didChangeMetrics]. | |
WidgetsBinding.instance.addObserver(this); | |
// This ensures that [_anchorRect] is up to date when this route is pushed. | |
_updateAnchorRect(); | |
return super.didPush(); | |
} | |
@override | |
bool didPop(T result) { | |
// This stops notifications of [didChangeMetrics]. | |
WidgetsBinding.instance.removeObserver(this); | |
// A route which does not need to react to screen size changes could | |
// update [_anchorRect] here, just before the pop transition, and avoid | |
// dealing with WidgetsBindingObserver. | |
// _updateAnchorRect(); | |
return super.didPop(result); | |
} | |
/// This method is only used to build the route content, in this case | |
/// [overlay]. | |
/// [_anchorRect] cannot be used here because this method is not triggered | |
/// by [setState]. | |
@override | |
Widget buildPage( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
) { | |
return overlay(context); | |
} | |
/// This method is the right place to do all the complex stuff, like | |
/// positioning the route content, decorating it or implementing | |
/// transition effects. | |
/// [Navigator] stacks all its routes in an [Overlay]. | |
/// We use a [Stack] to position [overlay] in the [OverlayEntry] of this | |
/// route. | |
@override | |
Widget buildTransitions( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
Widget child, | |
) { | |
return Stack( | |
children: [ | |
Positioned.fromRect( | |
rect: _anchorRect, | |
child: child, | |
), | |
], | |
); | |
} | |
} | |
Matrix4 getAnchorToNavigatorTransform( | |
RenderObject anchor, RenderObject navigator) { | |
Matrix4 anchorTransform = anchor.getTransformTo(null); | |
Matrix4 navigatorTransform = navigator.getTransformTo(null); | |
return anchorTransform..multiply(Matrix4.tryInvert(navigatorTransform)); | |
} | |
Rect getAnchorRectInNavigator(RenderObject anchor, RenderObject navigator) { | |
Matrix4 transform = getAnchorToNavigatorTransform(anchor, navigator); | |
Rect anchorRect = Offset.zero & (anchor as RenderBox).size; | |
return MatrixUtils.transformRect(transform, anchorRect); | |
} | |
class HomePage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Builder( | |
builder: (context) { | |
return IconButton( | |
icon: Icon(Icons.favorite), | |
onPressed: () { | |
Navigator.of(context).push(OverlayRoute( | |
anchor: context, | |
overlay: (_) => Material( | |
child: IconButton( | |
icon: Icon(Icons.close), | |
onPressed: () => Navigator.pop(context), | |
), | |
), | |
)); | |
}, | |
); | |
}, | |
), | |
), | |
); | |
} | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: HomePage(), | |
); | |
} | |
} | |
void main() { | |
runApp(MyApp()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment