Multiple GestureDetector onPan event in Stack not working - flutter

I have to implement a custom analog clock where the clock hands indicate the breakfast,lunch and dinner time and user is able to reset the time by rotating the hands. I was able to draw the clock and hands with the help of canvas and CustomPaint and i haved stacked up in Stack Widget.I have added GestureDetector for each hand and using onPan events am able to rotate the hands also.But the issue is that when multiple hands are put in Stack only the last added clock hand is moving with the gesture others are not detecting the gesture.How to pass down the event to the stack so that all the gesture detectors detects it?
For drawing clock hands
class ClockHandComponent extends StatefulWidget {
final String title;
final int time;
final Color color;
const ClockHandComponent({Key key, this.title, this.time, this.color})
: super(key: key);
#override
ClockHandComponentState createState() {
return new ClockHandComponentState();
}
}
class ClockHandComponentState extends State<ClockHandComponent> {
int time;
double angle;
ClockHandPainter _clockHandPainter;
#override
void initState() {
time = widget.time;
angle = 2 * pi / 24 * time;
initialize();
super.initState();
}
#override
void didUpdateWidget(ClockHandComponent oldWidget) {
initialize();
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: _clockHandPainter,
)),
);
}
_onPanUpdate(DragUpdateDetails details) {
print(details);
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details.globalPosition);
angle = -coordinatesToRadians(_clockHandPainter.center, position);
print(angle);
initialize();
setState(() {});
}
_onPanEnd(_) {}
_onPanDown(DragDownDetails details) {
print(details);
print(angle);
}
initialize() {
_clockHandPainter =
new ClockHandPainter(widget.title, time, widget.color, angle);
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
}
For drawing clock face
class ClockFaceComponent extends StatefulWidget {
#override
ClockFaceComponentState createState() {
return new ClockFaceComponentState();
}
}
class ClockFaceComponentState extends State<ClockFaceComponent> {
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(10.0),
child: new AspectRatio(
aspectRatio: 1.0,
child: new Container(
width: double.infinity,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockBackgroundColor,
),
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
//dial and numbers
new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: new ClockDialPainter(),
),
),
new Center(
child: new Container(
width: 40.0,
height: 40.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
)),
//centerpoint
new Center(
child: new Container(
width: 15.0,
height: 15.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockCenterColor,
),
),
),
getClockHands()
],
),
),
),
);
}
getClockHands() {
return new AspectRatio(
aspectRatio: 1.0,
child: new Stack(
fit: StackFit.expand,
children: getHands(),
));
}
getHands() {
List<Widget> widgets = new List();
List<Hands> hands = Hands.getHands();
for (Hands hand in hands) {
widgets.add(ClockHandComponent(
title: hand.title,
time: hand.time,
color: hand.color,
));
}
return widgets;
}
}

Related

How do I resize an image within a container using a pinch gesture?

