Floor plan in flutter - flutter

i'm trying to come up with best way to draw a floor plan in flutter, something like these images, but it would be for regals in one concrete shop, instead of plan of shopping centre with multiple shops.
floor plan 1
floor plan 2
i decided rectangles would be sufficient enough for now and i have multiple ideas on how to execute, but no idea which one is the best. or maybe there is even better one i have not thought of
1. using custom painter
regals have attributes: ax, ay, bx, by, so they go from point a (left bottom) to b (right upper)
code like this
final rect = Rect.fromPoints(
Offset(regal.ax.toDouble(), size.height - regal.ay.toDouble()),
Offset(regal.bx.toDouble(), size.height - regal.by.toDouble()),
);
this is good because it is flexible, there is pretty much unlimited range of options, but using CustomPainter is a bit buggy in my case, alongside with Transform and GestureDetector it bugs out sometimes and instead of clicking on "buttons" you need to track where user clicked, ehm, tapped.
2. using gridView?
i dont have thought this thru as much as first option, but big plus would be using styled buttons as regals, instead of rectangles.
possible problems would be button sizing, if one regal would be times bigger than others.
regal attributes would be order on x axis, order on y axis, x flex (for example 3 as 3 times of base size), y flex
i think i have not thought of the best solution yet.
what would it be?

Here is a quick playground using a Stack of Regals who are just Containers in this quick implementation under 250 lines of code.
Click the FloatActionButton to create random Regal. Then, you can define the position of each Regal and its Size, within the limit of the Floor Plan and Max/min Regal Size.
In this quick implementation, the position of a Regal can be defined both with Gestures or Sliders; while its size can only be defined using the sliders.
Package Dependencies
Riverpod (Flutter Hooks flavor) for State Management
Freezed for Domain classes immutability
Full Source Code (222 lines)
import 'dart:math' show Random;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66478145.floor_plan.freezed.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
),
);
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final regals = useProvider(regalsProvider.state);
return Scaffold(
body: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
Container(
width: kFloorSize.width,
height: kFloorSize.height,
color: Colors.amber.shade100),
...regals
.map(
(regal) => Positioned(
top: regal.offset.dy,
left: regal.offset.dx,
child: GestureDetector(
child: RegalWidget(regal: regal),
),
),
)
.toList(),
],
),
const SizedBox(width: 16.0),
RegalProperties(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(regalsProvider).createRegal(),
child: Icon(Icons.add),
),
);
}
}
class RegalWidget extends HookWidget {
final Regal regal;
const RegalWidget({Key key, this.regal}) : super(key: key);
#override
Widget build(BuildContext context) {
final _previousOffset = useState<Offset>(null);
final _refOffset = useState<Offset>(null);
return GestureDetector(
onTap: () => context.read(selectedRegalIdProvider).state = regal.id,
onPanStart: (details) {
_previousOffset.value = regal.offset;
_refOffset.value = details.localPosition;
},
onPanUpdate: (details) => context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: _previousOffset.value +
details.localPosition -
_refOffset.value),
),
child: Container(
width: regal.size.width,
height: regal.size.height,
color: regal.color,
),
);
}
}
class RegalProperties extends HookWidget {
#override
Widget build(BuildContext context) {
final regal = useProvider(selectedRegalProvider);
return Padding(
padding: EdgeInsets.all(16.0),
child: regal == null
? Text('Click a Regal to start')
: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('WIDTH'),
Slider(
min: kRegalMinSize.width,
max: kRegalMaxSize.width,
value: regal.size.width,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(value, regal.size.height)),
),
),
const SizedBox(height: 16.0),
Text('HEIGHT'),
Slider(
min: kRegalMinSize.height,
max: kRegalMaxSize.height,
value: regal.size.height,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(regal.size.width, value)),
),
),
const SizedBox(height: 16.0),
Text('LEFT'),
Slider(
min: 0,
max: kFloorSize.width - regal.size.width,
value: regal.offset.dx,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(value, regal.offset.dy)),
),
),
const SizedBox(height: 16.0),
Text('TOP'),
Slider(
min: 0,
max: kFloorSize.height - regal.size.height,
value: regal.offset.dy,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(regal.offset.dx, value)),
),
),
],
),
),
);
}
}
final selectedRegalIdProvider = StateProvider<String>((ref) => null);
final selectedRegalProvider = Provider<Regal>((ref) {
final selectedId = ref.watch(selectedRegalIdProvider).state;
final regals = ref.watch(regalsProvider.state);
return regals.firstWhereOrNull((regal) => regal.id == selectedId);
});
final regalsProvider =
StateNotifierProvider<RegalsNotifier>((ref) => RegalsNotifier());
class RegalsNotifier extends StateNotifier<List<Regal>> {
final Size floorSize;
final Size maxSize;
RegalsNotifier({
this.floorSize = const Size(600, 400),
this.maxSize = const Size(100, 100),
List<Regal> state,
}) : super(state ?? []);
void createRegal() {
state = [...state, Regal.random];
print(state.last);
}
void updateRegal(Regal updated) {
state = state.map((r) => r.id == updated.id ? updated : r).toList();
}
}
#freezed
abstract class Regal implements _$Regal {
const factory Regal({
String id,
Color color,
Offset offset,
Size size,
}) = _Regal;
static Regal get random {
final rnd = Random();
return Regal(
id: DateTime.now().millisecondsSinceEpoch.toString(),
color: Color(0xff555555 + rnd.nextInt(0x777777)),
offset: Offset(
rnd.nextDouble() * (kFloorSize.width - kRegalMaxSize.width),
rnd.nextDouble() * (kFloorSize.height - kRegalMaxSize.height),
),
size: Size(
kRegalMinSize.width +
rnd.nextDouble() * (kRegalMaxSize.width - kRegalMinSize.width),
kRegalMinSize.height +
rnd.nextDouble() * (kRegalMaxSize.height - kRegalMinSize.height),
),
);
}
}
// CONFIG
const kFloorSize = Size(600, 400);
const kRegalMinSize = Size(10, 10);
const kRegalMaxSize = Size(200, 200);

