How to Calculate the Coordinates of touch in Flutter? - dart

I want show a popup at touch Coordinates. I am using Stack and Positioned widgets to place the popup.

You can add a GestureDetector as parent of stack, and register onTapDownDetails listener. This should call your listener on every tapdown event, with global offset of the tap in TapDownDetails parameter of the your listener.
Here is sample code demonstrating the same.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Popup Demo'),
),
body: new MyWidget());
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
double posx = 100.0;
double posy = 100.0;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: new Stack(fit: StackFit.expand, children: <Widget>[
// Hack to expand stack to fill all the space. There must be a better
// way to do it.
new Container(color: Colors.white),
new Positioned(
child: new Text('hello'),
left: posx,
top: posy,
)
]),
);
}
}

You can simply use a Listener as the parent of your Stack and listen to it's onPointerDown event like so:
Listener(
onPointerDown: (event) {
// use event.localPosition.dx or event.localPosition.dy
},
child: Stack(
children: [
],
),
)

Related

Flutter - RepaintBoundary causes state reset of StatefulWidget

I have a preview widget that loads data after a user tap. This state (already tapped or not) should not be lost while scrolling (the preview is located in a list) or navigating through other screen.
The scrolling is solved by adding AutomaticKeepAliveClientMixin which saves the state when scrolling away.
Now i also need to wrap the preview widget (actually a more complex widget that contains the preview) with a RepaintBoundary, to be able to make a "screenshot" of this widget alone.
Before i wrap the widget with a RepaintBoundary, the state is saved both while scrolling and navigating to another screen.
After i add the RepaintBoundary the scrolling still works but for navigation the state is reset.
How can i wrap a Stateful widget that should hold its state with a RepaintBoundary?
Code is a simplified example of my implementation with the same problem.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Test';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: TestList(40),
),
);
}
}
class TestList extends StatefulWidget {
final int numberOfItems;
TestList(this.numberOfItems);
#override
_TestListState createState() => _TestListState();
}
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary(
key: GlobalKey(),
child: Preview()
);
},
);
}
}
class Preview extends StatefulWidget {
#override
_PreviewState createState() => _PreviewState();
}
class _PreviewState extends State<Preview> with AutomaticKeepAliveClientMixin {
bool loaded;
#override
void initState() {
super.initState();
print('_PreviewState initState.');
loaded = false;
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('_PreviewState build.');
if(loaded) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
},
child: ListTile(
title: Text('Loaded. Tap to navigate.'),
leading: Icon(Icons.visibility),
),
);
} else {
return GestureDetector(
onTap: () {
setState(() {
loaded = true;
});
},
child: ListTile(
title: Text('Tap to load.'),
),
);
}
}
}
class NewScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('New Screen')),
body: Center(
child: Text(
'Navigate back and see if loaded state is gone.',
style: TextStyle(fontSize: 14.0),
),
),
);
}
}
Take a look at RepaintBoundary.wrap, it assigns the RepaintBoundary widget a key based on its child or childIndex so state is maintained:
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary.wrap(
Preview(),
index,
);
},
);
}
}
https://api.flutter.dev/flutter/widgets/RepaintBoundary/RepaintBoundary.wrap.html
EDIT: As per the below comments, it looks like this solution would break the screenshot ability so you'd have to store the list of children widgets in your state like so:
class _TestListState extends State<TestList> {
List<Widget> _children;
#override
void initState() {
super.initState();
_children = List.generate(
widget.numberOfItems,
(_) => RepaintBoundary(
key: GlobalKey(),
child: Preview(),
));
}
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView(children: _children);
}
}

How to delete GridView item in flutter?

I'm trying to dynamically delete simple grid item on long press;
I've tried the most obvious way: created a list of grid data, and called setState on addition or deletion of the item.
UPD: Items works properly in the list, since it's initialisation loop moved to initState() method (just as #jnblanchard said in his comment), and don't generate new items at every build() call, but deletion is still doesn't work.
If it has more items, than can fit the screen, it deletes last row, (when enough items deleted), otherwise the following exception is thrown:
I/flutter (28074): The following assertion was thrown during performLayout():
I/flutter (28074): SliverGeometry is not valid: The "maxPaintExtent" is less than the "paintExtent".
I/flutter (28074): The maxPaintExtent is 540.0, but the paintExtent is 599.3. By definition, a sliver can't paint more
I/flutter (28074): than the maximum that it can paint!
My test code now:
main class
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of grid data
List<Widget> gridItemsList = [];
#override
void initState(){
super.initState();
//----filling the list----
for(int i =0; i<10; i++){
gridItemsList.add(
TestTile(i, (){
//adding callback for long tap
delete(i);
})
);
}
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
int index = gridItemsList.length+1;
add(
new TestTile(index, (){
delete(index);
})
);
},
),
]
),
body: GridView(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
children: gridItemsList
)
);
}
///method for adding the items
void add(Widget toAdd){
setState(() {
TestTile tile = toAdd as TestTile;
gridItemsList.add(toAdd);
print("tile number#${tile.index} added");
});
}
///method for deleting the items
void delete(int index){
setState(() {
gridItemsList.removeAt(index);
print("tile number#$index is deleted");
});
}
}
and separate widget class for grid items
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class TestTile extends StatelessWidget{
int _index;
var _callback;
TestTile(this._index, this._callback);
get index => _index;
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("data#$_index")
)
)
),
);
}
}
How can I delete an item from grid view?
p.s. the provided code is just my attempts of solving the problem - you can offer another way, if you want!
I wrote this up from the example app, it has a few things that you may find useful. Notably I abstract the list data-structure by holding the length of the list inside a stateful widget. I wrote this with a ListView but I think you could change that to a GridView without any hiccups.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(
title: Text("Owl"),
actions: <Widget>[IconButton(icon: Icon(Icons.remove), onPressed: () => this.setState(() => _counter > 1 ? _counter-- : _counter = 0)), IconButton(icon: Icon(Icons.add), onPressed: () => this.setState(() => _counter++))],
),
body: ListView.builder(itemExtent: 50, itemCount: _counter, itemBuilder: (context, index) => Text(index.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.title))
);
}
}
Finally I've got what I wanted.
I'll leave it here for someone who might have the same problem :)
Main class:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of some data
List<Person> partyInviteList = [];
_PrototypeState(){
//filling the list
for(int i=0; i<5; i++){
partyInviteList.add(Person.generateRandomPerson());
}
print("Person ${partyInviteList.toString()}");
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
//generating an item on tap
onPressed: () {
setState(() {
partyInviteList.add(Person.generateRandomPerson());
});
},
),
]
),
body: GridView.count(
crossAxisCount: 2,
children: List.generate(partyInviteList.length, (index) {
//generating tiles with people from list
return TestTile(
partyInviteList[index], (){
setState(() {
print("person ${partyInviteList[index]} is deleted");
partyInviteList.remove(partyInviteList[index]);
});
}
);
})
)
);
}
}
///person class
class Person{
Person(this.firstName, this.lastName);
static List<String> _aviableNames = ["Bob", "Alise", "Sasha"];
static List<String> _aviableLastNames = ["Green", "Simpson", "Stain"];
String firstName;
String lastName;
///method that returns random person
static Person generateRandomPerson(){
Random rand = new Random();
String randomFirstName = _aviableNames[rand.nextInt(3)];
String randomLastName = _aviableLastNames[rand.nextInt(3)];
return Person(randomFirstName, randomLastName);
}
#override
String toString() {
return "$firstName $lastName";
}
}
Support class:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/Prototype.dart';
class TestTile extends StatelessWidget{
final Person person;
var _callback;
TestTile(this.person, this._callback);
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("${person.toString()}")
)
)
),
);
}
}

Add time to countdown timer during while counting down in Flutter

I am trying to create a countdown timer application where I can press a button to add more time while the timer is actively counting down just like every microwave oven where there is a button you can press to add an extra minute to it's run time while it's running without stopping anything.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
));
}
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return new Text(
animation.value.toString(),
style: new TextStyle(fontSize: 150.0),
);
}
}
class MyApp extends StatefulWidget {
State createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;
static const int kStartValue = 4;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: new Duration(seconds: kStartValue),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.play_arrow),
onPressed: () => _controller.forward(from: 0.0),
),
body: new Container(
child: new Center(
child: new Countdown(
animation: new StepTween(
begin: kStartValue,
end: 0,
).animate(_controller),
),
),
),
);
}
}
This example from a similar question about timers makes sense to me and has been my jumping off point. I know I need to change the duration and replace the animation with a new one with the appropriate duration, but I never get anything close to the correct behavior I am looking for.
I think the below is what you're trying to accomplish. Tapping the FloatingActionButton adds 5 seconds.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Countdown Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Countdown Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _timeRemaining = 10;
Timer _timer;
#override
void initState() {
_timer = Timer.periodic(Duration(seconds: 1), (Timer t) => _getTime());
super.initState();
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text('$_timeRemaining'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_timeRemaining += 5;
}),
);
}
void _getTime() {
setState(() {
_timeRemaining == 0 ? _timeRemaining = 0 : _timeRemaining--;
});
}
}

Flutter Listview in stateless widget with initial offset

I have my own StatelessWidget with a ListView. I want it's state to be managed by parent StatefulWidget.
The behaviour I desire is that if I change a value, listView scrolls (or even jumps - it doesn't matter) to that value.
I thought that if I create stateless widget every time parent's setState() method is being invoked, the scrollController with initialOffset would make the list "move" but it doesn't. What is worth mentioning is that on first build initialOffset works as it should.
Here is example code of my problem:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 5;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new MyClass(_counter),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
child: new Icon(Icons.add),
),
);
}
}
class MyClass extends StatelessWidget {
final int extraValue;
final ScrollController scrollController;
MyClass(this.extraValue):
scrollController = new ScrollController(initialScrollOffset: extraValue*50.0);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemExtent: 50.0,
itemCount: 100,
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
if (index != extraValue)
return new Text(index.toString());
else
return new Text("EXTRA" + index.toString());
});
}
}
I'm not sure if it's a bug or my mistake.
Any ideas might be helpful :)
EDIT:
Inspired by Ian Hickson's answer I have solution to my problem:
void _incrementCounter() {
setState(() {
_counter++;
myClass.scrollController.animateTo(_counter*50.0, duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
});
}
The initial offset is... the initial offset. Not the current offset. :-)
You can cause the offset to change by calling methods on the ScrollController, like animateTo.

Flutter - Create a countdown widget

I am trying to build a countdown widget. Currently, I got the structure to work. I only struggle with the countdown itself. I tried this approach using the countdown plugin:
class _Countdown extends State<Countdown> {
int val = 3;
void countdown(){
CountDown cd = new CountDown(new Duration(seconds: 4));
cd.stream.listen((Duration d) {
setState((){
val = d.inSeconds;
});
});
}
#override
build(BuildContext context){
countdown();
return new Scaffold(
body: new Container(
child: new Center(
child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
),
),
);
}
}
However, the value changes very weirdly and not smooth at all. It start twitching. Any other approach or fixes?
It sounds like you are trying to show an animated text widget that changes over time. I would use an AnimatedWidget with a StepTween to ensure that the countdown only shows integer values.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
));
}
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return new Text(
animation.value.toString(),
style: new TextStyle(fontSize: 150.0),
);
}
}
class MyApp extends StatefulWidget {
State createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;
static const int kStartValue = 4;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: new Duration(seconds: kStartValue),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.play_arrow),
onPressed: () => _controller.forward(from: 0.0),
),
body: new Container(
child: new Center(
child: new Countdown(
animation: new StepTween(
begin: kStartValue,
end: 0,
).animate(_controller),
),
),
),
);
}
}
The countdown() method should be called from the initState() method of the State object.
class _CountdownState extends State<CountdownWidget> {
int val = 3;
CountDown cd;
#override
void initState() {
super.initState();
countdown();
}
...
Description of initState() from the Flutter docs:
The framework calls initState. Subclasses of State should override
initState to perform one-time initialization that depends on the
BuildContext or the widget, which are available as the context and
widget properties, respectively, when the initState method is called.
Here is a full working example:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Countdown Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new CountdownWidget();
}
}
class _CountdownState extends State<CountdownWidget> {
int val = 3;
CountDown cd;
#override
void initState() {
super.initState();
countdown();
}
void countdown(){
print("countdown() called");
cd = new CountDown(new Duration(seconds: 4));
StreamSubscription sub = cd.stream.listen(null);
sub.onDone(() {
print("Done");
});
sub.onData((Duration d) {
if (val == d.inSeconds) return;
print("onData: d.inSeconds=${d.inSeconds}");
setState((){
val = d.inSeconds;
});
});
}
#override
build(BuildContext context){
return new Scaffold(
body: new Container(
child: new Center(
child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
),
),
);
}
}
class CountdownWidget extends StatefulWidget {
#override
_CountdownState createState() => new _CountdownState();
}
based on #raju-bitter answer, alternative to use async/await on countdown stream
void countdown() async {
cd = new CountDown(new Duration(seconds:4));
await for (var v in cd.stream) {
setState(() => val = v.inSeconds);
}
}
Why not use a simple TweenAnimationBuilder its easy to use and you don't need to manage any stream controllers or worry about using streams and disposing them off etc;
TweenAnimationBuilder<double>(
duration: Duration(seconds: 10),
tween: Tween(begin: 100.0, end: 0.0),
onEnd: () {
print('Countdown ended');
},
builder: (BuildContext context, double value, Widget child) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Text('${value.toInt()}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 40)));
}),
here's the dartpad example to playaround
output:
originally answered here
Countdown example using stream, not using setState(...) therefore its all stateless.
this borrow idea from example flutter_stream_friends
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
static String appTitle = "Count down";
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: appTitle,
theme: new ThemeData(
primarySwatch: Colors.purple,
),
home: new StreamBuilder(
stream: new CounterScreenStream(5),
builder: (context, snapshot) => buildHome(
context,
snapshot.hasData
// If our stream has delivered data, build our Widget properly
? snapshot.data
// If not, we pass through a dummy model to kick things off
: new Duration(seconds: 5),
appTitle)),
);
}
// The latest value of the CounterScreenModel from the CounterScreenStream is
// passed into the this version of the build function!
Widget buildHome(BuildContext context, Duration duration, String title) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Center(
child: new Text(
'Count down ${ duration.inSeconds }',
),
),
);
}
}
class CounterScreenStream extends Stream<Duration> {
final Stream<Duration> _stream;
CounterScreenStream(int initialValue)
: this._stream = createStream(initialValue);
#override
StreamSubscription<Duration> listen(
void onData(Duration event),
{Function onError,
void onDone(),
bool cancelOnError}) =>
_stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
// The method we use to create the stream that will continually deliver data
// to the `buildHome` method.
static Stream<Duration> createStream(int initialValue) {
var cd = new CountDown(new Duration(seconds: initialValue));
return cd.stream;
}
}
The difference from stateful is that reload the app will restart counting. When using stateful, in some cases, it may not restart when reload.

Resources