I am pulling an image from the camera or gallery and need to make is so that image can be moved and resized with a pinch gesture freely within the bounds of a container.
The movement of the image is fine however I am having trouble getting the image to resize when zoomed in or out on. I am currently using GestureDetector but cannot get it to work.
Based on the attached code is there a better method of achieving the pinch zoom or am I misusingGestureDetector or putting it in the wrong place?
Code:
import 'package:flutter/material.dart';
import 'dart:io';
double canvasWidth = 0.7;
double canvasHeight = 0.5;
double imageCurrentScale = 1.0;
double imageBaseScale = 1.0;
class CanvasAreaWidget extends StatefulWidget {
const CanvasAreaWidget({super.key});
#override
State<CanvasAreaWidget> createState() => CanvasArea();
}
class CanvasArea extends State<CanvasAreaWidget> {
// Setup for canvas properties
final textWidgetKey = GlobalKey();
double titleLeftPosition = 0;
double imageScale = 1.0;
double prevImageScale = 1.0;
Offset postition = const Offset(0,0);
void initPosition() {
setState(() {
postition = Offset((MediaQuery.of(context).size.width*canvasWidth/2) - 25, (MediaQuery.of(context).size.height*canvasHeight/2) -25);
});
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => initPosition());
}
void updateScale(double zoom) => setState(() => imageScale = prevImageScale * zoom);
void commitScale() => setState(() => prevImageScale = imageScale);
void updatePosition(Offset newPosition) {
setState(() {
newPosition = newPosition - Offset(MediaQuery.of(context).size.width*((1-canvasWidth)/2), MediaQuery.of(context).size.width*((1-canvasHeight)/2));
postition = newPosition;
});
}
#override
Widget build(BuildContext context) {
//initPosition();
return Scaffold(
// The appBar contains the title of the page (current section of the app) as well as the back key to return to the main menu.
extendBodyBehindAppBar: true,
appBar: AppBar(
leading: const Icon(Icons.chevron_left),
title: const Text('Card Editor'),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
// Parent stack that all other widgets will be children of.
body: Stack(
children: [
Positioned(
// Position of drag area.
left: MediaQuery.of(context).size.width * ((1-canvasWidth)/2),
top: MediaQuery.of(context).size.width * ((1-canvasHeight)/2),
child: ClipRect(
child: Stack(
children: [
Container(
// Size of the drag area.
width: MediaQuery.of(context).size.width * canvasWidth,
height: MediaQuery.of(context).size.height * canvasHeight,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
),
Positioned(
left: postition.dx,
top: postition.dy,
child: GestureDetector(
onScaleUpdate: (ScaleUpdateDetails details) => updateScale(details.scale),
onScaleEnd: (ScaleEndDetails details) => commitScale(),
child: Draggable(
maxSimultaneousDrags: 1,
feedback: const ImageItemWidget(),
childWhenDragging: Container(),
onDragEnd: (details) => updatePosition(details.offset),
child: const ImageItemWidget(),
),
),
),
],
),
),
),
],
),
);
}
}
File? recievedImage;
class ImageItemWidget extends StatefulWidget {
const ImageItemWidget ({super.key});
#override
State<ImageItemWidget> createState() => ImageItem();
}
class ImageItem extends State<ImageItemWidget> {
static void getImageFile(File? imageFile) {
recievedImage = imageFile;
print("File received");
}
#override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width * canvasWidth,
height: MediaQuery.of(context).size.height * canvasHeight,
child: Image.file(
recievedImage!,
),
);
}
}

flutter web: InteractiveViewer

There are 10 boxes on the Artboard and the screen is big enough, you can see all 10 boxes.
On the other hand, if the screen size is small, all 10 boxes are not displayed on the screen.
I tried to move the 10 piece so that I could see the below, some of the boxes were cut off, so it didn't come out
I want to see all 10 boxes even when the screen is small. How do I solve this problem?
class ArtBoard extends StatefulWidget {
const ArtBoard({Key? key}) : super(key: key);
#override
_ArtBoardState createState() => _ArtBoardState();
}
class _ArtBoardState extends State<ArtBoard> {
TransformationController _transformController = TransformationController();
double _scale = 1;
Offset startOffset = Offset.zero;
#override
void initState() {
super.initState();
}
#override
void dispose() {
_transformController.dispose();
super.dispose();
}
void _handleViewerTransformed() =>
_scale = _transformController.value.row0[0];
#override
Widget build(BuildContext context) {
RandomColor _randomColor = RandomColor();
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Stack(
fit: StackFit.expand,
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey.shade500,
child: Center(
child: Text('Design Board',
style: TextStyle(fontSize: 8, color: Colors.white70))),
),
InteractiveViewer(
transformationController: _transformController,
boundaryMargin: EdgeInsets.all(double.infinity),
constrained: true,
minScale: 0.1,
maxScale: 5.0,
panEnabled: true,
scaleEnabled: true,
onInteractionUpdate: (ScaleUpdateDetails details) {
//_transformationController.value.scale(1);
//_transformationController.value.scale(details.scale);
//print('Scale update:$details');
},
onInteractionEnd: (ScaleEndDetails details) {
double correctScaleValue =
_transformController.value.getMaxScaleOnAxis();
// unzoom when interaction has ended
setState(() {
print('${_transformController.value}');
_transformController.toScene(Offset.zero);
});
},
child: ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Align(
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
width: 100,
height: 80,
color: _randomColor.randomColor(),
child: Text('Box :${index}')),
);
},
),
),
],
),
);
}
}
This is a problem flutter is known for because on like native code it doesn't handle size for you, I recommend using a package like sizer from https://pub.dev
SizedBox(
width: 100.sp.clamp(10,100)
)
Within this example ".sp" is a number (int and double) extension from the sizer package that gives you the size relative to the screen size then adding a ".clamp" makes it that you can have the size within your desired size.
10.sp.clamp(lowestPoint, maxPoint);
your this to get the longest side
final double longerSide = 100.w>100.h ? 100.w:100.h;
final double shorterSide = 100.w>100.h ? 100.h:100.w;
/// This builds a box using the lonerSide length and the shortSide length
Widget buildBox({required double lSide, required double sSide}) {
final averageHeight = longerSide / 10;
final averageWidth = shorterSide / 2;
return Container(width: averageWidth, height: averageHeight, color: Colors.red);
}

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