Related

Constrainting layout to not go out of bounds with Positioned widget

I am currently working on a layout that displays a Positioned widget on the entire screen.
It's positioning itself close to the detected barcode, Look at the image below for an example.
But when the barcode moves to close the the left edge of the screen, the UI elements are drawn partially offscreen. Is there a way I can fix this without having to calculate when I am going out of bounds each frame?
Here is the code that I use to set this up:
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
child: _controller == null
? const Center(
child: Text(
'Initializing Camera...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: Stack(
fit: StackFit.expand,
children: <Widget>[
CameraPreview(_controller!),
_buildResults(),
if (_scanResults.isNotEmpty)
_buildUIElements()
],
),
);
}
Widget _buildUIElements() {
Barcode barcode = _scanResults[0];
final Size imageSize = Size(
_controller!.value.previewSize!.height,
_controller!.value.previewSize!.width,
);
var boundingBox = barcode.boundingBox!;
var rect = scaleRect(rect: boundingBox, imageSize: imageSize, widgetSize: MediaQuery.of(context).size);
return AnimatedPositioned(
top: rect.bottom,
left: rect.left,
child: Card(
child: Text('This is an amaizing product'),
),
duration: const Duration(milliseconds: 500),
);
}
Maybe there is a better way to achieve this?
Don't mind the excessive use of ! still learning the whole null-safety thing :)
EDIT 1:
As suggested by pskink I have looked at how the tooltips in flutter work and made use of the SingleChildLayoutDelegate in combination with a CustomSingleChildLayout and this works perfectly for tracking the position but now there is no option to animate this.
My delegate class is as follows:
class CustomSingleChildDelegate extends SingleChildLayoutDelegate {
CustomSingleChildDelegate ({
required this.target,
required this.verticalOffset,
required this.preferBelow,
});
final Offset target;
final double verticalOffset;
final bool preferBelow;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
#override
Offset getPositionForChild(Size size, Size childSize) {
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
);
}
#override
bool shouldRelayout(CustomSingleChildDelegate oldDelegate) {
return target != oldDelegate.target
|| verticalOffset != oldDelegate.verticalOffset
|| preferBelow != oldDelegate.preferBelow;
}
}
And then updated my builder function with:
return CustomSingleChildLayout(
delegate: CustomSingleChildDelegate (target: rect.bottomCenter, verticalOffset: 20, preferBelow: true),
child: Card(
child: Text('This is an amaizing product'),
),
)
Having the AnimatedPositioned as child of the layout causes an exception.

