Consider this code:
Transform.scale(scale: .5, child:
Transform.scale(scale: .5, child:
_MyBox()));
Is there any way that MyBox can access it's resulting scale factor of 0.25, internally with it's own context?
In Unity this would be rectTransform.scale(.25) and rectTransform.localScale(.5). With Flash we could just walk up the displayList with .parent and calculate it ourselves. Any equivalent in Flutter?
[Edit] The answer seem to be:
RenderBox rb = context.findRenderObject() as RenderBox;
if (rb != null) {
scheduleMicrotask(() {
// Should return you the final scale of this context
print(rb.getTransformTo(null));
});
}
Related
I am trying to create a parallax effect for the background image of a container.
I followed this tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling.
However I would like for the background image to remain still in the viewport ad the user scrolls.
Basically I would like to achieve this behavior https://www.w3schools.com/howto/tryhow_css_parallax_demo.htm.
I believe that in order to achieve this I have to modify the paintChildren method inside ParallaxFlowDelegate
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.centerLeft(Offset.zero),
ancestor: scrollableBox);
// Determine the percent position of this list item within the
// scrollable area.
final viewportDimension = scrollable.position.viewportDimension;
final scrollFraction =
(listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
// Convert the background alignment into a pixel offset for
// painting purposes.
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
// Calculate the vertical alignment of the background
// based on the scroll percent.
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
final listItemSize = context.size;
final childRect =
verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0.0, childRect.top)).transform,
);
}
but I can't figure out how.
Any help would be greatly appreciated :)
I was just messing, of course I can figure it out.
For anyone interested simply follow the flutter dev tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling but replace their ParallaxFlowDelegate class with the following:
import 'package:flutter/material.dart';
class ParallaxFlowDelegate extends FlowDelegate {
final ScrollableState scrollable;
final BuildContext listItemContext;
final GlobalKey backgroundImageKey;
ParallaxFlowDelegate({
required this.scrollable,
required this.listItemContext,
required this.backgroundImageKey,
}) : super(repaint: scrollable.position);
#override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return BoxConstraints.expand(
width: constraints.maxWidth,
height: scrollable.position.viewportDimension
);
}
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemSize = context.size;
//Gets the offset of the top of the list item from the top of the viewport
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.topCenter(Offset.zero),
ancestor: scrollableBox);
// Get the size of the background image
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
//Gets the vertical size of the viewport (excludes appbars)
final viewportDimension = scrollable.position.viewportDimension;
// Determine the percent position of this list item within the
// scrollable area.
// - scrollFraction is 1 if listItem's top edge is at the bottom of the
// viewport.
// - scrollFraction is 0 if listItem's top edge is at the top of the
// viewport
// - scrollFraction is -1 if listItem's bottom edge is at the top of the
// viewport
final double scrollFraction, yOffset;
if(listItemOffset.dy > 0){
scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0, 1.0);
yOffset = -scrollFraction * backgroundSize.height;
}else{
scrollFraction = (listItemOffset.dy / listItemSize.height).clamp(-1, 0);
yOffset = -scrollFraction * listItemSize.height;
}
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0, yOffset)).transform,
);
}
#override
bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
return scrollable != oldDelegate.scrollable ||
listItemContext != oldDelegate.listItemContext ||
backgroundImageKey != oldDelegate.backgroundImageKey;
}
}
Works like a beauty for vertically scrolling pages :)
I am creating a note-taking app using Flutter and I use a CustomPainter for the drawing part. For better performance, the CustomPaint gets converted into an image after some Lines. Now the problem with images is when zooming in, the lines (which are Images now) are very pixelated. Now my first Idea to solve this was redrawing the lines after zooming.
But how do I redraw them correctly without them still having a bad resolution? Also, is it possible to only redraw the lines that are visible on the screen currently?
everything is wrapped with a layoutbuilder which provides constraints + pixelratio (scale):
LayoutBuilder(builder: (context, constraints) {
size = constraints.biggest;
scale = MediaQuery.of(context).devicePixelRatio;
How I did the zooming:
GestureDetector(
onScaleStart: (details) {
_initialFocalPoint = details.focalPoint;
_initialScale = _scale;
},
onScaleUpdate: (details) {
setState(() {
_sessionOffset =
details.focalPoint - _initialFocalPoint;
_scale = _initialScale * details.scale;
});
},
onScaleEnd: (details) {
setState(() {
_offset += _sessionOffset;
_sessionOffset = Offset.zero;
});
},
child: Transform.translate(
offset: _offset + _sessionOffset,
child: Transform.scale(
scale: _scale,
child: ....
my CustomPaint which comes somewhere after the Transform widgets:
CustomPaint(
size: size!,
willChange: true,
painter: DrawingViewPainter(
pointsList: drawingNotifier.points,
image: image
),
);
snippet with the custpmpaint to image converter (drawn lines get converted to images to save performance):
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = ui.Canvas(recorder);
canvas.scale(scale!);
DrawingViewPainter(image: image, pointsList: points).paint(canvas, size!);
ui.Picture picture = recorder.endRecording();
ui.Image newImage = await picture.toImage(
(size!.width * scale!).ceil(),
(size!.height * scale!).ceil(),
);
......
image = newImage
the Custompaint part with the image drawer:
canvas.drawImageRect(
image!,
Offset.zero & Size(image!.width.toDouble(), image!.height.toDouble()),
Offset.zero & size,
Paint());
}
(sorry for the huge amount of code)
If you know of a better way please let me know as well.
Here a video of the Problem
I want to design a simple game in which the ball hits the boxes and the user has to try to bring the ball up with the cursor.
When the ball returns, end of ball movement, is the offset at the bottom of the screen, and I want to reset the animation if the ball offset equals the cursor and then give it a new direction, but that never happens.
Please see the values I have printed.
532.0 is cursor.position.dy and others are positionBall.dy + renderBall.size.height.
Why only when the ball moves up (the moment I tap on the screen) the ball offset and the cursor offset are equal, but not in return?
---update---
When I increase the duration (for example, 10 seconds), or activate the Slow Animations button from the flutter inspector, the numbers get closer to each other, and by adjusting them to the int, the condition is made.
I/flutter (21563): 532.0
I/flutter (21563): 532.45585
I'm really confused and I do not know what is going on in the background.
void initState() {
super.initState();
Offset init = initialBallPosition();
final g = Provider.of<GameStatus>(context, listen: false);
var key = ball.key;
_animationController = AnimationController(duration: Duration(seconds: 1), vsync: this);
_tweenOffset = Tween<Offset>(begin: init, end: init);
_animationOffset = _tweenOffset.animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
)..addListener(() {
if (_animationController.isAnimating) {
//if (_animationController.status == AnimationStatus.forward) {
RenderBox renderBall = key.currentContext.findRenderObject();
final positionBall = renderBall.localToGlobal(Offset.zero);
print(cursor.position.dy);
print(positionBall.dy + renderBall.size.height);
if (positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270) {
print('bang');
colideWithCursor();
}
}
if (_animationController.status == AnimationStatus.completed) {
if (bottomOfBall().dy == Screen.screenHeight / ball.width) {
gameOver();
} else
collision();
}
});
_animationController.isDismissed;
}
#override
Widget build(BuildContext context) {
final game = Provider.of<GameStatus>(context, listen: false);
return Selector<GameStatus, bool>(
selector: (ctx, game) => game.firstShoot,
builder: (context, startGame, child) {
if (startGame) {
game.ballDirection = 90;
routing(game.ballDirection);
}
return UnconstrainedBox(child: (SlideTransition(position: _animationOffset, child: ball.createBall())));
});
}
The two numbers are never exactly matching because the animation value is checked every frame and the overlap is occurring between frames.
You probably either want to add a tolerance (eg consider the values to have matched if they're within a certain amount) or create some interpolation logic where you check if the ball is about to collide with the cursor in-between the current frame and the next. eg replace:
positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270
With:
positionBall.dy + renderBall.size.height + <current_speed_per_frame_of_ball> <= cursor.position.dy && g.ballDirection == 270
The important thing here is that the animations aren't actually fluid. An animation doesn't pass from 0.0 continuously through every conceivable value to 1.0. The value of the animation is only calculated when a frame is rendered so the values you'll actually get might be something along the lines of: 0.0, 0.14, 0.30, 0.44, 0.58....0.86, 0.99, 1.0. The exact values will depend on the duration of the animation and the exact times the Flutter framework renders each frame.
Since you asked (in the comments) for an example using onTick, here's an example app I wrote up for a ball that bounces randomly around the screen. You can tap to randomize it's direction and speed. Right now it kinda hurts your eyes because it's redrawing the ball in a new position on every frame.
You'd probably want to smoothly animate the ball between each change in direction (eg replace Positioned with AnimatedPositioned) to get rid of the eye-strain. This refactor is beyond what I have time to do.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math.dart' hide Colors;
Random _rng = Random();
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
get randomizedDirection =>
_randomDirectionWithVelocity((150 + _rng.nextInt(600)).toDouble());
Ticker _ticker;
Vector2 _initialDirection;
Duration prevT = Duration.zero;
BallModel _ballModel;
#override
void dispose() {
super.dispose();
_ticker.dispose();
}
void _init(Size size) {
_ballModel = BallModel(
Vector2(size.width / 2, size.height / 2),
randomizedDirection,
16.0,
);
_ticker = createTicker((t) {
// This sets state and forces a rebuild on every frame. A good optimization would be
// to only build when the ball changes direction and use AnimatedPositioned to fluidly
// draw the ball between changes in direction.
setState(() {
_ballModel.updateBall(t - prevT, size);
});
prevT = t;
});
_ticker.start();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: GestureDetector(
child: Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
// Initialize everything here because we need to access the constraints.
if (_ticker == null) _init(constraints.biggest);
return Stack(children: [
Ball(_ballModel),
]);
},
),
),
onTap: () => setState(() => _ballModel.v = randomizedDirection),
),
);
}
}
class BallModel {
// The current x,y position of the ball.
Vector2 p;
// The direction, including speed in pixels per second, of the ball
Vector2 v;
// The radius of the ball.
double r;
BallModel(this.p, this.v, this.r);
void updateBall(Duration elapsed, Size size) {
// Move the ball by v, scaled by what fraction of a second has passed
// since the last frame.
p = p + v * (elapsed.inMilliseconds / 1000);
// If the ball overflows on a given dimension, correct the overflow and update v.
var newX = _correctOverflow(p.x, r, 0, size.width);
var newY = _correctOverflow(p.y, r, 0, size.height);
if (newX != p.x) v.x = -v.x;
if (newY != p.y) v.y = -v.y;
p = Vector2(newX, newY);
}
}
class Ball extends StatelessWidget {
final BallModel b;
Ball(this.b);
#override
Widget build(BuildContext context) {
return Positioned(
left: b.p.x - b.r,
bottom: b.p.y - b.r,
child: DecoratedBox(
decoration:
BoxDecoration(shape: BoxShape.circle, color: Colors.black)),
width: 2 * b.r,
height: 2 * b.r);
}
}
double _correctOverflow(s, r, lowerBound, upperBound) {
var underflow = s - r - lowerBound;
// Reflect s across lowerBound.
if (underflow < 0) return s - 2 * underflow;
var overflow = s + r - upperBound;
// Reflect s across upper bound.
if (overflow > 0) return s - 2 * overflow;
// No over or underflow, return s.
return s;
}
Vector2 _randomDirectionWithVelocity(double velocity) {
return Vector2(_rng.nextDouble() - .5, _rng.nextDouble() - 0.5).normalized() *
velocity;
}
Writing game and physics logic from scratch gets really complicated really fast. I encourage you to use a game engine like Unity so that you don't have to build everything yourself. There's also a Flutter based game engine called flame that you could try out:
https://github.com/flame-engine/flame.
How to detect drag velocity in flutter ?
I want to draw on screen in flutter using custom paint, when the velocity is less the stroke width should be less, but when the drag velocity is high the stroke width should be greater.
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(
() {
RenderBox object = context.findRenderObject();
Offset _localPosition =
object.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(_localPosition);
},
);
},
onPanEnd: (DragEndDetails details) => {
_deletedPoints.clear(),
_points.add(null),
// _listPoints.add(_points),
// _listPoints = List.from(_listPoints)..add(_points),
},
child: CustomPaint(
painter: Draw(points: _points),
size: Size.infinite,
),
),
The custom draw widget that extends customPainter
class Draw extends CustomPainter {
List<Offset> points;
// List<List<Offset>> points;
Draw({this.points});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = brushColor
..strokeCap = StrokeCap.round
..strokeWidth = brushWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Draw oldDelegate) => oldDelegate.points != points;
}
velocity is how much the position changed in a given time.
for this purpose you have the details.delta.distance in the onPanUpdate callback, which returns a double indicating how much the pointer has moved since last update, the bigger it is, the larger the velocity.
in your case, you can change the stroke width based on the distance traveled.
I haven't used any of these myself but you could look into
DragUpdateDetails. details in onPanUpdate has an Offset object called delta. Which are coordinates which update every time onPanUpdate is called.
https://api.flutter.dev/flutter/gestures/DragUpdateDetails-class.html
There's also a class called VelocityTracker
https://api.flutter.dev/flutter/gestures/VelocityTracker-class.html
Hope this helps you a bit
[Update] a practical example
late final Ticker _velocityTicker;
final List<int> _velocityRecs = List.filled(6, 0, growable: true);
double _rec = 0;
get _deltaSum {
var sum = 0;
for (var i = 1; i < _velocityRecs.length; i++) {
sum += (_velocityRecs[i] - _velocityRecs[i - 1]).abs();
}
return sum;
}
/// in initState()
_velocityTicker = Ticker((duration) {
_velocityRecs.removeAt(0);
// add whatever values you want to track
_velocityRecs.add(_currentIndex);
// You get a 'velocity' here, do something you want
if (_deltaSum > 2) {
// _offsetYController.forward();
} else if (_deltaSum < 1) {
// _offsetYController.reverse();
}
});
/// then .start()/.stop() the ticker in your event callback
/// Since each ticker has a similar time interval,
/// for Simplicity, I did not use duration here,
/// if you need a more accurate calculation value,
/// you may need to use it
In short, You can't.
Although details.delta and VelocityTracker are mentioned above, neither of them technically has access to the user's PointerEvent velocity,
For example, if the user drags the slider and stops, but the finger does not leave the screen, the velocity should be about 0, but this state can't be captured in flutter.
velocityTracker doesn't help
If you want to implement it yourself, one idea is to create a Timer/Ticker, record the last value and put it into a list, then set it to zero, and each time you want get, the sum of the whole list will be averaged to get the velocity, you need to be careful to determine how much the length of your List is appropriate
If I want to translate any Widget in my flutter app, I will use below code.
Transform.translate()
Translate function required Offset (dx,dy). But I want to find dx or dy value of the current widget so I can slightly move widget to left and right or top and bottom from the current position on device.
That's how it works
If I do
Transform.translate(offset: Offset(-10, 20)
It's transform its position 10 to the left and 20 down from its normal render position.
It's not so easy to do. You have to work with render objects. You can copy-paste Transform and RenderTransform classes to begin with. RenderTransform has overridden method paint and its argument offset is widget's local position:
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform = _effectiveTransform;
final Offset childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) {
layer = context.pushTransform(needsCompositing, offset, transform, super.paint, oldLayer: layer);
} else {
super.paint(context, offset + childOffset);
layer = null;
}
}
}
For example, I'll move the widget to 0 position on X, if its X position is less than 20:
if (child != null) {
final Matrix4 transform = _effectiveTransform;
Offset childOffset;
// Move it to the edge, if X position is less than 20
if (offset.dx < 20) {
childOffset = Offset(-offset.dx, 0);
} else {
childOffset = MatrixUtils.getAsTranslation(transform);
}
if (childOffset == null) {
...
And then I'll add 10 pixels padding from left in parent's widget, but the widget will stick to the left side:
Container(
padding: EdgeInsets.only(top: 20, left: 10),
child: MyTransform.translate(
offset: Offset.zero,
child: Container(
width: 100,
height: 100,
color: Colors.red
)
),
),