How to move around your widgets (Image) after zooming (Flutter)? - android

I want to zoom an image in flutter and I am able to do it using GestureDetector and TransForm but I am not able to move around my image . It is just zooming . I want implement something dragging so that after zooming I can move around my image .
How can I do this ?
P.S. I tried using onPanStart ... in GestureDetector but I can't use both onScaleStart and onPanStart .
Here is my code :
class _ImageViewState extends State<ImageView> {
File img;
double _scale = 1.0;
double _prev = 1.0;
_ImageViewState(this.img);
#override
Widget build(BuildContext context) {
return SafeArea(
child: GestureDetector(
onScaleStart: (ScaleStartDetails details){
setState(() {
_scale = _prev;
});
},
onScaleUpdate: (ScaleUpdateDetails details){
setState(() {
_scale = _prev * details.scale;
});
print(_scale);
},
onScaleEnd: (ScaleEndDetails details){
setState(() {
_prev = _scale;
});
},
child : Transform(
alignment : FractionalOffset.center,
transform: Matrix4.diagonal3(Vector3(_scale , _scale , _scale)),
child: Image.file(img),
)
),
);
}
}

I have tried to implement something similar in my project. I'm using the matrix_gesture_detector package and created the following custom widget:
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,
),
),
);
}
}

Related

Flutter: How to make a rotatable coordinate system by gesture like maps in Ubuntu?

I want to create a page with a rotatable coordinate GestureDetector system run on Ubuntu and Android like this Video
This is my code use InteractiveViewer, CustomPainter and GestureDetector:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My App')),
body: InteractiveViewer(
child: Container(
color: Colors.amberAccent,
width: double.maxFinite,
height: double.maxFinite,
child: GestureDetector(
child: CustomPaint(
painter: MapPainter(
context: context,
)),
),
),
)));
}
}
class MapPainter extends CustomPainter {
final BuildContext context;
final double resolution = 0.05;
MapPainter({required this.context});
#override
void paint(Canvas canvas, Size size) {
canvas.save();
{
drawGrid(canvas);
}
canvas.restore();
}
void drawGrid(Canvas canvas) {
int numOfGrid = 50;
if (resolution == 0) return;
for (var i = -20; i <= numOfGrid; i++) {
canvas.drawLine(
Offset(i / resolution, -numOfGrid / resolution),
Offset(i / resolution, numOfGrid / resolution),
Paint()..color = Colors.grey.withOpacity(0.3),
);
canvas.drawLine(
Offset(-numOfGrid / resolution, i / resolution),
Offset(numOfGrid / resolution, i / resolution),
Paint()..color = Colors.grey.withOpacity(0.3),
);
}
}
#override
bool shouldRepaint(MapPainter oldDelegate) {
return true;
}
}
I tried matrix_gexture_detector like this Answer but the rotation function only works for Android, I don't know how to rotate in Ubuntu.
matrix_gesture_detector

flutter, Dart ) How to draw lines only using stylus pen (Samsung Galaxy Tab)

