I have a strange requirement. I saw this this question on SO but i can't make it work for my case.
I have an animated container which represent my screen. On pressing the ADD icon. I'm transforming the screen like this (The column in right side image is below the homescreen) . But inside that AnimatedContainer there is a LIST(as child).
Every time I do transfromation. The list is re building itself. Is there any way i can avoid it. ?
You can imagine that the homescreen is pinned to wall with two nails in the left and right top. As I press FAB. The left top nail is pulled out and the screen hangs on right nail support. and when i again press FAB, the left top is again pinned with nail.
This is the widget I'm using
https://pub.dev/packages/matrix4_transform
Here is minimal code to see the rebuilding
import 'package:flutter/material.dart';
import 'package:matrix4_transform/matrix4_transform.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
_MyStatefulWidgetState? home;
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
DrawerManager drawerManager = DrawerManager();
callSetState() {
setState(() {});
}
#override
Widget build(BuildContext context) {
print('Rebuild');
home = this;
return AnimatedContainer(
transform: Matrix4Transform()
.translate(x: drawerManager.xOffSet, y: drawerManager.yOffSet)
.rotate(drawerManager.angle)
.matrix4,
duration: Duration(milliseconds: 500),
child: Scaffold(
body: MyList(drawerManager),
),
);
}
}
class MyList extends StatelessWidget {
final Data myData = Data();
final DrawerManager drawerManager;
MyList(this.drawerManager);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: myData.data.length+1,
itemBuilder: (context, index) {
print('Building list' + index.toString());
if(index == 4){
return GestureDetector(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
drawerManager.callback(drawerManager.isOpened);
}),
);
}
else{return ListTile(
title: Text(myData.data[index]),
);}
},
),
);
}
}
class Data {
List<String> data = ['Hello1', 'Hello2', 'Hello3', 'Hello4'];
}
class DrawerManager {
double xOffSet = 0;
double yOffSet = 0;
double angle = 0;
bool isOpened = false;
void callback(bool isOpen) {
print('Tapped');
if (isOpen) {
xOffSet = 0;
yOffSet = 0;
angle = 0;
isOpened = false;
} else {
xOffSet = 150;
yOffSet = 80;
angle = -0.2;
isOpened = true;
}
callSetState();
}
void callSetState() {
home!.callSetState();
}
}
You can see that When you press that + icon. Screen transforms and the lists are rebuilding.
please use this class ,
https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html
Performance optimizations
If your builder function contains a subtree that does not depend on
the animation, it's more efficient to build that subtree once instead
of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the
AnimatedBuilder will pass it back to your builder function so that you
can incorporate it into your build.
You can easily achieve this using provider library.
Give it try đź‘Ť
I will recommend to use Getx if you r beginner and if
have experience in app development then I will
Recommend to use Bloc library check both of them on
Pub.
Related
I have a working snippet that I've wrote, but I kinda don't understand how flutter is (re)using the widgets creating in the build method:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(calculateNextOffset());
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
left: offsets[j].dx,
top: offsets[j].dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (int k = 0; k < offsets.length; k++) {
offsets[k] = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
Every time I click on a container, i expect my "offsets" state to be updated, but I also expect all the AnimationPositioned widgets, GestureDetector widgets and the square widgets that you see would be rerendered.
With rerendered i mean they would disappear from the screen and new ones would be rerendered (and the animation from the first widgets would be cancelled and never displayed)
However it works? Could someone explain this to me?
EDIT: I've updated my snippet of code in my question to match what i'm asking, which i'm also going to rephrase here:
Every time I click on a square, i want that square to disappear and all the other square to randomly animate to another position. But every time I click on a square, another random square is deleted, and the one i'm clicking is animating.
I want the square that I click on disappears and the rest will animate.
Following up from the comments - Actually, the square you click is disappears. However, to see this visually add this to your container color:
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
Now, your offsets are generating just fine and random. However, because you have an AnimatedContainer widget. This widget will remember the last x & y position of your square and animate the new square starting from that old x,y value to the new on you passed it. So if you really want the square you click on disappear - you will need to either use Positioned widget:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 100.0;
static const squareHeight = 100.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
offsets.add(calculateNextOffset());
}
print(offsets);
List<Widget> squareWidgets = [];
for (var offset in offsets) {
squareWidgets.add(Positioned(
left: offset.dx,
top: offset.dy,
//curve: Curves.easeIn,
//duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
for (var i = 0; i < offsets.length; i++) {
offsets[i] = calculateNextOffset();
}
offsets.add(calculateNextOffset());
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
If you want the rest of the squares to animate while only the one that is clicked disappears. You will need to rethink your implementation and track all square perhaps using unique keys and custom animations. Hope that helps!
I've finally found it:
In the context of the snippet inside the question: Every time you click on a square, it will correctly remove that item, but the widgets are rerendered from that new list, and the last widget that was previously rendered will be removed instead of the one that I clicked.
This has to do because every widget in the widget tree is rendered as an element inside the element tree. If the state of the element inside the element tree is the same, it will not rerender that one. and they are all just blue squares in the end, so there is no distinction.
You can find a very nice video made by the flutter devs here:
When to Use Keys - Flutter Widgets 101 Ep. 4
Long story short: Here is the snippet with the fix, which is to add a Key to each widget, then the state will change on the element inside the element tree and it will rerender (and remove) the correct widgets/elements:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<OffsetData> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(OffsetData(UniqueKey(), calculateNextOffset()));
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
key: offsets[j].key, // This line is the trick
left: offsets[j].offset.dx,
top: offsets[j].offset.dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (var offsetData in offsets) {
offsetData.offset = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
class OffsetData {
Offset offset;
final Key key;
OffsetData(this.key, this.offset);
}
I want to create an Interface in which it is possible to drag your finger over several Areas. This changes the state of the areas to a selected state (See images).
What is the best way to approach this?
Start Position:
Start Dragging:
Select First Area:
Selected All Areas:
The code needs some updates for current Flutter/Dart versions but this worked for me.
Updated code:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
#override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext!.findAncestorRenderObjectOfType<RenderBox>()!;
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({required Widget child, required this.index, Key? key}) : super(child: child, key: key);
#override
_Foo createRenderObject(BuildContext context) {
return _Foo(index);
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
_Foo(this.index);
}
I use Rect class.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class StackOverflow extends StatefulWidget {
const StackOverflow({Key? key}) : super(key: key);
#override
_StackOverflowState createState() => _StackOverflowState();
}
class _StackOverflowState extends State<StackOverflow> {
late List<bool> isSelected;
late List<GlobalKey> myGlobalKey;
late List<Offset> offsetWidgets;
late List<Size> sizeWidgets;
late List<Rect> listRect;
#override
void initState() {
super.initState();
isSelected = List.generate(3, (index) => false);
myGlobalKey = List.generate(3, (index) => GlobalKey());
offsetWidgets = <Offset>[];
sizeWidgets = <Size>[];
listRect = <Rect>[];
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
for (final key in myGlobalKey) {
sizeWidgets
.add((key.currentContext!.findRenderObject() as RenderBox).size);
offsetWidgets.add((key.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero));
}
for (int i = 0; i < 3; i++) {
final dx = offsetWidgets[i].dx + sizeWidgets[i].width;
final dy = offsetWidgets[i].dy + sizeWidgets[i].height;
listRect.add(Rect.fromPoints(offsetWidgets[i], Offset(dx, dy)));
}
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
if (listRect[0].contains(pointerMoveEvent.position)) {
if (!isSelected[0]) {
setState(() {
isSelected[0] = true;
});
}
} else if (listRect[1].contains(pointerMoveEvent.position)) {
if (!isSelected[1]) {
setState(() {
isSelected[1] = true;
});
}
} else if (listRect[2].contains(pointerMoveEvent.position)) {
if (!isSelected[2]) {
setState(() {
isSelected[2] = true;
});
}
}
},
child: Container(
color: Colors.amber,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawMaterialButton(
key: myGlobalKey[0],
fillColor: isSelected[0] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[0] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[1],
fillColor: isSelected[1] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[1] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[2],
fillColor: isSelected[2] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[2] = false;
});
},
),
],
),
),
);
}
}
This package: drag_select_grid_view offers another related approach. From the code, you can see among other fun things:
Use of a GestureDetector to encapsulate the selection area (a GridView here)
In the GridView.itemBuilder, a custom ProxyWidget (his Selectable) wraps the normal widget builder for your selectable items. This is used to expose mount/unmount points in order to hang onto the corresponding custom ProxyElement.
When a tap/motion is detected, he uses the current context to get a RenderObject covering the selection area to do manual hit testing with the local position Offset, by checking if any of the cached Elements contain the point, using each item's bounding box in the selection area's coordinate system. (see _findIndexOfSelectable and Selectable.containsOffset) (This is like #mario's answer, and presumably could be costlier than #a.shak's if there are many possible Elements to choose from on
screen.)
Results are passed back to the user through a ValueNotifier, which also lets the user control clearing or setting a custom the selection. (see the controller code)
For contrast, I'll try to describe #a.shak's answer in words:
In his GridState class wrap a Listener around the subtree representing your selection area. (although a GestureDetector could work, too)
in onPointerDown|Move, start detecting; onPointerUp you can clear/etc.
Detection requires getting the subtree’s RenderBox (a RenderObject) so you can do hitTesting with the pointer’s local position to find other intersecting ROs. Given the selection area's RB, convert the pointer to its local coordinates and do the RenderBox.hitTest, then walk along the BoxHitTestResult.path of intersecting objects to check whether any HitTestEntry is of a type we know can be selected. (i.e. the _Foo extends RenderProxyBox class - see below)
If it's a match, success! Track its info for UI updates and later use elsewhere.
Use a GlobalKey with the GridView to get the RenderBox corresponding to the selection area’s extents during hit testing. (Probably don’t need this as you can use the State’s own context…)
In The GridView.itemBuilder, wrap your selectable objects in a custom SingleChildRenderObjectWidget, used to get the item's RenderBox for hit testing and storing info.
Store info here like your item's index and push it down into a custom RenderBox that our SCROW creates.
Uses a RenderProxyBox since we don’t actually care about controlling the rendering; just delegate it all to the child. This custom class also lets us find our selectable object(s) of interest more easily during the hit test (see _detectTapedItem).
So in both cases, you need to implement some extra custom classes (ProxyWidget+ProxyElement vs SingleChildRenderObjectWidget+RenderProxyBox) in order to get the right RenderBoxes for hit testing with the selected point on the screen, and to store misc info like an item's index to update the UI and use later.
For custom shapes, you can have your CustomPainter override its hitTest method to leverage Path.contains() to restrict touches to be within the path only. See this answer. Or just use a a package like touchable to give your shapes gesture callbacks.
I am developing a maze app on flutter. I have a grid of 100 flatButtons that all start as grey. When a flatButton is pressed, I want it to turn green if the move is legitimate and red if the move is illegitimate (I used the splashColor property for this). Moves are constantly changing from illegitimate to legitimate (based on where the user currently is) and the splashColor of all the buttons should be dynamically updating. Right now, only the splashColor of a button only changes if I click it. I want the entire gridView of buttons to update their splashColor property whenever any button is pressed. Code is attached. Any help is much appreciated!
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:flutter/scheduler.dart' show timeDilation;
//lastmove initially set to 1 for simplicity.
int lastMove=1;
class GameButton extends StatefulWidget {
final int id;
int onPath=0;
bool testOnPath(){
if (this.onPath==1){
return true;
}
else {
return false;
}
}
bool testIfLegal(lastMove){
return (((this.id-lastMove).abs()==1) ^ ((this.id-lastMove).abs()==10));
}
bool moveCheck(){
if(this.testIfLegal(lastMove) & this.testOnPath()){
return true;
}
else{
return false;
}
}
GameButton(this.id, lastMove);
#override
_GameButtonState createState() => _GameButtonState();
}
class _GameButtonState extends State<GameButton> {
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: FlatButton(
color:Colors.grey,
splashColor: widget.moveCheck()?Colors.green:Colors.red,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
bool a=widget.moveCheck();
if(a){
setState(() {
lastMove=widget.id;
print("GOOD MOVE");
}
);
}
else{
print("Illegitmate Move");
}
},
),
);
}
}
class Maze extends StatefulWidget {
List<GameButton> empty_grid = [for(var i=0; i<100; i++) new GameButton(i,lastMove)];
#override
_MazeState createState() => _MazeState();
}
class _MazeState extends State<Maze> {
#override
Widget build(BuildContext context) {
}
}
void main() {
//slow down animation to give user more time
timeDilation = 3.0;
Maze maze1 = new Maze();
//filling maze manually-will be done with function in future
(maze1.empty_grid[0]).onPath = 1;
(maze1.empty_grid[10]).onPath = 1;
(maze1.empty_grid[20]).onPath = 1;
(maze1.empty_grid[30]).onPath = 1;
(maze1.empty_grid[40]).onPath = 1;
(maze1.empty_grid[50]).onPath = 1;
(maze1.empty_grid[60]).onPath = 1;
(maze1.empty_grid[70]).onPath = 1;
(maze1.empty_grid[80]).onPath = 1;
(maze1.empty_grid[90]).onPath = 1;
(maze1.empty_grid[91]).onPath = 1;
(maze1.empty_grid[92]).onPath = 1;
(maze1.empty_grid[93]).onPath = 1;
(maze1.empty_grid[94]).onPath = 1;
(maze1.empty_grid[95]).onPath = 1;
(maze1.empty_grid[96]).onPath = 1;
(maze1.empty_grid[97]).onPath = 1;
(maze1.empty_grid[98]).onPath = 1;
(maze1.empty_grid[99]).onPath = 1;
//done filling maze1
return runApp(
MaterialApp(
home: Scaffold(
backgroundColor: Colors.blue,
appBar: AppBar(
title: Text('Gorton Maze Test'),
backgroundColor: Colors.blueAccent,
),
body: Center(
child: GridView.builder(
itemCount: 100,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 10, crossAxisSpacing: 0, mainAxisSpacing: 0),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index){
return maze1.empty_grid[index];
},
)
),
),
)
);
}
Yes. You can update entire GridView if you wrap by StreamController.
On the other hand, I don't think do everything in main class is a great idea. You should split class and function for easier. It'll make you code cleaner.
body: HomePage()),
And in HomePage, I using stateful widget for more reasons.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
StreamController _controller;
#override
void initState() {
super.initState();
_controller = new StreamController();
}
#override
void dispose() {
_controller.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: _controller.stream,
builder: (_, __) {
return GridView.builder(
itemCount: 100,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 10,
crossAxisSpacing: 0,
mainAxisSpacing: 0),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return maze1.empty_grid[index];
},
);
}));
}
}
And every when you want to update your gridview. You can add StreamController with new value
_controller.add(null);
Hope this idea will help you.
With your existing design, you can achieve it by having a callback defined in GameButton that points to a method in the Maze class. Each time the button is pressed after changing the color of the button you call the callback into Maze class where you change the grid color as well. Sudo code:
class GameButton extends StatefulWidget {
final VoidCallback clickCallback
GameButton(this.id, lastMove, this.clickCallback);
.....
}
class Maze extends StatefulWidget {
List<GameButton> empty_grid = [for(var i=0; i<100; i++) new GameButton(i,lastMove, onButtonPressed)];
#override
_MazeState createState() => _MazeState();
void onButtonPressed(){
// change grid color based on index
}
}
class _MazeState extends State<Maze> {
#override
Widget build(BuildContext context) {
}
}
You can do it by calling the setstate method.
while clicking on the button after writing your logic use the setstate method. Basically setstate will rebuild the screen. so you can do something like this.
I am a JavaScript developer and I am new to Flutter. I just want to animate a set of images on mouse hover like this using Flutter for Web. It includes Scaling, Opacity and Grayscale transformations. How to accomplish this in Flutter?
Thanks in advance.
Other than the animation part of your question. The onHover argument of the InkWell only works if you specify the onTap argument first.
InkWell(
child: SomeWidget(),
onTap: () {
//You can leave it empty, like that.
}
onHover: (isHovering) {
if (isHovering) {
//The mouse is hovering.
} else {
//The mouse is no longer hovering.
}
}
)
From the documentation, here's the benefit of the boolean, which is passed to the onHover callback:
The value passed to the callback is true if a pointer has entered this part of
the material and false if a pointer has exited this part of the material.
This is just a demo to show that you can use onHover of Inkwell widget to accomplish the task. You will have to come up with the logic to decide how much offset and scale should be used and how to position the widget. In my example I have used a grid view. You can perhaps use a stack to set the currently active widget based on the hover.
Here is the example with a grid view. The live version of this is available in this dartpad.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
children: <Widget>[ImageHover(),ImageHover(),ImageHover(),ImageHover(),ImageHover(),ImageHover(),ImageHover(),],
);
}
}
class ImageHover extends StatefulWidget {
#override
_ImageHoverState createState() => _ImageHoverState();
}
class _ImageHoverState extends State<ImageHover> {
double elevation = 4.0;
double scale = 1.0;
Offset translate = Offset(0,0);
#override
Widget build(context) {
return InkWell(
onTap: (){},
onHover: (value){
print(value);
if(value){
setState((){
elevation = 20.0;
scale = 2.0;
translate = Offset(20,20);
});
}else{
setState((){
elevation = 4.0;
scale = 1.0;
translate = Offset(0,0);
});
}
},
child: Transform.translate(
offset: translate ,
child: Transform.scale(
scale: scale,
child: Material(
elevation: elevation,
child: Image.network(
'https://i.ytimg.com/vi/acm9dCI5_dc/maxresdefault.jpg',
),
),
),
),
);
}
}
Just create an extension
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension HoverExtension on Widget {
Widget get translateOnHover {
return kIsWeb ? TranslateOnHover(child: this) : ThisContainer(child: this);
}
}
class ThisContainer extends StatelessWidget {
ThisContainer({this.child});
final child;
#override
Widget build(BuildContext context) {
return Container(child: child);
}
}
class TranslateOnHover extends StatefulWidget {
final Widget child;
TranslateOnHover({required this.child});
#override
_TranslateOnHoverState createState() => _TranslateOnHoverState();
}
class _TranslateOnHoverState extends State<TranslateOnHover> {
double scale = 1.0;
#override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (e) => _mouseEnter(true),
onExit: (e) => _mouseEnter(false),
child: TweenAnimationBuilder(
duration: const Duration(milliseconds: 200),
tween: Tween<double>(begin: 1.0, end: scale),
builder: (BuildContext context, double value, _) {
return Transform.scale(scale: value, child: widget.child);
},
),
);
}
void _mouseEnter(bool hover) {
setState(() {
if (hover)
scale = 1.03;
else
scale = 1.0;
});
}
}
And use it anywhere by calling
yourWidget.translateOnHover
It's sad that there's not a built-in feature already, given that Flutter extended to web also, but luckily there is a package for this: hovering 1.0.4
You just need to install the package running this command flutter pub add hovering or adding hovering: ^1.0.4 to your dependencies. Then you can use HoverWidget,HoverContainer, HoverAnimatedContainer, and some more. It's not perfect, but it's an easy way to do it, specially for not complicated animations.
You can check the official docs of the package for more info: https://pub.dev/packages/hovering
I'm not sure if how to do this.
I have a Column of AnimatedContainers. Initially all of them are 200 height. I want to implement some kind of callback, so when user tap on one item, this one becomes smaller(say 100 height) and the rest of the item in the list disappear. My AnimatedContainers are Stateful widgets
I guess I would have to use to callbacks, one for the columnn (parent) and the other to notify the children, but I don't know how to do this.
Summarised, what I have right now is
Stateless(Column(Stateful(List<AnimatedContainer>)))
If something is not clear please comment
Thanks
EDIT to add some code and more info
class SectionButton extends StatefulWidget {
double screenHeight;
Stream stream;
int index;
SectionButton(this.screenHeight, this.stream, this.index);
#override
_SectionButtonState createState() => _SectionButtonState();
}
class _SectionButtonState extends State<SectionButton> {
double height;
StreamSubscription streamSubscription;
initState() {
super.initState();
this.height = this.widget.screenHeight / n_buttons;
streamSubscription =
widget.stream.listen((_) => collapse(this.widget.index));
}
void collapse(int i) {
if (this.widget.index != i) {
setState(() {
this.height = 0;
});
} else {
setState(() {
this.height = this.widget.screenHeight / appBarFraction;
});
}
}
#override
dispose() {
super.dispose();
streamSubscription.cancel();
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
height: this.height,
duration: Duration(milliseconds: 300),
);
}
}
class HomeScreen extends StatelessWidget {
List<Widget> sections = [];
bool areCollapsed = false;
final changeNotifier = new StreamController.broadcast();
void createSections(screenHeight) {
for (var i = 0; i < buttonNames.length; i++) {
this.sections.add(GestureDetector(onTap:(){
print("Section i was tapped");
changeNotifier.sink.add(i);}, //THIS IS NOT WORKING
child: SectionButton(screenHeight, changeNotifier.stream, i),),);
}
}
#override
Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
double screenHeight =
mediaQueryData.size.height - mediaQueryData.padding.vertical;
createSections(screenHeight);
return SafeArea(
child: SizedBox.expand(
child: Column(children: this.sections)
),
);
}
}
BTW what I'm trying to implement is something like this:
You have to store your height in a variable and wrap your widget with GestureDetector
GestureDetector(
onTap: () {
setState(() { _height = 100.0; });
},
child: Container(
child: Text('Click Me'),
),
)
Alternatively, you can also use AnimatedSize Widget.
Animated widget that automatically transitions its size over a given
duration whenever the given child's size changes.