Image Map -like functionality in Flutter? - flutter

I've searched quite a bit and not found a solid answer, so if this is a dupe, I honestly tried.
I have an app I'm rewriting and moving away from an html-based hybrid platform (specifically Trigger.io); doing the rewrite in Flutter and Dart, on the quick.
Part of the app includes a pretty simple screen where the user can click on an image of a human body, and via an image map, get back an identifier and caption for the body part (right forearm, left knee, head, etc).
I simply can not find an analog to this behavior and capability in Flutter. Have I missed something simple because I was totally over thinking it?
Thanks much.

You could wrap your Image in a GestureDetector and specify onTapDown (or onTapUp) callbacks that check the tapped coordinates and act accordingly.
(To convert global coordinates to local coordinates, see: flutter : Get Local position of Gesture Detector)
Here's a rough attempt:
import 'package:quiver/iterables.dart' show enumerate;
class ImageMap extends StatelessWidget {
const ImageMap({
Key key,
#required this.image,
#required this.onTap,
#required this.regions,
}) : super(key: key);
final Widget image;
final List<Path> regions;
/// Callback that will be invoked with the index of the tapped region.
final void Function(int) onTap;
void _onTap(BuildContext context, Offset globalPosition) {
RenderObject renderBox = context.findRenderObject();
if (renderBox is RenderBox) {
final localPosition = renderBox.globalToLocal(globalPosition);
for (final indexedRegion in enumerate(regions)) {
if (indexedRegion.value.contains(localPosition)) {
onTap(indexedRegion.index);
return;
}
}
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) => _onTap(context, details.globalPosition),
child: image,
);
}
}

Related

Flutter - Prevent custom draggable widget from being affected by a keyboard showing and hiding