Flutter Flare, Rive, is it able to use for background?

I just succeed flutter-flare for my flutter widgets
but I could not find a example to use it for background (Container)
is this possible?
Use official example code https://github.com/2d-inc/Flare-Flutter/tree/stable/example/teddy/lib
with a little modification, you can use Stack and Positioned and change attribute value like top
code snippet
return Scaffold(
backgroundColor: Color.fromRGBO(93, 142, 155, 1.0),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 50,
left:0,
right: 0,
child: Container(
height: 200,
padding:
const EdgeInsets.only(left: 30.0, right: 30.0),
child: FlareActor(
"assets/Teddy.flr",
shouldClip: false,
alignment: Alignment.bottomCenter,
fit: BoxFit.contain,
controller: _teddyController,
)),),
Positioned(
child: SingleChildScrollView(
working demo
full code
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flare_flutter/flare.dart';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_dart/math/vec2d.dart';
import 'package:flare_flutter/flare_controls.dart';
// Adapted these helpful functions from:
// https://github.com/flutter/flutter/blob/master/packages/flutter/test/material/text_field_test.dart
// Returns first render editable
RenderEditable findRenderEditable(RenderObject root) {
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
return renderEditable;
}
List<TextSelectionPoint> globalize(
Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
return TextSelectionPoint(
box.localToGlobal(point.point),
point.direction,
);
}).toList();
}
Offset getCaretPosition(RenderBox box) {
final RenderEditable renderEditable = findRenderEditable(box);
if (!renderEditable.hasFocus) {
return null;
}
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(renderEditable.selection),
renderEditable,
);
return endpoints[0].point + const Offset(0.0, -2.0);
}
class SigninButton extends StatelessWidget {
final Widget child;
final Gradient gradient;
final double width;
final double height;
final Function onPressed;
const SigninButton({
Key key,
#required this.child,
this.gradient,
this.width = double.infinity,
this.height = 50.0,
this.onPressed,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
width: width,
height: 50.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
gradient: LinearGradient(
colors: <Color>[
Color.fromRGBO(160, 92, 147, 1.0),
Color.fromRGBO(115, 82, 135, 1.0)
],
)),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
child: Center(
child: child,
)),
),
);
}
}
class TeddyController extends FlareControls {
// Store a reference to our face control node (the "ctrl_look" node in Flare)
ActorNode _faceControl;
// Storage for our matrix to get global Flutter coordinates into Flare world coordinates.
Mat2D _globalToFlareWorld = Mat2D();
// Caret in Flutter global coordinates.
Vec2D _caretGlobal = Vec2D();
// Caret in Flare world coordinates.
Vec2D _caretWorld = Vec2D();
// Store the origin in both world and local transform spaces.
Vec2D _faceOrigin = Vec2D();
Vec2D _faceOriginLocal = Vec2D();
bool _hasFocus = false;
// Project gaze forward by this many pixels.
static const double _projectGaze = 60.0;
String _password;
#override
bool advance(FlutterActorArtboard artboard, double elapsed) {
super.advance(artboard, elapsed);
Vec2D targetTranslation;
if (_hasFocus) {
// Get caret in Flare world space.
Vec2D.transformMat2D(_caretWorld, _caretGlobal, _globalToFlareWorld);
// To make it more interesting, we'll also add a sinusoidal vertical offset.
_caretWorld[1] +=
sin(new DateTime.now().millisecondsSinceEpoch / 300.0) * 70.0;
// Compute direction vector.
Vec2D toCaret = Vec2D.subtract(Vec2D(), _caretWorld, _faceOrigin);
Vec2D.normalize(toCaret, toCaret);
Vec2D.scale(toCaret, toCaret, _projectGaze);
// Compute the transform that gets us in face "ctrl_face" space.
Mat2D toFaceTransform = Mat2D();
if (Mat2D.invert(toFaceTransform, _faceControl.parent.worldTransform)) {
// Put toCaret in local space, note we're using a direction vector
// not a translation so transform without translation
Vec2D.transformMat2(toCaret, toCaret, toFaceTransform);
// Our final "ctrl_face" position is the original face translation plus this direction vector
targetTranslation = Vec2D.add(Vec2D(), toCaret, _faceOriginLocal);
}
} else {
targetTranslation = Vec2D.clone(_faceOriginLocal);
}
// We could just set _faceControl.translation to targetTranslation, but we want to animate it smoothly to this target
// so we interpolate towards it by a factor of elapsed time in order to maintain speed regardless of frame rate.
Vec2D diff =
Vec2D.subtract(Vec2D(), targetTranslation, _faceControl.translation);
Vec2D frameTranslation = Vec2D.add(Vec2D(), _faceControl.translation,
Vec2D.scale(diff, diff, min(1.0, elapsed * 5.0)));
_faceControl.translation = frameTranslation;
return true;
}
// Fetch references for the `ctrl_face` node and store a copy of its original translation.
#override
void initialize(FlutterActorArtboard artboard) {
super.initialize(artboard);
_faceControl = artboard.getNode("ctrl_face");
if (_faceControl != null) {
_faceControl.getWorldTranslation(_faceOrigin);
Vec2D.copy(_faceOriginLocal, _faceControl.translation);
}
play("idle");
}
onCompleted(String name) {
play("idle");
}
// Called by [FlareActor] when the view transform changes.
// Updates the matrix that transforms Global-Flutter-coordinates into Flare-World-coordinates.
#override
void setViewTransform(Mat2D viewTransform) {
Mat2D.invert(_globalToFlareWorld, viewTransform);
}
// Transform the [Offset] into a [Vec2D].
// If no caret is provided, lower the [_hasFocus] flag.
void lookAt(Offset caret) {
if (caret == null) {
_hasFocus = false;
return;
}
_caretGlobal[0] = caret.dx;
_caretGlobal[1] = caret.dy;
_hasFocus = true;
}
void setPassword(String value) {
_password = value;
}
bool _isCoveringEyes = false;
coverEyes(cover) {
if (_isCoveringEyes == cover) {
return;
}
_isCoveringEyes = cover;
if (cover) {
play("hands_up");
} else {
play("hands_down");
}
}
void submitPassword() {
if (_password == "bears") {
play("success");
} else {
play("fail");
}
}
}
typedef void CaretMoved(Offset globalCaretPosition);
typedef void TextChanged(String text);
// Helper widget to track caret position.
class TrackingTextInput extends StatefulWidget {
TrackingTextInput(
{Key key, this.onCaretMoved, this.onTextChanged, this.hint, this.label, this.isObscured = false})
: super(key: key);
final CaretMoved onCaretMoved;
final TextChanged onTextChanged;
final String hint;
final String label;
final bool isObscured;
#override
_TrackingTextInputState createState() => _TrackingTextInputState();
}
class _TrackingTextInputState extends State<TrackingTextInput> {
final GlobalKey _fieldKey = GlobalKey();
final TextEditingController _textController = TextEditingController();
Timer _debounceTimer;
#override
initState() {
_textController.addListener(() {
// We debounce the listener as sometimes the caret position is updated after the listener
// this assures us we get an accurate caret position.
if (_debounceTimer?.isActive ?? false) _debounceTimer.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
if (_fieldKey.currentContext != null) {
// Find the render editable in the field.
final RenderObject fieldBox =
_fieldKey.currentContext.findRenderObject();
Offset caretPosition = getCaretPosition(fieldBox);
if (widget.onCaretMoved != null) {
widget.onCaretMoved(caretPosition);
}
}
});
if (widget.onTextChanged != null) {
widget.onTextChanged(_textController.text);
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: TextFormField(
decoration: InputDecoration(
hintText: widget.hint,
labelText: widget.label,
),
key: _fieldKey,
controller: _textController,
obscureText: widget.isObscured,
validator: (value) {}),
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TeddyController _teddyController;
#override
initState() {
_teddyController = TeddyController();
super.initState();
}
#override
Widget build(BuildContext context) {
EdgeInsets devicePadding = MediaQuery.of(context).padding;
return Scaffold(
backgroundColor: Color.fromRGBO(93, 142, 155, 1.0),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 50,
left:0,
right: 0,
child: Container(
height: 200,
padding:
const EdgeInsets.only(left: 30.0, right: 30.0),
child: FlareActor(
"assets/Teddy.flr",
shouldClip: false,
alignment: Alignment.bottomCenter,
fit: BoxFit.contain,
controller: _teddyController,
)),),
Positioned(
child: SingleChildScrollView(
padding: EdgeInsets.only(
left: 20.0, right: 20.0, top: devicePadding.top + 150.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.all(Radius.circular(25.0))),
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TrackingTextInput(
label: "Email",
hint: "What's your email address?",
onCaretMoved: (Offset caret) {
_teddyController.lookAt(caret);
}),
TrackingTextInput(
label: "Password",
hint: "Try 'bears'...",
isObscured: true,
onCaretMoved: (Offset caret) {
_teddyController.coverEyes(caret != null);
_teddyController.lookAt(null);
},
onTextChanged: (String value) {
_teddyController.setPassword(value);
},
),
SigninButton(
child: Text("Sign In",
style: TextStyle(
fontFamily: "RobotoMedium",
fontSize: 16,
color: Colors.white)),
onPressed: () {
_teddyController.submitPassword();
})
],
)),
)),
])),
),
],
)),
);
}
}

