I'm trying to make a custom widget that the user can zoom and pan an image and tap to overlay points on the image. I think I'm close but can't quite get the points to line up under the tap.
I copied the relevant zoom-pan from here:
https://github.com/flutter/flutter/blob/master/examples/layers/widgets/gestures.dart
I also have my GestureDetector pulled out into its own widget as per this post to get the correct RenderBox for global to local offset conversion
flutter : Get Local position of Gesture Detector
import 'package:flutter/material.dart';
class ImageWithOverlays extends StatefulWidget {
final List<FractionalOffset> fractionalOffsets;
ImageWithOverlays(this.fractionalOffsets);
#override
_ImageWithOverlaysState createState() => _ImageWithOverlaysState();
}
class _ImageWithOverlaysState extends State<ImageWithOverlays> {
Offset _startingFocalPoint;
Offset _previousOffset;
Offset _offset = Offset.zero;
double _previousZoom;
double _zoom = 1.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: _handleScaleStart,
onScaleUpdate:_handleScaleUpdate,
onTapDown: (details){
setState(() {
widget.fractionalOffsets.add(_getFractionalOffset(context, details.globalPosition));
});
},
onDoubleTap: _handleScaleReset,
child: Transform(
transform: Matrix4.diagonal3Values(_zoom, _zoom, 1.0) + Matrix4.translationValues(_offset.dx, _offset.dy, 0.0),
child: Center(
child: Container(
child:Stack(
children: <Widget>[
Image.network(
'https://picsum.photos/250?image=9',
), ]
..addAll(_getOverlays(context)),
)),
),
));
}
void _handleScaleStart(ScaleStartDetails details) {
setState(() {
_startingFocalPoint = details.focalPoint;
_previousOffset = _offset;
_previousZoom = _zoom;
});
}
void _handleScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_zoom = _previousZoom * details.scale;
// Ensure that item under the focal point stays in the same place despite zooming
final Offset normalizedOffset = (_startingFocalPoint - _previousOffset) / _previousZoom;
_offset = details.focalPoint - normalizedOffset * _zoom;
});
}
void _handleScaleReset() {
setState(() {
_zoom = 1.0;
_offset = Offset.zero;
widget.fractionalOffsets.clear();
});
}
List<Widget> _getOverlays(BuildContext context) {
return widget.fractionalOffsets
.asMap()
.map((i, fo) => MapEntry(
i,
Align(
alignment: fo,
child: _buildIcon((i + 1).toString(), context)
)
))
.values
.toList();
}
Widget _buildIcon(String indexText, BuildContext context) {
return FlatButton.icon(
icon: Icon(Icons.location_on, color: Colors.red),
label: Text(
indexText,
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
);
}
Widget _buildCircleIcon(String indexText) {
return Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
indexText,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold
),
));
}
FractionalOffset _getFractionalOffset(BuildContext context, Offset globalPosition) {
var renderbox = context.findRenderObject() as RenderBox;
var localOffset = renderbox.globalToLocal(globalPosition);
var width = renderbox.size.width;
var height = renderbox.size.height;
return FractionalOffset(localOffset.dx/width,localOffset.dy/height);
}
}
But I'm having a rough time getting the layout right. I think I have two issues:
The image likes to jump to the upper left. I'm not sure how to get it to stay centered.
When I zoom in, my _getFractionalOffset function is not returning the correct fraction. The icons in this screenshot are not showing up under my tap
My matrix math is poor, as is my understanding of how Flutter does its transforms, so I would appreciate any insight.
Figured it out, I had a couple issues with my code that when compounded made debugging annoying.
First, I should have been multiplying the scale/translate matrices, not adding them:
vector.Matrix4 get _transformationMatrix {
var scale = vector.Matrix4.diagonal3Values(_zoom, _zoom, 1.0);
var translation = vector.Matrix4.translationValues(_offset.dx, _offset.dy, 0.0);
var transform = translation * scale;
return transform;
}
Second, I was trying to fuss around with doing the translating from local offset to the image frame instead of letting vector_math do it for me. The new overlay offset calculation:
Offset _getOffset(BuildContext context, Offset globalPosition) {
var renderbox = context.findRenderObject() as RenderBox;
var localOffset = renderbox.globalToLocal(globalPosition);
var localVector = vector.Vector3(localOffset.dx, localOffset.dy,0);
var transformed = Matrix4.inverted(_transformationMatrix).transform3(localVector);
return Offset(transformed.x, transformed.y);
}
Since _transformationmatrix takes a point from the image frame to the screen frame, I needed to left multiply the inverse to get from the screen frame (localOffset) to the image frame (returned value).
Finally, I'm using the Positioned widget instead of Align so I can set the positioning in pixels:
double _width = 100;
double _height = 50;
List<Widget> _getOverlays(BuildContext context) {
return widget.points
.asMap()
.map((i, offset) => MapEntry(
i,
Positioned(
left: offset.dx-_width/2,
width:_width,
top: offset.dy -_height/2,
height:_height,
child: _buildIcon((i + 1).toString(), context)
)
))
.values
.toList();
}
where widget.points is a List<Offset> of offsets returned by _getOffset above, and width/height are the width and height of my icon widget.
Hope this helps someone!
Related
how can I achieve something like this in Flutter:
Just replace Text to Image.network
Use below code
UnicornOutlineButton(
strokeWidth: 2,
radius: 24,
gradient: LinearGradient(
colors: [Colors.black, Colors.redAccent]),
child: Text('OMG', style: TextStyle(fontSize: 16)),
onPressed: () {},
),
Make new class with the name UnicornOutlineButton
class UnicornOutlineButton extends StatelessWidget {
final _GradientPainter _painter;
final Widget _child;
final VoidCallback _callback;
final double _radius;
UnicornOutlineButton({
#required double strokeWidth,
#required double radius,
#required Gradient gradient,
#required Widget child,
#required VoidCallback onPressed,
}) : this._painter = _GradientPainter(strokeWidth: strokeWidth, radius: radius, gradient: gradient),
this._child = child,
this._callback = onPressed,
this._radius = radius;
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _painter,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _callback,
child: InkWell(
borderRadius: BorderRadius.circular(_radius),
onTap: _callback,
child: Container(
constraints: BoxConstraints(minWidth: 88, minHeight: 48),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_child,
],
),
),
),
),
);
}
}
class _GradientPainter extends CustomPainter {
final Paint _paint = Paint();
final double radius;
final double strokeWidth;
final Gradient gradient;
_GradientPainter({#required double strokeWidth, #required double radius, #required Gradient gradient})
: this.strokeWidth = strokeWidth,
this.radius = radius,
this.gradient = gradient;
#override
void paint(Canvas canvas, Size size) {
// create outer rectangle equals size
Rect outerRect = Offset.zero & size;
var outerRRect = RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
// create inner rectangle smaller by strokeWidth
Rect innerRect = Rect.fromLTWH(strokeWidth, strokeWidth, size.width - strokeWidth * 2, size.height - strokeWidth * 2);
var innerRRect = RRect.fromRectAndRadius(innerRect, Radius.circular(radius - strokeWidth));
// apply gradient shader
_paint.shader = gradient.createShader(outerRect);
// create difference between outer and inner paths and draw it
Path path1 = Path()..addRRect(outerRRect);
Path path2 = Path()..addRRect(innerRRect);
var path = Path.combine(PathOperation.difference, path1, path2);
canvas.drawPath(path, _paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}
Im using the flutter_neumorphics dependency https://pub.dev/packages/flutter_neumorphic
to make my circular Neumorphic containers. However I'm facing an issue when Im trying to resize it.
I want the outer circle to be smaller and more compact.
Here is the code I've tried:
Widget build(BuildContext context) {
final percentage = 50.0;
return Stack(
children: <Widget>[
Align(
child: Neumorphic(
boxShape: NeumorphicBoxShape.circle(),
padding: EdgeInsets.all(80),
style: NeumorphicStyle(
depth: NeumorphicTheme.embossDepth(context),
),
child: CustomPaint(
painter: NeuProgressPainter(
circleWidth: 20,
completedPercentage: percentage,
defaultCircleColor: Colors.transparent,
),
child: Center(),
),
),
),
Align(
child: Neumorphic(
boxShape: NeumorphicBoxShape.circle(),
padding: EdgeInsets.all(80),
style: NeumorphicStyle(
color: Colors.white,
depth: NeumorphicTheme.depth(context),
),
),
),
],
);
}
Where the NeuProgressPainter is defined as:
class NeuProgressPainter extends CustomPainter {
//
Color defaultCircleColor;
Color percentageCompletedCircleColor;
double completedPercentage;
double circleWidth;
NeuProgressPainter(
{this.defaultCircleColor,
this.percentageCompletedCircleColor,
this.completedPercentage,
this.circleWidth});
getPaint(Color color) {
return Paint()
..color = color
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = circleWidth;
}
#override
void paint(Canvas canvas, Size size) {
Paint defaultCirclePaint = getPaint(defaultCircleColor);
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
Rect boundingSquare = Rect.fromCircle(center: center, radius: radius);
paint(
List<Color> colors,
) {
final Gradient gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomRight,
colors: colors,
);
return Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = circleWidth
..shader = gradient.createShader(boundingSquare);
}
canvas.drawCircle(center, radius, defaultCirclePaint);
double arcAngle = 2 * pi * (completedPercentage / 100);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2,
arcAngle,
false,
paint(
[
kcolor,
kDarkOrange,
kOrange,
],
),
);
}
#override
bool shouldRepaint(CustomPainter painter) {
return true;
}
}
This is what my output looks like:
Im trying to achieve something similar to this inspiration:
How about this?
Padding(
padding: const EdgeInsets.all(80.0),
child: Align(
child: Neumorphic(
boxShape: NeumorphicBoxShape.circle(),
padding: EdgeInsets.all(20),
style: NeumorphicStyle(
depth: NeumorphicTheme.embossDepth(context),
),
child: CustomPaint(
painter: NeuProgressPainter(
circleWidth: 20,
completedPercentage: percentage,
defaultCircleColor: Colors.transparent,
),
child: Center(),
),
),
),
),
I have a Draggable Container with the following decoration.
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ThemeColors.red, ThemeColors.yellow, ThemeColors.green])
I would like to have it animated that my frame is getting greener or redder, further I drag left or right.
Here's a simple example to detect horizontal drag and change between colors inside your gradient:
class GradientScreen extends StatefulWidget {
#override
_GradientScreenState createState() => _GradientScreenState();
}
class _GradientScreenState extends State<GradientScreen> {
var percentage = 0.0;
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text('Animated Drag Gradient'),
centerTitle: true,
),
body: GestureDetector(
onHorizontalDragUpdate: (details) {
setState(() => percentage = (details.localPosition.dx - 0) / (width - 0));
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_colorTween(Colors.green[900], Colors.red[900]),
Colors.yellow,
_colorTween(Colors.green[900], Colors.red[900])
],
)
),
),
),
);
}
Color _colorTween(Color begin, Color end) {
return ColorTween(begin: begin, end: end).transform(percentage);
}
}
The result of this easy implementation is the following:
I have created a flutter widget that moves the circle with accelerometer. It is very laggy as I have to use setState to change the position of the circle when phone is moved. Is there an alternative to creating this?
I have used AnimatedBuilder here but not sure how that can change the position of circle when device is moved smoothly.
class _AnimationWidgetState extends State<AnimationWidget>
with TickerProviderStateMixin {
AnimationController _animeController;
Animation _anime;
double x = 0.0, y = 0.0;
#override
void initState() {
super.initState();
_animeController =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
_anime = Tween(begin: 0.5, end: 0.5).animate(
CurvedAnimation(parent: _animeController, curve: Curves.ease));
accelerometerEvents.listen((AccelerometerEvent event) {
var a = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
var b = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
if ((x - a).abs() > 0.02 || (y - b).abs() > 0.02) {
setState(() {
x = a; y = b;
});
}
});
}
#override
void dispose() {
_animeController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_animeController.forward();
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return AnimatedBuilder(
animation: _animeController,
builder: (context, child) {
return Scaffold(
body: Transform(
transform: Matrix4.translationValues(
_anime.value * width * x, _anime.value * height * y, 0.0),
child: Center(
child: CircleAvatar(
radius: 15.0,
backgroundColor: Colors.green,
),
),
),
);
},
);
}
}
Animation is not at all smooth. This is because I have to use setState but the movement of circle is working as desired.
The whole purpose of using AnimationController is to listen to its events - that's what AnimatedBuilder does and rebuilds its subtree accordingly.
I will post here my overall recommendations on what to change in the code.
Remove setState - that's what makes your entire layout rebuild all over again, i.e. lags.
Also trigger _animeController listeners, i.e. AnimatedBuilder in your case - to rebuild itself.
accelerometerEvents.listen((AccelerometerEvent event) {
var a = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
var b = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
if ((x - a).abs() > 0.02 || (y - b).abs() > 0.02) {
x = a; y = b;
_animeController.value = _animeController.value; // Trigger controller's listeners
}
});
Start animation in initState, instead of build. This is the second thing that produces lags in your case. .forward triggers rebuild of your widgets, which leads to an infinite loop.
#override
void initState() {
super.initState();
_animeController.forward();
}
Use child property of your AnimatedBuilder to save up resources on rebuilding the avatar block every time. Also I'm not sure what Scaffold is here for - let's remove it if it's not needed.
AnimatedBuilder(
animation: _animeController,
builder: (context, child) => Transform(
transform: Matrix4.translationValues(_anime.value * width * x, _anime.value * height * y, 0.0),
child: child,
),
child: Center(
child: CircleAvatar(
radius: 15.0,
backgroundColor: Colors.green,
),
),
);
Follow Official Animations Tutorial to master Flutter animations.
Let me know if this helps.
I'm very new to Flutter and I've encountered a problem. I have to clip a png based on a given height value. That works fine with this Class I made:
class ScaleClipper extends CustomClipper<Rect> {
double value;
#override
Rect getClip(Size size) {
Rect rect = Rect.fromLTWH(0.0, 0.0 + value, size.width, size.height);
return rect;
}
#override
bool shouldReclip(ScaleClipper oldClipper) {
return true;
}
ScaleClipper(double value) {
this.value = value;
}
}
Now I want to animate the change in the image. I've tried to wrap it in Widgets mentioned here: https://flutter.io/docs/development/ui/widgets/animation
But I didn't get it to work properly.
This is the widget where I display the image:
ClipRect(
clipper: ScaleClipper(value),
child: Container(
margin: new EdgeInsets.only(
left: 30.0, top: 30.0, right: 20.0, bottom: 30.0),
width: 150.0,
height: 420.0,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/image.png"),
fit: BoxFit.contain))),
),
Do I need the to change my approach to the problem or can I animate the clipping of the image?
I found the answer for future newcomers. You can just use a simple Tween animation like this
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {
fracturedValue = desiredClipValue * animation.value;
});
});