I've created a custom draggable widget in Flutter, for my app, which can be used anywhere, simply by using a stack and adding the widget on top of that stack. This is the code:
import 'package:flutter/material.dart';
class DraggableWidget extends StatefulWidget {
final Widget child;
final Offset initialOffset;
final VoidCallback onPressed;
final GlobalKey parentKey;
const DraggableWidget({
Key? key,
required this.child,
required this.initialOffset,
required this.onPressed,
required this.parentKey,
}) : super(key: key);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
final GlobalKey _key = GlobalKey();
bool _isDragging = false;
late Offset _offset;
late Offset _minOffset;
late Offset _maxOffset;
#override
void initState() {
super.initState();
_offset = widget.initialOffset;
WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
}
void _setBoundary(_) {
final RenderBox parentRenderBox =
widget.parentKey.currentContext?.findRenderObject() as RenderBox;
final RenderBox renderBox =
_key.currentContext?.findRenderObject() as RenderBox;
try {
final Size parentSize = parentRenderBox.size;
final Size size = renderBox.size;
setState(() {
_minOffset = const Offset(0, 0);
_maxOffset = Offset(
parentSize.width - size.width, parentSize.height - size.height);
});
} catch (e) {
print('catch: $e');
}
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx - pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy - pointerMoveEvent.delta.dy;
if (newOffsetX < _minOffset.dx) {
newOffsetX = _minOffset.dx;
} else if (newOffsetX > _maxOffset.dx) {
newOffsetX = _maxOffset.dx;
}
if (newOffsetY < _minOffset.dy) {
newOffsetY = _minOffset.dy;
} else if (newOffsetY > _maxOffset.dy) {
newOffsetY = _maxOffset.dy;
}
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
#override
Widget build(BuildContext context) {
return Positioned(
right: _offset.dx,
bottom: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}
Now, this does work really well when the screen dimensions are fixed/do not change. However, I have noticed a bug, whereby if the keyboard slides up on a phone (for a texfield input) this shrinks the widget moveable area. Then, when the keyboard is removed, instead of the widget seeing the whole screen area again, it only sees an area equivalent to when the keyboard was out. That means that where before the widget could be dragged all over the screen, it can now only be dragged within the total area of the screen minus the area of the keyboard, even when the keyboard has been closed/removed, almost like there's an imaginary boundary.
Is there a way to prevent this from happening? Essentially, the draggable widget needs to move around the whole screen when the keyboard is closed and when the keyboard is open, it needs to move around the area of the screen minus the area of the keyboard. When the keyboard is closed again, it needs to move around the whole screen again.
Thanks in advance!
I noticed that it was actually happening because the draggable widget was being built before the preceding search delegate's keyboard had closed (this screen comprises a SingleChildScrollView widget with a number of modular stateful widgets as its children, one of which is a text button that calls a search delegate) so it took the height of the screen minus the keyboard height as the available height.
I don't know if this is the most elegant solution but I have managed to fix it by calling
final keyboardSize = MediaQuery.of(context).viewInsets.bottom;
in the build method, before returning the scaffold, which contained the stack (which itself contains the SingleChildScrollView and overlaid draggable widget).
Then, I made that keyboardSize parameter a required variable of the custom draggable widget and modified the _maxOffset parameter of the _setBoundary(_) function as follows:
_maxOffset = Offset(
parentSize.width - size.width,
parentSize.height - size.height + widget.keyboardSize,
);
This now seems to ensure that the draggable widget can be moved around the whole of the screen, not just the screen height minus the keyboard height.

Detect if a SingleChildScrollview/Listview can be scrolled

So, i have a widget with infinite width (Row) . To fill the widget with items i'm using a Lisview builder with Axis horizontal. I also can use a SingleChildScrollview with axis horizontal and a Row as the child.
If there is a few items, the width of the screen is not filled, so that's great. However, when there is a lot of items, the Listview becomes "scrollable" , and i can scroll to the right to reveal the items.
I would like to know if the list is scrollable (if it overflows). The purpose is to show a little text saying "Scroll to reveal more>>".
I know i can use maths and calculate the items width with the screen width. However... The Listview already knows this, so i was wondering if there was a way of getting access to that information
I had a similar problem. I found this solution here: Determine Scroll Widget height
Here's my question in case it's helpful: How do you tell if scrolling is not needed in a SingleChildScrollView when the screen is first built?
Solution:
Import the scheduler Flutter library:
import 'package:flutter/scheduler.dart';
Create a boolean flag inside the state object but outside of the build method to track whether build has been called yet:
bool buildCalledYet = false;
Create a boolean isScrollable variable inside the state object but outside of the build method:
bool isScrollable;
Add the following in the beginning of the build method:
if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
isScrollable = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(The purpose of buildCalledYet is to prevent this code from causing build to be called over and over again.)
If isScrollable is true, then show your text.
I created a widget called ScrollCheckerWidget with a controller property to pass the controller of a scrollable widget and check if it is scrollable in the view. The ScrollCheckerWidget widget has a builder callback function where you can return a widget and use the isScrollable boolean from the builder to check if the widget related to the controller is scrollable or not. Here you have the code:
import 'package:flutter/material.dart';
class ScrollCheckerWidget extends StatefulWidget {
const ScrollCheckerWidget({
Key? key,
required this.controller,
required this.builder,
}) : super(key: key);
final ScrollController controller;
final Widget Function(bool isScrollable) builder;
#override
State<EGScrollCheckerWidget> createState() => _EGScrollCheckerWidgetState();
}
class _EGScrollCheckerWidgetState extends State<EGScrollCheckerWidget> {
late final ScrollController _scrollController;
late bool _isScrollable;
#override
void initState() {
_scrollController = widget.controller;
_isScrollable = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isScrollable = _scrollController.position.maxScrollExtent > 0;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.builder.call(_isScrollable);
}
}
In builder:
return NotificationListener(
onNotification: (scrollNotification) {
print("ScrollStartNotification");
// add your logic here
},
child: SingleChildScrollView(...
Am trying to detect exact same thing myself, wrapping SingleChildScrollView with NotificationListener at least gave me a point of reference to start working on this.

How to use SizeChangedLayoutNotifier?

I got a Wrap inside my flexibleSpace in my SliverAppBar. Now when the size of my Wrap changes (dynamically) I need to get a notification, so I can change the height of my flexibleSpace in my SliverAppBar accordingly.
I read the docs to the SizeChangedLayoutNotifier but I don't really understand how to use it. I wrapped my Wrap as a child in a SizeChangedLayoutNotifier but for me it is very unclear how to catch the notification with the NotificationListener<SizeChangedLayoutNotification>. I couldn't find any code example.
I would really appreciate your help :) Thanks!
I finally figured it out and will post the solution here for others, having the same problem.
I put my Wrap inside like this:
new NotificationListener<SizeChangedLayoutNotification>(
onNotification: gotNotification,
child: SizeChangedLayoutNotifier(
key: _filterBarChangeKey,
child: Wrap( // my WRAP stuff)
)
);
and having my callback like this:
bool gotNotification(SizeChangedLayoutNotification notification) {
// change height here
_filterBarChangeKey = GlobalKey();
}
I also found another solution from here not using SizeChangedLayoutNotification at all to solve my problem. For me this was even better. I just wrapped my Widget inside an MeaserSize widget which provides an onSizeChanged callback.
typedef void OnWidgetSizeChange(Size size);
class MeasureSize extends StatefulWidget {
final Widget child;
final OnWidgetSizeChange onChange;
const MeasureSize({
Key key,
#required this.onChange,
#required this.child,
}) : super(key: key);
#override
_MeasureSizeState createState() => _MeasureSizeState();
}
class _MeasureSizeState extends State<MeasureSize> {
var widgetKey = GlobalKey();
#override
Widget build(BuildContext context) {
WidgetsBinding.instance
.addPostFrameCallback((_) => widget.onChange(widgetKey.currentContext.size));
return Container(
key: widgetKey,
child: widget.child,
);
}
}

Standardizing sizes across application, but still use `const`

I'd like to standard size across a flutter application to comfort to a 4 pt grid. Here's one example of how this could be done:
class Spacing {
const Spacing(double val) : points = val * 4;
final double points;
}
class PtPadding extends Padding {
PtPadding({Spacing padding, Widget child}) : super(padding: padding.points, child: child);
}
PtPadding(padding: Spacing(4), child: Text('Hello'));
// or just with regular old Padding
Padding(padding: Spacing(4).points, child: Text('Hello'));
This is great, but it seems I forgo the ability to const my specialized PtPadding forces developers to use Spacing. On the other hand, just using Spacing in a constructor and accessing the points, prevents any widget from being "const"able. So it seems like I have to take a performance hit if I want to implement this spacing in my system.
I could have a class with static const members that point to doubles, but then I'm restrained to the sizes available (ie I can only have so many static members) and I also don't get the benefits of type restrictions.
I'm wondering if anyone else has thoughts in how I might approach this.
For what it's worth, I understand why Spacing(4).points is not a const (methods inherently aren't consts), but not sure how to get around this.
The problem is, you are extending Padding. Widgets are not made to be extended. Instead, you should use composition.
class Spacing {
const Spacing(double val) : points = val * 4;
final double points;
}
class PtPadding extends StatelessWidget {
const PtPadding({
Key key,
#required this.padding,
this.child,
}) : super(key: key);
final Spacing padding;
final Widget child;
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(padding.points),
child: child,
);
}
}

Raw touch/swipe data in Flutter? Compared to Java w/ Android Studio?

I am very new to flutter development, and I have to make a fairly quick decision on whether or not it is the right platform for my internship project.
I have to create an interface which requires all directional swipes to navigate to different menus (I'm thinking of doing nested horizontal and vertical scrolling, which I have had trouble with in Android Studio) - but more importantly, I have to save the raw data from the touching/tapping/swiping. I can't just save "left swipe" or "right swipe", I also have to know pressure, velocity, location, exact path, etc.
Is this feasible in flutter? How does flutter handle this raw data as opposed to Android studio? Does flutter only determine the approximate direction of the swipe and that's it?
I have tried searching for answers all day, but I must be missing some key word, because I have been unable to find the answer so far.
GestureDetector is a very extensive Widget in this regard. It has all the capabilities you are searching for. A simpler version of it, which also has Material design built in, is InkWell, but this might be lacking some of the functionality you are searching for.
With a GestureDetector wrapped about your Widget you will be able to catch all hit events (you can even specify HitTestBehavior (with the behavior parameter).
For your custom interactions there are plenty of callbacks implemented. I linked you to the constructor, which contains a bunch of useful parameters, like onTapDown/Up, onVertical/HorizontalDragStart/Update/End.
This is not even everything, but using those you can programatically define your behavior. Let me explain the concept with a small example:
Offset start;
void verticalDragStart(DragStartDetails details) {
// process the start by working with the details
start = details.globalPosition;
// ...
}
void verticalDragUpdate(DragUpdateDetails details) {
// apply your logic
Offset delta = details.delta;
// ...
}
// use DragEnd, also for horizontal, pan etc.
#override
Widget build(BuildContext context) => GestureDectector(
onVerticalDragStart: verticalDragStart,
// ...
);
I hope that you can imagine all the possibilties this enables. You can also combine different callbacks. Just take a look at the parameters in the documentation and experiment with what fits for you.
I think that this is the raw data you asked for.
You can get the raw touch movements using Listener. The following example shows how to grab a list of points. It uses them to draw the line you just traced with your finger. You can't tell the pressure, but can tell the exact path and velocity (if you stored time with each point). The higher level detector, GestureDetector, hides these raw movements from you, but interprets them into the traditional swipes.
(Notes about the example... shouldRepaint should be smarter, points returned by Listener are in global co-ordinates so may need to be converted to local (this simple example works because there's no AppBar))
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Gesture',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Offset> points = [];
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Listener(
onPointerDown: (e) {
points = [];
points.add(e.position);
},
onPointerMove: (e) {
points.add(e.position);
},
onPointerUp: (e) {
points.add(e.position);
setState(() {});
},
child: new CustomPaint(
painter: new PathPainter(points),
child: new Container(
width: 300.0,
height: 300.0,
color: Colors.black12,
),
),
),
);
}
}
class PathPainter extends CustomPainter {
List<Offset> points;
Path path = new Path();
PathPainter(this.points) {
if (points.isEmpty) return;
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
path.lineTo(o.dx, o.dy);
}
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
path,
new Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 4.0,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true; // todo - determine if the path has changed
}
}