Implementing circular context menu in flutter [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I have already searched for circular menu packages for flutter and all of them have fabs arranged radially. I want the UI such that wherever the user taps(onhold) a circular menu will popup with buttons divided in sectors.
Here is a quick playground for such a Radial Context Menu.
It uses the concepts of State Management, Providers, Overlays...
I tried to keep it under 300 lines of code.
Structure of the project:
Domain Layer
For the purpose of this demo, I used Boxes defined by their id, name and color.
The Radial Menu is defined by a MenuConfig and MenuActions
State Management Layer
For the boxes, I used a boxesProvider providing a list of boxes and the possibility to update their color.
For the menu, I defined a menuProvider as a StateNotifierProvider.family as well as a ScopedProvider for the current item on which my Radial Menu is applied.
Presentation Layer
The structure of the Widget Tree is as follows:
Providercope
> MaterialApp
> HomePage
> Scaffold
> GridView
> BoxWidget
> ProviderScope [contextIdProvider.overrideWithValue(box.id)]
> ContextMenuDetector
The ContextMenuDetector is a GestureDetector that create the RadialMenu inside an OverlayEntry and manages the menu user experience thanks to olLongPressStart, olLongPressMoveUpdate, and olLongPressUp
I let you discover the full source code:
Full source code:
import 'dart:math' show pi, cos, sin;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66483259.radial_menu.freezed.dart';
void main() {
runApp(ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Radial Menu Demo',
home: HomePage(),
)));
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final boxes = useProvider(boxesProvider.state);
return Scaffold(
appBar: AppBar(title: Text('Radial Menu Demo')),
body: GridView.count(
crossAxisCount: 3,
children: boxes.map((box) => BoxWidget(box: box)).toList(),
),
);
}
}
class BoxWidget extends StatelessWidget {
final Box box;
const BoxWidget({Key key, this.box}) : super(key: key);
#override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [contextIdProvider.overrideWithValue(box.id)],
child: ContextMenuDetector(
child: Container(
decoration: BoxDecoration(
color: box.color,
border: Border.all(color: Colors.black87, width: 2.0),
),
child: Text(box.name),
),
),
);
}
}
class ContextMenuDetector extends HookWidget {
final Widget child;
const ContextMenuDetector({Key key, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
final contextId = useProvider(contextIdProvider);
final _menuConfig = useProvider(menuProvider(contextId).state);
final _offsetCorrection = useState(Offset.zero);
final _overlayEntry = useState<OverlayEntry>(null);
OverlayEntry _createMenu({Offset offset}) => OverlayEntry(
builder: (BuildContext overlayContext) {
return ProviderScope(
overrides: [contextIdProvider.overrideWithValue(contextId)],
child: Positioned(
left: offset.dx - kMenuRadius,
top: offset.dy - kMenuRadius,
child: RadialMenu(),
),
);
},
);
void _showMenu({Offset offset}) {
_overlayEntry.value = _createMenu(offset: offset);
Overlay.of(context).insert(_overlayEntry.value);
}
void _hideMenu() {
_overlayEntry.value?.remove();
}
return GestureDetector(
onLongPressStart: (details) {
final menuOffset = Offset(
details.globalPosition.dx.clamp(
kMenuRadius, MediaQuery.of(context).size.width - kMenuRadius),
details.globalPosition.dy.clamp(
kMenuRadius, MediaQuery.of(context).size.height - kMenuRadius),
);
_offsetCorrection.value = details.globalPosition - menuOffset;
_showMenu(offset: menuOffset);
},
onLongPressMoveUpdate: (details) {
final offset = details.localOffsetFromOrigin + _offsetCorrection.value;
if (offset.distance <= kMenuRadius) {
final sextant =
(((offset.direction / pi + 2 + 1 / 6) % 2) * 3).floor();
if (sextant < 4) {
context.read(menuProvider(contextId)).selectAction(sextant);
return;
}
}
context.read(menuProvider(contextId)).selectAction(-1);
},
onLongPressUp: () {
if (_menuConfig.selectedAction >= 0) {
_menuConfig.currentAction?.callback?.call(context, contextId);
}
_hideMenu();
},
child: child,
);
}
}
class RadialMenu extends HookWidget {
#override
Widget build(BuildContext context) {
final contextId = useProvider(contextIdProvider);
final config = useProvider(menuProvider(contextId).state);
return Material(
color: Colors.transparent,
child: Container(
width: 2 * kMenuRadius,
height: 2 * kMenuRadius,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: CustomPaint(
painter: RadialMenuPainter(config: config),
size: Size(2 * kMenuRadius, 2 * kMenuRadius),
child: Stack(
children: [
Positioned(
top: .4 * kMenuRadius,
left: .5 * kMenuRadius,
right: .5 * kMenuRadius,
child: Text('Menu $contextId', textAlign: TextAlign.center),
),
...config.actions.asMap().entries.map(
(action) {
final angle = pi * action.key / 3;
return Positioned(
left: kMenuRadius * (.6 + .5 * cos(angle)),
top: kMenuRadius * (.6 + .5 * sin(angle)),
child: SizedBox(
width: .8 * kMenuRadius,
height: .8 * kMenuRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(action.value.label,
style: TextStyle(fontSize: 12.0)),
Icon(action.value.iconData, size: 16.0),
],
),
),
);
},
),
],
),
),
),
);
}
}
class RadialMenuPainter extends CustomPainter {
final MenuConfig config;
RadialMenuPainter({this.config});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = config.color
..style = PaintingStyle.stroke
..strokeWidth = size.shortestSide * .1;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.shortestSide / 2, paint);
config.actions.asMap().entries.forEach((action) {
Paint paint = Paint()
..color = action.key == config.selectedAction
? action.value.color
: action.value.color.withOpacity(.4)
..style = PaintingStyle.fill;
Path path = Path()
..moveTo(size.width / 2, size.height / 2)
..arcTo(
Rect.fromLTWH(size.width * .1, size.height * .1, size.width * .8,
size.height * .8),
pi * (action.key / 3 - 1 / 6),
pi / 3,
false)
..close();
canvas.drawPath(path, paint);
});
}
#override
bool shouldRepaint(covariant RadialMenuPainter oldDelegate) =>
oldDelegate.config.selectedAction != config.selectedAction;
}
final boxesProvider =
StateNotifierProvider<BoxesNotifier>((ref) => BoxesNotifier(24));
class BoxesNotifier extends StateNotifier<List<Box>> {
BoxesNotifier(int nbBoxes)
: super(
List.generate(
nbBoxes,
(index) => Box(id: index, name: 'Box $index', color: Colors.white),
),
);
updateBoxColor(int id, Color color) {
state = [...state]..[id] = state[id].copyWith(color: color);
}
}
#freezed
abstract class Box with _$Box {
const factory Box({int id, String name, Color color}) = _Box;
}
final contextIdProvider =
ScopedProvider<int>((ref) => throw UnimplementedError());
final menuProvider = StateNotifierProvider.family<MenuNotifier, int>(
(ref, id) => MenuNotifier(menuConfig));
class MenuNotifier extends StateNotifier<MenuConfig> {
MenuNotifier(MenuConfig state) : super(state);
void selectAction(int index) {
state = state.copyWith(selectedAction: index);
}
}
#freezed
abstract class MenuConfig implements _$MenuConfig {
const factory MenuConfig({
#Default(Colors.white) Color color,
#Default([]) List<MenuAction> actions,
int selectedAction,
}) = _RadialMenuConfig;
const MenuConfig._();
MenuAction get currentAction => actions[selectedAction];
}
#freezed
abstract class MenuAction with _$MenuAction {
const factory MenuAction(
{String label,
IconData iconData,
void Function(BuildContext, int id) callback,
Color color}) = _MenuAction;
}
final menuConfig = MenuConfig(
color: Colors.lightBlue.shade200,
actions: [
MenuAction(
label: 'Cut',
iconData: Icons.cut,
color: Colors.red.shade200,
callback: (context, id) =>
context.read(boxesProvider).updateBoxColor(id, Colors.red.shade200),
),
MenuAction(
label: 'Copy',
iconData: Icons.copy,
color: Colors.green.shade200,
callback: (context, id) =>
context.read(boxesProvider).updateBoxColor(id, Colors.green.shade200),
),
MenuAction(
label: 'Paste',
iconData: Icons.paste,
color: Colors.blue.shade200,
callback: (context, id) =>
context.read(boxesProvider).updateBoxColor(id, Colors.blue.shade200),
),
MenuAction(
label: 'Undo',
iconData: Icons.undo,
color: Colors.indigo.shade200,
callback: (context, id) => context
.read(boxesProvider)
.updateBoxColor(id, Colors.indigo.shade200),
),
],
);
const double kMenuRadius = 100.0;
Package Dependencies
Riverpod (Flutter Hooks flavor) for State Management
Freezed for Domain classes immutability
For apps nowadays, it is a common UX practice to radially pop-up more buttons for extra actions that the user can perform.
This, however, is useful for presenting multiple actions to the user, but is not useful in cases where you want the user to perform a primary action.
Still, if your case fits the need, be sure to check out this tutorial from Jeff #firehsip.io

