I use this answer in my solution.
In order to implement dragging picture on an enlarged scale.
But in the original version, there was no increase in the picture using double-tap.
And I tried to make it myself. And it seems to me, something happened.
Here is my code:
class ZoomableWidget extends StatefulWidget {
final Widget child;
const ZoomableWidget({Key key, this.child}) : super(key: key);
#override
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
Matrix4 matrix = Matrix4.identity();
Matrix4 zerada = Matrix4.identity();
bool zoomed = false;
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: (){
matrix.scale(!zoomed ? 2.0 : 1.0);
zoomed = !zoomed;
if (!zoomed) matrix = Matrix4.identity();
setState(() {
matrix = matrix;
});
},
child: MatrixGestureDetector(
shouldRotate: false,
shouldScale: true,
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
setState(() {
matrix = zoomed ? m : matrix;
});
},
child: Transform(
transform: matrix,
child: widget.child,
),
),
);
}
}
But immediately after the picture increased, she drove down.
And when I start dragging image, the zoom is reset to the default value.
Please tell me what should I do to fix my problem?
Related
I have a working snippet that I've wrote, but I kinda don't understand how flutter is (re)using the widgets creating in the build method:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(calculateNextOffset());
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
left: offsets[j].dx,
top: offsets[j].dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (int k = 0; k < offsets.length; k++) {
offsets[k] = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
Every time I click on a container, i expect my "offsets" state to be updated, but I also expect all the AnimationPositioned widgets, GestureDetector widgets and the square widgets that you see would be rerendered.
With rerendered i mean they would disappear from the screen and new ones would be rerendered (and the animation from the first widgets would be cancelled and never displayed)
However it works? Could someone explain this to me?
EDIT: I've updated my snippet of code in my question to match what i'm asking, which i'm also going to rephrase here:
Every time I click on a square, i want that square to disappear and all the other square to randomly animate to another position. But every time I click on a square, another random square is deleted, and the one i'm clicking is animating.
I want the square that I click on disappears and the rest will animate.
Following up from the comments - Actually, the square you click is disappears. However, to see this visually add this to your container color:
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
Now, your offsets are generating just fine and random. However, because you have an AnimatedContainer widget. This widget will remember the last x & y position of your square and animate the new square starting from that old x,y value to the new on you passed it. So if you really want the square you click on disappear - you will need to either use Positioned widget:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 100.0;
static const squareHeight = 100.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
offsets.add(calculateNextOffset());
}
print(offsets);
List<Widget> squareWidgets = [];
for (var offset in offsets) {
squareWidgets.add(Positioned(
left: offset.dx,
top: offset.dy,
//curve: Curves.easeIn,
//duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
for (var i = 0; i < offsets.length; i++) {
offsets[i] = calculateNextOffset();
}
offsets.add(calculateNextOffset());
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
If you want the rest of the squares to animate while only the one that is clicked disappears. You will need to rethink your implementation and track all square perhaps using unique keys and custom animations. Hope that helps!
I've finally found it:
In the context of the snippet inside the question: Every time you click on a square, it will correctly remove that item, but the widgets are rerendered from that new list, and the last widget that was previously rendered will be removed instead of the one that I clicked.
This has to do because every widget in the widget tree is rendered as an element inside the element tree. If the state of the element inside the element tree is the same, it will not rerender that one. and they are all just blue squares in the end, so there is no distinction.
You can find a very nice video made by the flutter devs here:
When to Use Keys - Flutter Widgets 101 Ep. 4
Long story short: Here is the snippet with the fix, which is to add a Key to each widget, then the state will change on the element inside the element tree and it will rerender (and remove) the correct widgets/elements:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<OffsetData> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(OffsetData(UniqueKey(), calculateNextOffset()));
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
key: offsets[j].key, // This line is the trick
left: offsets[j].offset.dx,
top: offsets[j].offset.dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (var offsetData in offsets) {
offsetData.offset = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
class OffsetData {
Offset offset;
final Key key;
OffsetData(this.key, this.offset);
}
I am using the Matrix Gesture Detector widget in the flutter. I managed to do reset the scaling but when next time I zoom in it starts from the last zoomed position. I can't find a solution to fix that.
Can someone please tell me a way to start from the original position
Here is my code
import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
class ZoomableWidget extends StatefulWidget {
final Widget child;
const ZoomableWidget({Key key, this.child}) : super(key: key);
#override
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
Matrix4 matrix = Matrix4.identity();
Matrix4 resetMatrix = Matrix4.identity();
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: (){
setState(() {
matrix = resetMatrix;
});
},
child: MatrixGestureDetector(
shouldRotate: false,
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
setState(() {
matrix = m;
});
},
child: Transform(
transform: matrix,
child: widget.child,
),
),
);
}
}
You need to fix your onMatrixUpdate method:
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
setState(() {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, rm);
});
},
I got this problem too, but I got the solution. First you need a callback for scaleEnd. And then you need to pass all the matrix from the last value from onMatrixUpdate to the widget. And then you need some boolean in onScaleStart to change the matrix to your lat
void onScaleStart(ScaleStartDetails details) {
if (widget.mMatrix != null) {
matrix = MxGestureDetector.compose(
widget.mMatrix!,
widget.tMatrix!,
widget.sMatrix!,
widget.rMatrix!,
);
}
translationUpdater.value = details.focalPoint;
scaleUpdater.value = 1.0;
rotationUpdater.value = 0.0;
}
How can I Draw and animate something like this in Flutter?
What you need seems just a canvas. With a canvas, you can draw anything you'd like to, and animate it by redraw on time.
Canvas is supported in Flutter, by extending CustomPaint,
Like this:
class SinCanvas extends StatelessWidget {
const SinCanvas({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
size:Size(600,400),
painter: SinPainter()
),
);
}
}
class SinPainter extends CustomPainter{
#override void paint(Canvas canvas, Size size){
final midY = size.height/2;
final paint = Paint()..style = PaintingStyle.fill
..color = Colors.black;
canvas.drawLine(Offset(0,midY), Offset(size.width,midY), paint);
var pt = Offset(0,midY);
for(double i=0;i<600;i+=0.1){
final npt = Offset(i,midY-sin(i/50)*100);
canvas.drawLine(pt, npt, paint);
pt = npt;
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
To animate, you can change the StatelessWidget to a StatefulWidget, and update the graphics with State.
Hope this solves your question.
I implemented an effect that I found on this medium article https://medium.com/flutter/perspective-on-flutter-6f832f4d912e
that allows you rotate widgets in 3d space. My only problem with this is that all of my widgets seem to have no thickness in the z direction so unsightly things like this happen:
My unrotated logo looks like this:
And this is the code I used to create this rotation effect:
import 'package:flutter/material.dart';
class PerspectiveContainer extends StatefulWidget {
final Widget child;
PerspectiveContainer({Key key, #required this.child}) : super(key: key);
#override
_PerspectiveState createState() => _PerspectiveState();
}
class _PerspectiveState extends State<PerspectiveContainer> {
Offset _offset = Offset.zero;
#override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001) // perspective
..rotateX(0.004 * _offset.dy)
..rotateY(-0.004 * _offset.dx),
alignment: FractionalOffset.center,
child: GestureDetector(
onPanUpdate: (details) => setState(() => _offset += details.delta),
onDoubleTap: () => setState(() => _offset = Offset.zero),
child: widget.child,
),
);
}
}
So would it be possible to give my BINGO widget thickness in the z direction?(The bingo logo was made with a Table widget)
Material Design works sometimes with shadows and elevation here, which would give you some 3D look, although it is actually 2d. https://material.io/design/environment/elevation.html#elevation-in-material-design
Some Widgets in Flutter have an elevation attribute, e.g. Card.
What I want to build is a widget that can make its child widget zoomable similar to the zoomable behavior.
Gestures I want to cover are
Pinch To Zoom
Double Tap to Zoom
Tap to get the local Position of the widget
Here is my widget plan:
ZoomableWidget(
child: // My custom Widget which should be zoomable.
)
Here is my current progress:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
class ZoomableWidget extends StatefulWidget {
final Widget child;
const ZoomableWidget({Key key, this.child}) : super(key: key);
#override
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
double _scale = 1.0;
double _previousScale;
#override
Widget build(BuildContext context) {
return ClipRect(
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_previousScale = _scale;
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_scale = _previousScale * details.scale;
});
},
onScaleEnd: (ScaleEndDetails details) {
_previousScale = null;
},
child: Transform(
transform: Matrix4.diagonal3(Vector3(_scale.clamp(1.0, 5.0),
_scale.clamp(1.0, 5.0), _scale.clamp(1.0, 5.0))),
alignment: FractionalOffset.center,
child: widget.child,
),
),
);
}
}
The problem I have faced is, I cannot change the center of the pinch thus the image only zooms at (0,0) even after I zoom in the corner. Also, I cannot access horizontal drag and vertical drag to scroll the widget.
Thanks in advance.
As of Flutter 1.20, InteractiveViewer widget supports pan and Zoom out of the box.
To make any widget zoomable you need to simply wrap the child with InteractiveViewer.
#override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
panEnabled: false, // Set it to false to prevent panning.
boundaryMargin: EdgeInsets.all(80),
minScale: 0.5,
maxScale: 4,
child: FlutterLogo(size: 200),
),
);
}
This is working perfectly now, thanks for the reference #pskink.
import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
class ZoomableWidget extends StatefulWidget {
final Widget child;
const ZoomableWidget({Key key, this.child}) : super(key: key);
#override
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
Matrix4 matrix = Matrix4.identity();
#override
Widget build(BuildContext context) {
return MatrixGestureDetector(
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
setState(() {
matrix = m;
});
},
child: Transform(
transform: matrix,
child: widget.child,
),
);
}
}
I loved de resolution, you should put that in a packged in pub, you can even put some custom options, in my code I put doubletap to reset the zoom and locked the rotation.
import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
class ZoomableWidget extends StatefulWidget {
final Widget child;
const ZoomableWidget({Key key, this.child}) : super(key: key);
#override
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
Matrix4 matrix = Matrix4.identity();
Matrix4 zerada = Matrix4.identity();
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: (){
setState(() {
matrix = zerada;
});
},
child: MatrixGestureDetector(
shouldRotate: false,
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
setState(() {
matrix = m;
});
},
child: Transform(
transform: matrix,
child: widget.child,
),
),
);
}
}
You can use Zoom Widget Zoom Widget only need set a canvas size and child
Zoom(
width: 1800,
height: 1800,
child: Center(
child: Text("Happy zoom!!"),
)
);
As an alternative to MatrixGestureDetector, you can use the photo_view package: https://pub.dev/packages/photo_view
It has good limiting of the screen constraints so you can't drag the child off-screen, a bounce effect when hitting min/max size, and many other options.
It can be used with a custom child like this:
PhotoView.customChild(
child: <your widget>
)
I use zoom widget
first, add to pubsec.yaml :
dependencies:
zoom_widget: ^2.0.0
then import:
import 'package:zoom_widget/zoom_widget.dart';
Center text with max width and max height:
Zoom(
maxZoomWidth: 1800,
maxZoomHeight: 1800,
child: Center(
child: Text("Happy zoom!!"),
)
);