I'm trying to display a widget once I have info about the max scroll extent. I can find that number if I assign an instance of ScrollController to the controller property of a scrollable widget.
My problem is that the ScrollController gets attached to the scrollable widget during the build, so I can not use the max scroll extent number before the first build. Thus what I was trying to do is display an empty Container in the first build and then switch that empty Container with the widget I actually want. Something like this:
_scrollController.positions.length == 0 ? new Container() : new Align(
alignment: Alignment.center,
child: new Container(
width: constraints.maxWidth,
height: 50.0,
color: Colors.black,
)
)
Now this does not work of course because _scrollController.positions.length will be 0 at the beginning and nowhere do I call setState when this value changes (when the controller gets attached).
So my question: Is there a place where I can get notified whenever the ScrollController gets attached to a scrollable widget? Or is there a better approach for this?
If the scrollable is widget.child.
#override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.child,
);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
widget.child.update(notification.metrics);
}
return false;
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {
applyInitialScrollPosition();
}
void applyInitialScrollPosition() {
ScrollController scrollControler = widget.child.controller;
ScrollPosition position = scrollControler.position;
ScrollNotification notification = ScrollUpdateNotification(
metrics: FixedScrollMetrics(
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
pixels: position.pixels,
viewportDimension: position.viewportDimension,
axisDirection: position.axisDirection),
context: null,
scrollDelta: 0.0);
_handleScrollNotification(notification);
}
The child must extends ChangeNotifier and has an update method:
void update(ScrollMetrics metrics) {
assert(metrics != null);
_lastMetrics = metrics; // Save the metrics.
notifyListeners();
}
All this only works if a scroll controller has explicitly been defined for the scrollable (widget.child).
Related
I've been trying to implement a gyroscope image viewer using the sensors package, however, the result seems to be very laggy. I have found a similar project on YouTube which is trying to achieve a similar goal, but as you can see in the video the animation is also very laggy.
The following code is simply outputting the data from the event, I notice how the data is being updated lags like 50ms in between updates.
Is there a way to smoothen the animation or update the data faster? Or is this a Flutter limitation?
NOTE:
I have tried --release version as suggested by other posts but the result stays the same.
import 'package:sensors/sensors.dart';
class MyGyro extends StatefulWidget {
final Widget child;
MyGyro({this.child});
#override
_MyGyroState createState() => _MyGyroState();
}
class _MyGyroState extends State<MyGyro> {
double gyroX = 0;
double gyroY = 0;
#override
void initState() {
super.initState();
gyroscopeEvents.listen((GyroscopeEvent event) {
setState(() {
gyroX = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
gyroY = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
});
});
}
#override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
child: Transform.translate(
offset: Offset(gyroY, 0),
child: Container(
child: Center(
child: Column(
children: [Text("X: ${gyroX}"), Text("Y: ${gyroY}"),],
),
),
),
),
);
}
}
I have found that is purely the problem of the sensors package I was using, either they have hard coded a slower interval when listening to the sensor event, or they are just using the default interval by the IOS channel.
So, I have found another package called flutter_sensors which had solved the problem. It's a very simple API to access the sensor events, but it allows you to change the interval.
I've got a simple AnimatedWidget with one child widget.
AnimatedContainer(
duration: Duration(milliseconds: 2000),
curve: Curves.bounceOut,
decoration: BoxDecoration(
color: Colors.purple,
),
child: FlutterLogo(
size: _boxSize,
),
),
where _boxSize is being animated like so:
void _startAnimation() => setState(() {
_boxSize *= 1.7;
});
AnimatedContainer is not working for child widgets, however. You need to change direct properties of AnimatedContainer for the animation to work.
This is in compliance with documentation:
The [AnimatedContainer] will automatically animate between the old
and new values of properties when they change using the provided curve
and duration. Properties that are null are not animated.
Its child and descendants are not animated.
What is the equivalent of AnimatedContainer which is ALSO ABLE to animate its children?
There are few widgets which will animate the child. You can swap the new flutter logo widget with preferred size using AnimatedSwitcher Widget.
AnimatedSwitcher - This widget will swap the child widget with a new widget.
AnimatedPositioned - It'll change the position of the child from the stack widget whenever the given position changes.
AnimatedAlign - Animated version of align which will change the alignment of the child whenever the given alignment changes.
AnimatedCrossFade - It fades between two children and animate itself between their sizes.
There is no magic widget which would simply recursively animate all children. But I think what you want is an implicitly animated widget. ie. you change the constructor parameters of a widget, and as it changes it animates from one value to the next.
The easiest way is probably the ImplicitlyAnimatedWidget with a AnimatedWidgetBaseState. So for your example to animate a boxSize attribute this could look like:
class AnimatedFlutterLogo extends ImplicitlyAnimatedWidget {
const AnimatedFlutterLogo({Key key, #required this.boxSize, #required Duration duration})
: super(key: key, duration: duration);
final double boxSize;
#override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() => _AnimatedFlutterLogoState();
}
class _AnimatedFlutterLogoState extends AnimatedWidgetBaseState<AnimatedFlutterLogo> {
Tween<double> _boxSize;
#override
void forEachTween(visitor) {
_boxSize = visitor(_boxSize, widget.boxSize, (dynamic value) => Tween<double>(begin: value));
}
#override
Widget build(BuildContext context) {
return Container(
child: FlutterLogo(
size: _boxSize?.evaluate(animation),
),
);
}
}
which is imho already pretty concise, the only real boilerplate is basically the forEachTween(visitor) method which has to create Tween objects for all properties you'd like to animate.
Was wondering is there a way to control textscalefactor for TextField widget in flutter. Basically I want to limit a text field from growing too large when a user increase font/text size in their devices accessibility settings.
Thanks.
You can wrap it with a MediaQuery with a custom textScaleFactor
Widget build(BuildContext context) {
final mqData = MediaQuery.of(context);
final mqDataNew = mqData.copyWith(textScaleFactor: mqData.textScaleFactor > 5.0 ? 5.0 : mqData.textScaleFactor)
return MediaQuery(data: mqDataNew, child: TextField());
}
The TextField does not need to be a direct child of MediaQuery.
You can set lower and upper bounds for the textScaleFactor of your entire app.
return MaterialApp(
builder: (context, child) {
const lowerLimit = 1.0;
const upperLimit = 1.2;
final mediaQueryData = MediaQuery.of(context);
final scale = mediaQueryData.textScaleFactor.clamp(lowerLimit, upperLimit);
return MediaQuery(
child: child,
data: MediaQuery.of(context).copyWith(textScaleFactor: scale),
);
},
);
I am trying to draw a widget whenever a user presses the screen.
Currently I am doing this by storing a list of widgets and when ontapup is fired on the gesture i am adding to a list of widgets.
Widget build(BuildContext context) {
Widget draw = new Text("A");
List<Widget> children = new List<Widget>();
return new Scaffold(
appBar: new AppBar(
title: const Text('Heading'),
leading: new Icon(Icons.question_answer),
),
body: new GestureDetector(
onTapUp: (details) {
setState(() {
children.add(new Positioned(
left: details.globalPosition.dx,
top: details.globalPosition.dy,
child: draw,
));
});
},
child: new Stack(children: children)
...
So my code is working I am drawing the widget when I click but my problem is that when adding the new Positioned() to stack the position is based on the screen which does not include the appbar offset. Is there a way to get the stacks initial x/y position? Or is there a way to get the appbars height? How do I get a widgets position or height/width?
Ok for anyone else who has the same issue I needed to create my own widget and use
context.findRenderObject()
and
globalToLocal()
Just FYI global to local did not work while in the one solution I needed to make it its own widget.
To get the offset of a widget, you must get the renderObject, cast it as a RenderBox, and then convert it's local position to a global position. Like this:
#override
Widget build(BuildContext context) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
If you need the position of a child widget, you can wrap that child in a LayoutBuilder, like this:
Container(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints box) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
)
)
I'm trying to implement double-tap-to-zoom in my zoomable_images plugin but the GestureTapCallback doesn't provide the tap location information.
Ideally the offset would be returned by the callback. Is there another API for this?
You can provide a GestureTapDownCallback callback as the onTapDown argument of the GestureDetector constructor. The GestureTapDownCallback takes a TapDownDetails argument that includes the global position of the tap. You can then convert it to relative coordinates using BuildContext.findRenderObject and RenderBox.globalToLocal:
Offset _tapPosition;
void _handleTapDown(TapDownDetails details) {
final RenderBox referenceBox = context.findRenderObject();
setState(() {
_tapPosition = referenceBox.globalToLocal(details.globalPosition);
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
/* ... */
onTapDown: _handleTapDown,
);
}
In your onDoubleTap handler, you can reference _tapPosition to find out where the most recent tap was located.
For an example of this in action, see InkWell.
as of [✓] Flutter (Channel stable, 2.5.3)
GestureDetector(
onTapDown: (details) {
var position = details.globalPosition;
// you can also check out details.localPosition;
if (position.dx < MediaQuery.of(context).size.width / 2){
// tap left side
} else {
// tap rigth size
}
},
child: SomeChildWidget(),
),
If you want to handle double taps, you'll need to store the tap position from the onDoubleTapDown and then work with onDoubleTap:
late Offset _doubleTapPosition;
...
onDoubleTap: () {
//do your stuff with _doubleTapPosition here
},
onDoubleTapDown: (details) {
final RenderBox box = context.findRenderObject() as RenderBox;
_doubleTapPosition = box.globalToLocal(details.globalPosition);
},
Original answer