Im a flutter beginner and making a drawing app that can draw lines only with stylus device.
my test app can draw lines with finger and stylu but I want to implement the function that can draw lines only with a stylus.
I know there's a PointerData class and PointerDeviceKind.stylus enum. but I dont know how to use this class.
Here's my code and I used the provider.
OHHH I found the GestureRecognizer constructor and it says It's possible to limit this recognizer to a specific set of PointerDeviceKinds by providing the optional supportedDevices argument. haha but I have no idea how to apply this constructor to my code...
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => DrawingProvider(), child: BlankPage()),
);
}
}
class BlankPage extends StatefulWidget {
const BlankPage({Key? key}) : super(key: key);
#override
State<BlankPage> createState() => _BlankPageState();
}
class _BlankPageState extends State<BlankPage> {
#override
Widget build(BuildContext context) {
var p = Provider.of<DrawingProvider>(context);
return GestureDetector(
child: Scaffold(
body: Column(
children: [
Row(
children: [
penWidget(p: p),
colorWidget(context: context, color: Colors.black),
],
),
Expanded(
child: Stack(
children: [
Center(
child: Image(
image: AssetImage('image/image1.jpg'),
fit: BoxFit.cover,
),
),
Positioned.fill(
child: CustomPaint(
painter: DrawingPainter(p.lines),
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanDown: (s) {
p.penMode ? p.penDrawStart(s.localPosition) : null;
},
onPanUpdate: (s) {
p.penMode ? p.penDrawing(s.localPosition) : null;
},
child: Container(
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (_) {
print("컨테이너 눌림");
},
),
),
),
],
),
),
],
)),
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter(this.lines);
final List<List<DotInfo>> lines;
#override
void paint(Canvas canvas, Size size) {
for (var oneLine in lines) {
Color? color;
double? size;
var path = Path();
var l = <Offset>[];
for (var oneDot in oneLine) {
color ??= oneDot.color;
size ??= oneDot.size;
l.add(oneDot.offset);
}
path.addPolygon(l, false);
canvas.drawPath(
path,
Paint()
..color = color!
..strokeWidth = size!
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..isAntiAlias = true);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
drawing_provider.dart
class DrawingProvider extends ChangeNotifier {
final lines = <List<DotInfo>>[];
double _penSize = 3;
double get penSize => _penSize;
Color _selectedColor = Colors.black;
Color get selectedColor => _selectedColor;
bool _penMode = false;
bool get penMode => _penMode;
void changePenMode() {
_penMode = !_penMode;
print("penMode is called : ${penMode} ");
_eraseMode = false;
_highlighterMode = false;
notifyListeners();
}
void penDrawStart(Offset offset) {
var oneLine = <DotInfo>[];
oneLine.add(DotInfo(offset, penSize, _selectedColor));
lines.add(oneLine);
notifyListeners();
}
void penDrawing(Offset offset) {
lines.last.add(DotInfo(offset, penSize, _selectedColor));
print("Drawing");
notifyListeners();
}
I solved this problem by using Listener widget, onPointerDown and onPointerMove, onPointerCancel
if(s.kind == PointerDeviceKind.stylus) {
p.penMode ? p.penDrawStart(s.localPosition) : null;
p.highlighterMode ? p.highlighterDrawStart(s.localPostion) : null;

How to create an infinite rain of icons in Flutter?

I want to create an infinite rain of icons in Flutter. Let me explain.
Here is the widget that will be performing the raining/falling animation. It uses an AnimationController along with a GravitySimulation to make the widget fall. It takes in an endDistance, which is how far it should go down. It renders a simple icon (also an argument).
import 'package:clickr/providers/GameEngineProvider.dart';
import 'package:clickr/utils/functions/randInt.dart';
import "package:flutter/material.dart";
import 'package:flutter/physics.dart';
import 'package:provider/provider.dart';
class FallingIcon extends StatefulWidget {
final double endDistance;
final double? iconLeft;
final double? iconRight;
final double? iconBottom;
final Widget? child;
const FallingIcon({
Key? key,
required this.endDistance,
this.iconLeft,
this.iconRight,
this.iconBottom,
this.child,
}) : super(key: key);
#override
_FallingIconState createState() => _FallingIconState();
}
class _FallingIconState extends State<FallingIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(
milliseconds: randInt(2000, 3000),
),
);
_controller.animateWith(
GravitySimulation(
10,
0,
widget.endDistance,
0,
),
);
_controller.forward();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Positioned(
child: child!,
top: _controller.value * widget.endDistance,
left: widget.iconLeft,
right: widget.iconRight,
bottom: widget.iconBottom,
);
},
child: widget.child ?? const FlutterLogo(),
);
}
}
Here, I am using this widget inside a stack:
import 'package:clickr/providers/GameEngineProvider.dart';
import 'package:clickr/providers/IconSetsProvider.dart';
import 'package:clickr/screens/HomeScreen.Authenticated.dart';
import 'package:clickr/widgets/CircleSticker.dart';
import 'package:clickr/widgets/FallingIcon.dart';
import 'package:clickr/widgets/StatusBarSpacer.dart';
import "package:flutter/material.dart";
import 'package:provider/provider.dart';
import '../widgets/FloatingActionButtonCustomBar.dart';
class PlayScreen extends StatefulWidget {
const PlayScreen({Key? key}) : super(key: key);
static const routeName = "/play";
#override
State<PlayScreen> createState() => _PlayScreenState();
}
class _PlayScreenState extends State<PlayScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final iconSetsProvider =
Provider.of<IconSetsProvider>(context, listen: false);
final gameEngineProvider =
Provider.of<GameEngineProvider>(context, listen: true);
return Scaffold(
body: Column(
children: [
const StatusBarSpacer(),
Expanded(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Stack(
children: [
FallingIcon(
endDistance: constraints.maxHeight,
child: Image.asset(
iconSetsProvider.getRandomIconFromIconSet(
iconSetsProvider.currentIconSet!,
),
width: 40,
),
),
],
);
},
),
),
FloatingActionButtonCustomBar(
children: [
FloatingActionButton(
onPressed: () {
Navigator.of(context).pushReplacementNamed(
HomeScreenAuthenticatated.routeName);
},
child: const Icon(Icons.home),
heroTag: "home",
),
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.pause),
heroTag: "pause",
),
],
),
],
),
);
}
}
The above code renders one FallingIcon, which successfully animates and falls until it reaches the end of the screen and cannot be seen any more. Now, I want to multiply this icon, so there should be multiple icons falling on the screen. And I want this to never stop. It should just be an infinite rain of icons. And it shouldn't be systematic, like one wave after the other. It should just be random infinite rain of icons.
I have attempted to use a stream and a new event would be pushed onto the stream randomly. But then I couldn't figure out how to render FallingIcons based on the items in the stream.
I wonder if this is possible in Flutter. Thanks.
this kind of "raining/falling animation" could be implemented with one CustomPaint widget (instead of rebuilding lots of widgets with AnimatedBuilder)
the core idea is to provide a Listenable variable to CustomPainter.repaint property (see super(repaint: ...) below) - of course you need to change your CustomPainter so that it draws your vertically falling images
class FooSpritePaint extends StatefulWidget {
#override
State<FooSpritePaint> createState() => _FooSpritePaintState();
}
class _FooSpritePaintState extends State<FooSpritePaint> with TickerProviderStateMixin {
late Ticker ticker;
final notifier = ValueNotifier(Duration.zero);
ui.Image? sprite;
#override
void initState() {
super.initState();
ticker = Ticker(_tick);
rootBundle.load('images/sprites.png')
.then((data) => decodeImageFromList(data.buffer.asUint8List()))
.then(_setSprite);
}
_tick(Duration d) => notifier.value = d;
_setSprite(ui.Image image) {
setState(() {
// print('image: $image');
sprite = image;
ticker.start();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
foregroundPainter: SpritePainter(sprite, notifier),
child: Center(
child: TextButton(
onPressed: () => ticker.isTicking? ticker.stop() : ticker.start(),
child: const Text('click to stop / start', textScaleFactor: 1.5),
),
),
),
);
}
}
class SpritePainter extends CustomPainter {
final ui.Image? sprite;
final ValueNotifier<Duration> notifier;
final p = Paint();
SpritePainter(this.sprite, this.notifier) : super(repaint: notifier);
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
if (sprite != null) {
final ms = notifier.value.inMilliseconds;
final frame = ms ~/ 80;
// print(frame);
final frames = [frame, frame + 2, frame + 4].map((f) => f % 6).toList();
final spritePhases = [
phase(size.width, ms * 0.066),
phase(size.width, ms * 0.075 + 40),
phase(size.width, ms * 0.075 + 80),
];
final transforms = [
for (int i = 0; i < spritePhases.length; i++)
ui.RSTransform(1, 0, spritePhases[i][0], size.height / 2 - 45),
];
final rects = [
for (int i = 0; i < spritePhases.length; i++)
Rect.fromLTWH(frames[i] * 100, spritePhases[i][1] * 100, 100, 100),
];
canvas.drawAtlas(sprite!, transforms, rects, null, null, null, p);
}
}
List<double> phase(double width, double x) {
final w = width + 100;
x = x % (2 * w);
return x < w? [x - 100, 0] : [2 * w - x - 100, 1];
}
#override
bool shouldRepaint(SpritePainter oldDelegate) => false;
}
the above sample code uses the following image that should be placed in images/sprites.png folder:
EDIT
and as a proof of concept here you have such a sample CustomPainter:
class Bird {
Bird(int ms, this.rect, List<double> r, Size size) :
startTimeMs = ms,
scale = lerpDouble(1, 0.3, r[0])!,
rotation = pi * lerpDouble(-1, 1, r[2])!,
xSimulation = FrictionSimulation(0.75, r[1] * size.width, lerpDouble(size.width / 2, -size.width / 2, r[1])!),
ySimulation = GravitySimulation(lerpDouble(10, 1000, r[0])!, -rect.height / 2, size.height + rect.height / 2, 100);
final int startTimeMs;
final Rect rect;
final Simulation xSimulation;
final Simulation ySimulation;
final double scale;
final double rotation;
double x(int ms) => xSimulation.x(_normalizeTime(ms));
double y(int ms) => ySimulation.x(_normalizeTime(ms));
bool isDead(int ms) => ySimulation.isDone(_normalizeTime(ms));
double _normalizeTime(int ms) => (ms - startTimeMs) / Duration.millisecondsPerSecond;
RSTransform transform(int ms, Size size) {
final translateY = y(ms);
return RSTransform.fromComponents(
translateX: x(ms),
translateY: translateY,
anchorX: rect.width / 2,
anchorY: rect.height / 2,
rotation: rotation * translateY / size.height,
scale: scale,
);
}
}
class FallingBirdsPainter extends CustomPainter {
final ui.Image? sprite;
final ValueNotifier<Duration> notifier;
final imagePaint = Paint();
final backgroundPaint = Paint()..color = Colors.black26;
final random = Random();
final birds = <Bird>[];
int nextReport = 0;
static const spriteRects = [
Rect.fromLTRB(000, 0, 103, 140),
Rect.fromLTRB(103, 0, 217, 140),
Rect.fromLTRB(217, 0, 312, 140),
Rect.fromLTRB(312, 0, 410, 140),
];
FallingBirdsPainter(this.sprite, this.notifier) : super(repaint: notifier);
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.drawPaint(backgroundPaint);
if (sprite != null) {
final ms = DateTime.now().millisecondsSinceEpoch;
if (random.nextDouble() < 0.15) {
// drop new bird
birds.add(Bird(ms, spriteRects[random.nextInt(4)], List.generate(3, (i) => random.nextDouble()), size));
}
final transforms = birds.map((bird) => bird.transform(ms, size)).toList();
final rects = birds.map((bird) => bird.rect).toList();
canvas.drawAtlas(sprite!, transforms, rects, null, null, null, imagePaint);
// dead birds cleanup
birds.removeWhere((bird) => bird.isDead(ms));
if (ms >= nextReport) {
nextReport = ms + 6000;
print('flying birds population: ${birds.length}');
}
}
}
#override
bool shouldRepaint(FallingBirdsPainter oldDelegate) => false;
}
it uses the following image:
all you need is to replace
foregroundPainter: SpritePainter(sprite, notifier),
with
foregroundPainter: FallingBirdsPainter(sprite, notifier),
and
rootBundle.load('images/sprites.png')
with
rootBundle.load('images/birds.png')
the final result is similar to this:

I'm getting a hint that declaration 'build' , 'initstate', 'dispose', loadModel' isn't referenced. what should i do?

This is my code.
I am doing this in android studio using flutter project.
I'm getting a warning in my code that declaration 'build' , 'initstate', 'dispose', loadModel' isn't referenced. what should i do? and when I run the project, it shows HomePage State error.How can this be resolved?
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:flutter/services.dart';
import 'package:tflite/tflite.dart';
import 'dart:async';
//import 'lib.dart';
import 'main.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
{
late CameraController cameraController;
//defining variables
late CameraImage imgCamera;
bool isWorking=false;
late double imgHeight;
late double imgWidth;
late List recognitionsList;
initCamera()
{
cameraController=CameraController(cameras[0],ResolutionPreset.medium);
cameraController.initialize().then((value)
{
if(!mounted)
{
return;
}
setState(()
{
cameraController.startImageStream((imageFromStream)=>
{
if(!isWorking)
{
isWorking=true,
imgCamera= imageFromStream,
runModelOnStreamFrame(),
}
});
});
});
}
runModelOnStreamFrame() async {
imgHeight = imgCamera.height + 0.0;
imgWidth=imgCamera.width + 0.0;
recognitionsList= (await Tflite.detectObjectOnFrame(bytesList:imgCamera.planes.map((plane){
return plane.bytes;
}).toList() ,
model:"SSDMobileNet", //change it to other name assets/ssd_mobilenet.tflite
imageHeight: imgCamera.height,
imageWidth: imgCamera.width,
imageMean: 127.5,
imageStd: 127.5,
numResultsPerClass: 1,
threshold: 0.4,
))!;
isWorking=false;
setState(()
{
imgCamera;
});
Future <dynamic> loadModel() async
{
Tflite.close();
try{
late String response;
response = (await Tflite.loadModel(
model:"assets/ssd_mobilenet.tflite",
labels: "assets/ssd_mobilenet.txt"
))!;
print(response);
}
// catch{}
on PlatformException
{
print("unable to load model");
}
}
#override
void dispose()
{
super.dispose();
cameraController.stopImageStream();
Tflite.close();
}
#override
void initState()
{
super.initState();
initCamera();
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen)
{
if(recognitionsList == null) return[];
if(imgHeight == null || imgWidth == null) return[];
double factorX = screen.width;
double factorY = imgHeight;
Color colorPick = Colors.lightGreenAccent;
return recognitionsList.map((result)
{
return Positioned(
left:result["rect"]["x"] * factorX,
top:result["rect"]["y"] * factorY,
width:result["rect"]["w"] * factorX,
height:result["rect"]["h"] * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color:Colors.lightGreenAccent,width:2.0),
),
child:Text(
"${result['detectedClass']} ${(result['confidenceInClass'] * 100).toStringAsFixed(0)}%",
style:TextStyle(
background:Paint()..color= colorPick,
color:Colors.black,
fontSize:16.0,
),
),
),
);
}).toList();
}
#override
//build=null;
Widget build(BuildContext context)
{
Size size = MediaQuery.of(context).size;
List<Widget> stackChildrenWidgets =[];
stackChildrenWidgets.add(
Positioned(
top: 0.0,
left : 0.0,
width:size.width,height:size.height-100,
child:Container(
height:size.height-100,
child:(!cameraController.value.isInitialized)
? new Container()
: AspectRatio(
aspectRatio: cameraController.value.aspectRatio,
child: CameraPreview(cameraController),
),
),
),
);
if(imgCamera != null)
{
stackChildrenWidgets.addAll(displayBoxesAroundRecognizedObjects(size));
}
return SafeArea(
child: Scaffold
(
backgroundColor: Colors.black,
body: Container(
margin : EdgeInsets.only(top:50),
color:Colors.black,
child:Stack(
children:stackChildrenWidgets,
),
),
),
);
}
}
#override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}}
please help me with this.
You have to put your functions such as:
'build' , 'initstate', 'dispose', loadModel'
out of the function named: runModelOnStreamFrame().
Copy each function and paste it one by one inside the StatefulWidget function.