Flutter FutureProvider Value Not Updating In Builder Method

The Problem
I am building a basic app in Flutter that gets the user's location and displays nearby places in a swipe-card format similar to Tinder. I managed to implement geolocation however when using FutureProvider/Consumer I'm experiencing a weird bug where the user's relative distance to the place is overwritten with the first distance value in the card deck. Although I am new to flutter and the Provider package, I believe there is a simple fix to this.
Side note: After searching around on Google, I attempted to use FutureProvider.value() to prevent the old value from updating but had no luck.
Thank you in advance for any assistance or direction!
A Quick Demo
Packages Used
card_swipe.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:provider/provider.dart';
import 'package:swipe_stack/swipe_stack.dart';
import '../services/geolocator_service.dart';
import '../models/place.dart';
class CardSwipe extends StatelessWidget {
#override
Widget build(BuildContext context) {
final _currentPosition = Provider.of<Position>(context);
final _placesProvider = Provider.of<Future<List<Place>>>(context);
final _geoService = GeoLocatorService();
return FutureProvider(
create: (context) => _placesProvider,
child: Scaffold(
backgroundColor: Colors.grey[300],
body: (_currentPosition != null)
? Consumer<List<Place>>(
builder: (_, places, __) {
return (places != null)
? Column(
children: [
SizedBox(height: 10.0),
Container(
margin: EdgeInsets.only(top: 120.0),
height: 600,
child: SwipeStack(
children: places.map((place) {
return SwiperItem(builder:
(SwiperPosition position,
double progress) {
return FutureProvider(
create: (context) =>
_geoService.getDistance(
_currentPosition.latitude,
_currentPosition.longitude,
place.geometry.location.lat,
place.geometry.location.lng),
child: Consumer<double>(
builder: (_, distance, __) {
return (distance != null)
? Center(
child: Card(
child: Container(
height: 200,
width: 200,
child: Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
Text(place.name),
Text(
'${(distance / 1609).toStringAsFixed(3)} mi'), // convert meter to mi
],
),
),
),
),
)
: Container();
}),
);
});
}).toList(),
visibleCount: 3,
stackFrom: StackFrom.Top,
translationInterval: 6,
scaleInterval: 0.03,
onEnd: () => debugPrint("onEnd"),
onSwipe: (int index, SwiperPosition position) =>
debugPrint("onSwipe $index $position"),
onRewind:
(int index, SwiperPosition position) =>
debugPrint("onRewind $index $position"),
),
),
],
)
: Center(
child: CircularProgressIndicator(),
);
},
)
: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
geolocator_service.dart
import 'package:geolocator/geolocator.dart';
class GeoLocatorService {
final geolocator = Geolocator();
Future<Position> getLocation() async {
return await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.location,
);
}
Future<double> getDistance(
double startLat, double startLng, double endLat, double endLng) async {
return await geolocator.distanceBetween(startLat, startLng, endLat, endLng);
}
}
place.dart
Quick note: Place class does import a custom class called geometry.dart however this is purely for structuring the Place object and I'm certain it doesn't affect the bug. Therefore, it has been omitted.
import './geometry.dart';
class Place {
final String name;
final Geometry geometry;
Place(this.name, this.geometry);
Place.fromJson(Map<dynamic, dynamic> parsedJson)
: name = parsedJson['name'],
geometry = Geometry.fromJson(
parsedJson['geometry'],
);
}
You have to add a key to the SwiperItem with some unique value (like the name of the place) since currently flutter thinks that the widget has stayed the same so the Consumer gets the state of the old topmost widget.
By adding the key you tell flutter that you removed the topmost widget and the new topmost is in fact the second widget