Flutter: How to set boundaries for a Draggable widget?

I'm trying to create a drag and drop game. I would like to make sure that the Draggable widgets don't get out of the screen when they are dragged around.
I couldn't find an answer to this specific question. Someone asked something similar about constraining draggable area Constraining Draggable area but the answer doesn't actually make use of Draggable.
To start with I tried to implement a limit on the left-hand side.
I tried to use a Listener with onPointerMove. I've associated this event with a limitBoundaries method to detect when the Draggable exits from the left side of the screen. This part is working as it does print in the console the Offset value when the Draggable is going out (position.dx < 0). I also associated a setState to this method to set the position of the draggable to Offset(0.0, position.dy) but this doesn't work.
Could anybody help me with this?
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
void limitBoundaries(PointerEvent details) {
if (details.position.dx < 0) {
print(details.position);
setState(() {
position = Offset(0.0, position.dy);
});
}
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
onPointerMove: limitBoundaries,
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
position = drag.offset - posOffset;
});
},
),
),
);
}
}
Try this. I tweaked this from: Constraining Draggable area .
ValueNotifier<List<double>> posValueListener = ValueNotifier([0.0, 0.0]);
ValueChanged<List<double>> posValueChanged;
double _horizontalPos = 0.0;
double _verticalPos = 0.0;
#override
void initState() {
super.initState();
posValueListener.addListener(() {
if (posValueChanged != null) {
posValueChanged(posValueListener.value);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildDraggable(),
]));
}
_buildDraggable() {
return SafeArea(
child: Container(
margin: EdgeInsets.only(bottom: 100),
color: Colors.green,
child: Builder(
builder: (context) {
final handle = GestureDetector(
onPanUpdate: (details) {
_verticalPos =
(_verticalPos + details.delta.dy / (context.size.height))
.clamp(.0, 1.0);
_horizontalPos =
(_horizontalPos + details.delta.dx / (context.size.width))
.clamp(.0, 1.0);
posValueListener.value = [_horizontalPos, _verticalPos];
},
child: Container(
child: Container(
margin: EdgeInsets.all(12),
width: 110.0,
height: 170.0,
child: Container(
color: Colors.black87,
),
decoration: BoxDecoration(color: Colors.black54),
),
));
return ValueListenableBuilder<List<double>>(
valueListenable: posValueListener,
builder:
(BuildContext context, List<double> value, Widget child) {
return Align(
alignment: Alignment(value[0] * 2 - 1, value[1] * 2 - 1),
child: handle,
);
},
);
},
),
),
);
}
I've found a workaround for this issue. It's not exactly the output I was looking for but I thought this could be useful to somebody else.
Instead of trying to control the drag object during dragging, I just let it go outside of my screen and I placed it back to its original position in case it goes outside of the screen.
Just a quick note if someone tries my code, I forgot to mention that I'm trying to develop a game for the web. The output on a mobile device might be a little bit odd!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
if (drag.offset.dx > 0) {
position = drag.offset - posOffset;
} else {
position = widget.initPos;
}
});
},
),
),
);
}
}
I'm still interested if someone can find a proper solution to the initial issue :-)
you could use the property onDragEnd: of the widget Draggable and before setting the new position compare it with the height or width of your device using MediaQuery and update only if you didn't pass the limits of your screen, else set the new position to the initial one.
Example bellow :
Positioned(
left: position.dx,
top: position.dy,
child: Draggable(
maxSimultaneousDrags: 1,
childWhenDragging:
Opacity(opacity: .2, child: rangeEvent(context)),
feedback: rangeEvent(context),
axis: Axis.vertical,
affinity: Axis.vertical,
onDragEnd: (details) => updatePosition(details.offset),
child: Transform.scale(
scale: scale,
child: rangeEvent(context),
),
),
)
In the method updatePosition, you verify the new position before updating:
void updatePosition(Offset newPosition) => setState(() {
if (newPosition.dy > 10 &&
newPosition.dy < MediaQuery.of(context).size.height * 0.9) {
position = newPosition;
} else {
position = const Offset(0, 0);// initial possition
}
});