Basically, I want to render a ModalRoute that is dependent on some widget in the route below it.
To achieve that I am using a GlobalKey which I attach to a widget in the lower route:
/// in LOWER route (widget that is in lower route)
#override
Widget build(BuildContext context) {
return Container(
key: globalKey,
child: ..,
);
}
/// UPPER route (different class!)
/// called using a function on tap in the lower route widget
/// `showModalRoute(globalKey)`
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final renderBox = globalKey.currentContext.findRenderObject() as RenderBox;
final Size size = renderBox.size;
return SizedBox(
width: size.width,
height: size.height,
child: ..,
);
}
I am trying to make this respond to orientation changes. The widget in the lower route changes size when the orientation changes.
The problem here is that the upper route seems to be built before the lower route. Maybe this is not the case, however, the size is always the previous size, i.e. I get the landscape size when rotating to potrait and vise versa as if the upper route was built before the lower route (my assumption). The same applies to the position. I basically get the previous RenderBox.
Is there any way for me to get the current position of my widget, i.e. via renderBox.localToGlobal(0, 0)? I imagine that I could achieve this by having the buildPage render after the GlobalKey has the new size.
Check this code tell me if it worked as you expected
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
StreamController<MyWidgetStatus> firstRouteStatus =
StreamController.broadcast();
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Orination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
GlobalKey _stateKey;
MyWidgetStatus _status;
double height;
double width;
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
_stateKey = GlobalKey();
SchedulerBinding.instance.addPostFrameCallback(_calculatePositionOffset);
super.initState();
}
_calculatePositionOffset(_) {
_status = _getPositions(_stateKey);
firstRouteStatus.add(_status);
print("position = ${_status.position}");
}
MyWidgetStatus _getPositions(_key) {
final RenderBox renderBoxRed = _key.currentContext.findRenderObject();
final position = renderBoxRed.localToGlobal(Offset.zero);
final height = renderBoxRed.constraints.maxHeight;
final width = renderBoxRed.constraints.maxWidth;
return MyWidgetStatus(position: position, width: width, hight: height);
}
void didChangeMetrics() {
print("Metrics changed");
SchedulerBinding.instance.addPostFrameCallback(_calculatePositionOffset);
super.didChangeMetrics();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigate_next),
onPressed: () {
_settingModalBottomSheet(context);
}),
body: OrientationBuilder(
builder: (context, orientation) {
return Center(
child: LayoutBuilder(
builder: (context, constraints) => SizedBox(
key: _stateKey,
height: orientation == Orientation.portrait ? 100.0 : 50,
width: orientation == Orientation.portrait ? 50.0 : 100.0,
child: Container(
color: Colors.red,
),
),
),
);
},
),
);
}
void _settingModalBottomSheet(context) {
showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return Scaffold(
body: StreamBuilder(
stream: firstRouteStatus.stream,
builder: (context, AsyncSnapshot<MyWidgetStatus> snapshot) =>
snapshot.hasData
? Container(
child: Text("Position = ${snapshot.data.position}"),
)
: Text("No Data"),
),
);
});
}
}
class MyWidgetStatus {
final Offset position;
final double hight;
final double width;
MyWidgetStatus({
this.position,
this.hight,
this.width,
});
}
Edit: if you need the information to be rendered at the beginning you can use a BehaviorSubject instead of the native StreamController like
import 'package:rxdart/rxdart.dart';
StreamController<MyWidgetStatus> firstRouteStatus =
BehaviorSubject();
you also have to add the RxDart package in pubspec.yaml it is 0.22.0 at the time of writing this line.
rxdart: ^0.22.0
Related
I have a preview widget that loads data after a user tap. This state (already tapped or not) should not be lost while scrolling (the preview is located in a list) or navigating through other screen.
The scrolling is solved by adding AutomaticKeepAliveClientMixin which saves the state when scrolling away.
Now i also need to wrap the preview widget (actually a more complex widget that contains the preview) with a RepaintBoundary, to be able to make a "screenshot" of this widget alone.
Before i wrap the widget with a RepaintBoundary, the state is saved both while scrolling and navigating to another screen.
After i add the RepaintBoundary the scrolling still works but for navigation the state is reset.
How can i wrap a Stateful widget that should hold its state with a RepaintBoundary?
Code is a simplified example of my implementation with the same problem.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Test';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: TestList(40),
),
);
}
}
class TestList extends StatefulWidget {
final int numberOfItems;
TestList(this.numberOfItems);
#override
_TestListState createState() => _TestListState();
}
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary(
key: GlobalKey(),
child: Preview()
);
},
);
}
}
class Preview extends StatefulWidget {
#override
_PreviewState createState() => _PreviewState();
}
class _PreviewState extends State<Preview> with AutomaticKeepAliveClientMixin {
bool loaded;
#override
void initState() {
super.initState();
print('_PreviewState initState.');
loaded = false;
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('_PreviewState build.');
if(loaded) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
},
child: ListTile(
title: Text('Loaded. Tap to navigate.'),
leading: Icon(Icons.visibility),
),
);
} else {
return GestureDetector(
onTap: () {
setState(() {
loaded = true;
});
},
child: ListTile(
title: Text('Tap to load.'),
),
);
}
}
}
class NewScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('New Screen')),
body: Center(
child: Text(
'Navigate back and see if loaded state is gone.',
style: TextStyle(fontSize: 14.0),
),
),
);
}
}
Take a look at RepaintBoundary.wrap, it assigns the RepaintBoundary widget a key based on its child or childIndex so state is maintained:
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary.wrap(
Preview(),
index,
);
},
);
}
}
https://api.flutter.dev/flutter/widgets/RepaintBoundary/RepaintBoundary.wrap.html
EDIT: As per the below comments, it looks like this solution would break the screenshot ability so you'd have to store the list of children widgets in your state like so:
class _TestListState extends State<TestList> {
List<Widget> _children;
#override
void initState() {
super.initState();
_children = List.generate(
widget.numberOfItems,
(_) => RepaintBoundary(
key: GlobalKey(),
child: Preview(),
));
}
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView(children: _children);
}
}
When the bottom child is tapped (the height changes when its tapped), the parent color (blue) is shown while animating.
Is there anyway to prevent the animation?
Should I be thinking about this in a different way?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(color: Colors.blue, child: CustomScrollView(slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate([
TapBox(color: Colors.red),
TapBox(color: Colors.white),
TapBox(color: Colors.red),
]),
)
],))
);
}
}
class TapBox extends StatefulWidget {
final Color color;
TapBox({ this.color });
#override
State<StatefulWidget> createState() => _TapBoxState();
}
class _TapBoxState extends State<TapBox> {
double height = 500;
onTap() {
setState(() {
if (height == 500) {
height = 250;
} else {
height = 500;
}
});
}
#override
Widget build(BuildContext context) {
return Material(child: InkWell(onTap: onTap, child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(width: constraints.maxWidth, height: height, color: widget.color);
})));
}
}
In iOS, I wrote a somewhat complex custom UIViewController that handles transitioning between unique child controllers; most notably, a special header view at the top of each one. I'm still trying to really wrap my head around end to end architecture in Flutter, and would like some suggestions on how to accomplish this. There are two types of headers - Arc and Profile, and each one goes from an expanded to a collapsed state as the user scrolls. Additionally, navigation between any combination of type and state can have a transition defined.
Here is how it looks when used in a TabBar for example. Transitions are handled gracefully wether nested in Tab/NavigationControllers or not.
This is what I threw together, I hope it helps (click for video):
Note:
It would be better to reduce the amount of animation controllers, ideally to a single controller that controls both the header extent and the arc curvature
There is no animation for the content below the header, but I'm sure you could add that as well.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anim playground',
theme: ThemeData(
brightness: Brightness.dark,
),
home: AnimatedPageTest(),
);
}
}
class AnimatedPageTest extends StatefulWidget {
#override
_AnimatedPageTestState createState() => _AnimatedPageTestState();
}
class _AnimatedPageTestState extends State<AnimatedPageTest> {
bool _arc = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: AnimatedPage(
appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
children: List.generate(30, (index) => ListTile(title: Text('index'),)),
),),
persistentFooterButtons: <Widget>[
FlatButton(
child: Text('Switch'),
onPressed: () {
setState(() {
_arc = !_arc;
});
},
)
],
);
}
}
enum HeaderAppearance { arc, profile }
double _getTargetMaxExtent(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 150.0;
} else {
return 75.0;
}
}
double _getTargetArcAnimationValue(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 1.0;
} else {
return 0.0;
}
}
class AnimatedPage extends StatefulWidget {
AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final List<Widget> children;
#override
_AnimatedPageState createState() => _AnimatedPageState();
}
class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
AnimationController _maxExtentAnimation;
#override
void initState() {
super.initState();
_maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
}
#override
void didUpdateWidget(AnimatedPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_maxExtentAnimation.animateTo(
_getTargetMaxExtent(widget.appearance),
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
}
#override
void dispose() {
_maxExtentAnimation.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _maxExtentAnimation,
builder: (context, child) {
return CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: AnimatedHeaderDelegate(
appearance: widget.appearance,
backgroundImage: widget.backgroundImage,
minExtent: 50.0,
maxExtent: _maxExtentAnimation.value,
),
),
child,
],
);
},
child: SliverList(delegate: SliverChildListDelegate(widget.children)),
);
}
}
class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});
final HeaderAppearance appearance;
final String backgroundImage;
#override
final double minExtent;
#override
final double maxExtent;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
return AnimatedHeader(
appearance: appearance,
backgroundImage: backgroundImage,
curvatureMultiplier: 1.0 - shrinkRelative,
);
}
#override
bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
return appearance != oldDelegate.appearance ||
minExtent != oldDelegate.minExtent ||
maxExtent != oldDelegate.maxExtent;
}
}
class AnimatedHeader extends StatefulWidget {
AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final double curvatureMultiplier;
#override
_AnimatedHeaderState createState() => _AnimatedHeaderState();
}
class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
AnimationController _arcAnimation;
#override
void initState() {
super.initState();
_arcAnimation = AnimationController(
vsync: this,
value: _getTargetArcAnimationValue(widget.appearance),
duration: Duration(milliseconds: 600),
);
}
#override
void didUpdateWidget(AnimatedHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
builder: (context, child) {
return ClipPath(
clipper: ArcClipper(
curvature: _arcAnimation.value * widget.curvatureMultiplier,
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: Container(
key: ValueKey(widget.backgroundImage),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.backgroundImage),
fit: BoxFit.cover,
),
),
),
),
Center(
child: Text(
'TITLE',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class ArcClipper extends CustomClipper<Path> {
ArcClipper({this.curvature});
final double curvature;
#override
Path getClip(Size size) {
if (curvature == 0.0) {
return Path()..addRect(Offset.zero & size);
} else {
return Path()
..moveTo(0.0, 0.0)
..lineTo(size.width, 0.0)
..lineTo(size.width, size.height)
..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
..close();
}
}
#override
bool shouldReclip(ArcClipper oldClipper) {
return curvature != oldClipper.curvature;
}
}
I want show a popup at touch Coordinates. I am using Stack and Positioned widgets to place the popup.
You can add a GestureDetector as parent of stack, and register onTapDownDetails listener. This should call your listener on every tapdown event, with global offset of the tap in TapDownDetails parameter of the your listener.
Here is sample code demonstrating the same.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Popup Demo'),
),
body: new MyWidget());
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
double posx = 100.0;
double posy = 100.0;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: new Stack(fit: StackFit.expand, children: <Widget>[
// Hack to expand stack to fill all the space. There must be a better
// way to do it.
new Container(color: Colors.white),
new Positioned(
child: new Text('hello'),
left: posx,
top: posy,
)
]),
);
}
}
You can simply use a Listener as the parent of your Stack and listen to it's onPointerDown event like so:
Listener(
onPointerDown: (event) {
// use event.localPosition.dx or event.localPosition.dy
},
child: Stack(
children: [
],
),
)
I have my own StatelessWidget with a ListView. I want it's state to be managed by parent StatefulWidget.
The behaviour I desire is that if I change a value, listView scrolls (or even jumps - it doesn't matter) to that value.
I thought that if I create stateless widget every time parent's setState() method is being invoked, the scrollController with initialOffset would make the list "move" but it doesn't. What is worth mentioning is that on first build initialOffset works as it should.
Here is example code of my problem:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 5;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new MyClass(_counter),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
child: new Icon(Icons.add),
),
);
}
}
class MyClass extends StatelessWidget {
final int extraValue;
final ScrollController scrollController;
MyClass(this.extraValue):
scrollController = new ScrollController(initialScrollOffset: extraValue*50.0);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemExtent: 50.0,
itemCount: 100,
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
if (index != extraValue)
return new Text(index.toString());
else
return new Text("EXTRA" + index.toString());
});
}
}
I'm not sure if it's a bug or my mistake.
Any ideas might be helpful :)
EDIT:
Inspired by Ian Hickson's answer I have solution to my problem:
void _incrementCounter() {
setState(() {
_counter++;
myClass.scrollController.animateTo(_counter*50.0, duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
});
}
The initial offset is... the initial offset. Not the current offset. :-)
You can cause the offset to change by calling methods on the ScrollController, like animateTo.