How to use the sizes of other widgets before the widget tree is drawn ? [with Example]

This Shows all 3 cases discussed in this other post with a simple example.
How to use the constraints and sizes of other widgets during the build phase
NOTE:
I know that this is weird
I know that if used improperly this could create layouts that are never drawn
Just Reading Constraints is not enough because sometimes the constraints don't exist (like in this particular case)
GOAL:
Get what is drawn on screen to stabilize after 0 ReBuilds (or 1 build) instead of 2 ReBuilds
CURRENT PROCESS:
Build 1
Build 2
Build 3
when ("automaticReBuilding " == true) => the system automatically rebuilds itself depending on the quantity of dependancies (this is determined by you)
[the fact that automatic rebuilding runs the build function multiple times is what creates the stutter problem I have referred to now and in previous posts]
when ("automaticReBuilding" == false) => the system waits for you to rebuild things manually
//--------------------------------------------------CODE START
import 'package:flutter/material.dart';
import 'dart:async';
//Desired Behavior on FIRST build (It should not take 3)
//CASE 1 (parent uses child size) : eye.width = vane.width * 10
//CASE 2 (child uses parent size) : pupil.width = iris.width / 2
//CASE 3: (child uses sibling size) : iris.width = vane.width * 5
//Desired Sizes (can be read from Render Tree in Flutter Inspector) [in original config of 4 letters]
//vane = 30
//pupil = 75
//iris = 150
//eye = 300
//NOTE: that vane width (aka size) is not determined until we see what is inside of it
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StateFull();
}
}
class StateFull extends StatefulWidget {
#override
_StateFullState createState() => new _StateFullState();
}
var vaneKey = new GlobalKey();
var vaneWidth;
var irisKey = new GlobalKey();
var irisWidth;
class _StateFullState extends State<StateFull> {
//NOTE: change this to either run the rebuild in one shot or slowly see the progression
bool automaticReBuilding = false;
//NOTE: this starts here because the first build method isn't technically a rebuild
int timesReBuilt = -1;
//NOTE: this is set MANUALLY given the dependencies between your widgets
//In this particular case C relies on B which relies on A
//so (first) I get the size of A, (second) I use the size of A to get B, (third) i use the size of B to get C
//which comes down to 3 rebuilds
int requiredBuildsPerChange = 3;
int timesBuilt = 0;
rebuild(){
setState(() {
});
}
rebuildAsync() async{
await Future.delayed(Duration.zero);
setState(() {
});
}
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
timesReBuilt++;
if(automaticReBuilding){
timesBuilt++;
print("build #" + timesBuilt.toString());
if(timesBuilt < requiredBuildsPerChange)
rebuildAsync();
else
timesBuilt = 0;
}
var complexWidget = complexRelationshipWidget();
return new MaterialApp(
title: '3 Cases Test',
home: new Scaffold(
backgroundColor: Colors.brown,
body: new Stack(
children: <Widget>[
new Align(
alignment: Alignment.center,
child: complexWidget,
),
new Container(
padding: EdgeInsets.all(16.0),
alignment: Alignment.bottomRight,
child: new RaisedButton(
onPressed: () => (automaticReBuilding == false) ? rebuild() : null,
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.update),
new Text("Manual ReBuilds\nOR\nAutomatic Frame Stutter\n$timesReBuilt", textAlign: TextAlign.center,),
],
),
),
),
],
)
),
);
}
Container complexRelationshipWidget() {
vaneWidth = vaneKey?.currentContext?.findRenderObject()?.semanticBounds?.size?.width;
irisWidth = irisKey?.currentContext?.findRenderObject()?.semanticBounds?.size?.width;
return new Container( //-----EYE-----
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
width: vaneWidth == null ? null : vaneWidth * 10,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Container( //-----VANE-----
key: vaneKey,
color: Colors.red,
child: new Text("vane"),
),
new Container( //-----IRIS-----
key: irisKey,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
width: vaneWidth == null ? null : vaneWidth * 5,
alignment: Alignment.center,
child: new Container( //-----PUPIL
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
width: irisWidth == null ? null : irisWidth / 2,
),
),
],
)
);
}
}
//--------------------------------------------------CODE END