Flutter: enable image zoom in/out on double tap using InteractiveViewer

I want to enable zoom in and out on double tap of the image, together with scaling in/out on pinch.
I saw some tutorials on YouTube where they implemented this feature using GestureDetector like this one but for some reason, it didn't work out for me.
In order to implement scaling in/out on pinch, I relied on this answer, and it really works well, but I also want to enable zoom in/out on double tapping the image. Looking up a way to do so on the internet, unfortunately, yielded nothing.
Is there any way to enable zoom in/out with both pinch and double tap using InteractiveViewer?
here is my code:
#override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(80),
panEnabled: false,
scaleEnabled: true,
minScale: 1.0,
maxScale: 2.2,
child: Image.network("https://pngimg.com/uploads/muffin/muffin_PNG123.png",
fit: BoxFit.fitWidth,
)
),
);
}
You can use a GestureDetector, that gives you the position of the click and with that you can zoom with the TransformationController at the click position:
final _transformationController = TransformationController();
TapDownDetails _doubleTapDetails;
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: Center(
child: InteractiveViewer(
transformationController: _transformationController,
/* ... */
),
),
);
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
if (_transformationController.value != Matrix4.identity()) {
_transformationController.value = Matrix4.identity();
} else {
final position = _doubleTapDetails.localPosition;
// For a 3x zoom
_transformationController.value = Matrix4.identity()
..translate(-position.dx * 2, -position.dy * 2)
..scale(3.0);
// Fox a 2x zoom
// ..translate(-position.dx, -position.dy)
// ..scale(2.0);
}
}
To animate the transition on double tap, you have to create an explicit animation on top of Till's code.
class _WidgetState extends State<Widget> with SingleTickerProviderStateMixin {
.
.
.
AnimationController _animationController;
Animation<Matrix4> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
)..addListener(() {
_transformationController.value = _animation.value;
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
.
.
.
void _handleDoubleTap() {
Matrix4 _endMatrix;
Offset _position = _doubleTapDetails.localPosition;
if (_transformationController.value != Matrix4.identity()) {
_endMatrix = Matrix4.identity();
} else {
_endMatrix = Matrix4.identity()
..translate(-_position.dx * 2, -_position.dy * 2)
..scale(3.0);
}
_animation = Matrix4Tween(
begin: _transformationController.value,
end: _endMatrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
.
.
.
}
Here's a full, portable solution with included customizable animation:
class DoubleTappableInteractiveViewer extends StatefulWidget {
final double scale;
final Duration scaleDuration;
final Curve curve;
final Widget child;
const DoubleTappableInteractiveViewer({
super.key,
this.scale = 2,
this.curve = Curves.fastLinearToSlowEaseIn,
required this.scaleDuration,
required this.child,
});
#override
State<DoubleTappableInteractiveViewer> createState() => _DoubleTappableInteractiveViewerState();
}
class _DoubleTappableInteractiveViewerState extends State<DoubleTappableInteractiveViewer>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
Animation<Matrix4>? _zoomAnimation;
late TransformationController _transformationController;
TapDownDetails? _doubleTapDetails;
#override
void initState() {
super.initState();
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: widget.scaleDuration,
)..addListener(() {
_transformationController.value = _zoomAnimation!.value;
});
}
#override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
final newValue =
_transformationController.value.isIdentity() ?
_applyZoom() : _revertZoom();
_zoomAnimation = Matrix4Tween(
begin: _transformationController.value,
end: newValue,
).animate(
CurveTween(curve: widget.curve)
.animate(_animationController)
);
_animationController.forward(from: 0);
}
Matrix4 _applyZoom() {
final tapPosition = _doubleTapDetails!.localPosition;
final translationCorrection = widget.scale - 1;
final zoomed = Matrix4.identity()
..translate(
-tapPosition.dx * translationCorrection,
-tapPosition.dy * translationCorrection,
)
..scale(widget.scale);
return zoomed;
}
Matrix4 _revertZoom() => Matrix4.identity();
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _transformationController,
child: widget.child,
),
);
}
}
Example usage:
DoubleTappableInteractiveViewer(
scaleDuration: const Duration(milliseconds: 600),
child: Image.network(imageUrl),
),
Play around with it on dartpad.

Categories

Resources