Flutter: inverted ClipOval

I am new to Flutter and I am trying to write a library to allow users to pan/zoom their profile picture.
In order to make it visual, I would like to stack their picture with an "inverted" ClipOval, to show the boundaries.
So far, this is the result I obtain:
This shows the boundaries but this is not user friendly and I would like to "invert" the ClipOval so that the center of the clip is "clear" and the outside is grayed out (something like a mask).
Is there any way to achieve this?
Here is the code I have so far (part of it comes from flutter_zoomable_image):
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ImagePanner extends StatefulWidget {
ImagePanner(this.image, {Key key}) : super(key: key);
/// The image to be panned
final ImageProvider image;
#override
_ImagePannerState createState() => new _ImagePannerState();
}
class _ImagePannerState extends State<ImagePanner> {
ImageStream _imageStream;
ui.Image _image;
double _zoom = 1.0;
Offset _offset = Offset.zero;
double _scale = 16.0;
#override
void didChangeDependencies() {
_resolveImage();
super.didChangeDependencies();
}
#override
void reassemble() {
_resolveImage();
super.reassemble();
}
#override
Widget build(BuildContext context) {
if (_image == null) {
return new Container();
}
return new Container(
width: double.INFINITY,
color: Colors.amber,
child: new Padding(
padding: new EdgeInsets.all(50.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new AspectRatio(
aspectRatio: 1.0,
child: new Stack(
children: [
_child(),
new Opacity(
opacity: 0.5,
child: new ClipOval(
child: new Container(
color: Colors.black,
),
),
),
],
),
),
],
)),
);
}
Widget _child() {
Widget bloated = new CustomPaint(
child: new Container(),
painter: new _ImagePainter(
image: _image,
offset: _offset,
zoom: _zoom / _scale,
),
);
bloated = new Stack(
children: [
new Container(
),
bloated
],
);
return new Transform(
transform: new Matrix4.diagonal3Values(_scale, _scale, _scale),
child: bloated);
}
void _resolveImage() {
_imageStream = widget.image.resolve(createLocalImageConfiguration(context));
_imageStream.addListener(_handleImageLoaded);
}
void _handleImageLoaded(ImageInfo info, bool synchronousCall) {
print("image loaded: $info $synchronousCall");
setState(() {
_image = info.image;
});
}
}
class _ImagePainter extends CustomPainter {
const _ImagePainter({this.image, this.offset, this.zoom});
final ui.Image image;
final Offset offset;
final double zoom;
#override
void paint(Canvas canvas, Size size) {
paintImage(canvas: canvas, rect: offset & (size * zoom), image: image);
}
#override
bool shouldRepaint(_ImagePainter old) {
return old.image != image || old.offset != offset || old.zoom != zoom;
}
}
The outcome I would like to obtain is the following so that users will directly see the boundaries and will be able to center, pan, zoom their profile picture INSIDE the oval.
(I made this via Photoshop, since I don't know how to achieve this with Flutter)
Many thanks for your help.
There's a couple other ways you could do this - you could simply draw an overlay in a CustomCanvas using a path that has a circle & rectangle, as all you really need is a rectangular semi-transparent rectangle with a hole in it. But you can also use a CustomClipper which gives you more flexibility in the future without having to draw stuff manually.
void main() {
int i = 0;
runApp(new MaterialApp(
home: new SafeArea(
child: new Stack(
children: <Widget>[
new GestureDetector(
onTap: () {
print("Tapped! ${i++}");
},
child: new Container(
color: Colors.white,
child: new Center(
child: new Container(
width: 400.0,
height: 300.0,
color: Colors.red.shade100,
),
),
),
),
new IgnorePointer(
child: new ClipPath(
clipper: new InvertedCircleClipper(),
child: new Container(
color: new Color.fromRGBO(0, 0, 0, 0.5),
),
),
)
],
),
),
));
}
class InvertedCircleClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
return new Path()
..addOval(new Rect.fromCircle(
center: new Offset(size.width / 2, size.height / 2),
radius: size.width * 0.45))
..addRect(new Rect.fromLTWH(0.0, 0.0, size.width, size.height))
..fillType = PathFillType.evenOdd;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
IgnorePointer is needed, or events won't be propagated through the semi-transparent part (assuming you need touch events).
How this works is that the Path used by clipPath is a circle in the middle (you need to adjust the size manually) with a rectangle taking up the entire size. fillType = PathFillType.evenOdd is important because it tells the path's fill should be between the circle and the rectangle.
If you wanted to use a customPainter instead, the path would be the same and you'd just draw it instead.
This all results in this: