Flutter - Save Multiple Widget Transformation Before setState - flutter

I have a Stack with multiple MatrixGesture Containers with images that can be drag around, pinch zoom and rotate. I want to save the state of each container in the position and shape that it is, because after a little change using setState, everything go back to the original position.
All the information of the images is in a List with a specific object type.
Here is a snippet of the code to display the images:
class NewOutfitState extends State<NewOutfit> {
List<DisplayGarment> garmentsList;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("New"),
body: new Container(
child:MyList(myList: garmentsList, canvasSize: realCanvas,);
)
}
}
class MyListState extends State<MyList>{
List<DisplayGarment> myList;
double realCanvas;
bool flag=false;
int touch=0;
#override
void initState() {
super.initState();
myList=widget.myList;
realCanvas = widget.canvasSize;
}
#override
Widget build(BuildContext context) {
return Stack(
children: getList(),
);
}
List<Widget> getList(){
List<Widget> listWidget=[];
for(int i=0;i<myList.length;i++) {
final ValueNotifier<Matrix4> notifier1= ValueNotifier(Matrix4.identity());
DisplayGarment _garments = myList[i];
listWidget.add(
MatrixGestureDetector(
key: Key(i.toString()),
onMatrixUpdate: (m, tm, sm, rm) {
notifier1.value = m;
},
child: AnimatedBuilder(
animation: notifier1,
builder: (ctx, child) {
return Transform(
transform: notifier1.value,
child: Stack(
children: <Widget>[
Container(
padding: EdgeInsets.all(4),
alignment: Alignment(0, -0.5),
child:
Container(
padding: EdgeInsets.all(0),
height: imgHeigh,
width: imgWidth,
child:
DottedBorder(
color: Colors.transparent,
strokeWidth: 0,
child: Center(
child: CachedNetworkImage(
imageUrl:
'https://fashiers.com/garments_img/'+imgUrl,
height: imgHeigh,
width: imgWidth,
)),
);
,)
)
],),);},),
)
);
}
return listWidget;
}
}
Also is it possible to set a border around the image when that is tap on and remove border on other that was probably tap before?
I appreciate any help with this.

For the first part, you should not put notifier1 initialization inside the build function. That's why every time you use setStatus, your position reset. This part should be initialize inside initState() and that means you should keep all garments location value in this class. (e.g. List<ValueNotifier>)
Second part, indexing the garments may be the instinctive solution. Display/Hide border by:
child: _selectedIndex == i ?
// Widget with border
: //Widget without border
and set _selectedIndex inside onMatrixUpdate()

Related

How to animate multiple widgets while scrolling in Flutter

I need to implement custom animation while scrolling the list of users. See an example
My current view is composed of next elements:
SingleChildScrollView contains Column with:
Row of three top elements (each of is a custom widget with basically Stack of avatar, medal and details (Column))
Row as a table header
ListView of other users.
SingleChildScrollView is wrapped with NotificationListener for ScrollNotification which is populated to provider. The scroll value is then listened in every top element to perform animation of its own.
I would like to know some general path and algorithm here to take. I tried AnimatedPositioned but as soon as it is applied on multiple elements it causes performance issues. Should I use AnimationController or some more custom things so far? Any help would be appreciated.
As pskink mentioned, using SliverPersistentHeader can be archive, This is a demo widget to illustrate how it can be done. You need to play with value. My favorite part is using .lerp , doubleLerp... to position the items.
class Appx extends StatelessWidget {
const Appx({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: CustomSliverPersistentHeaderDelegate(),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 3333,
width: 200,
),
),
],
),
);
}
}
class CustomSliverPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return LayoutBuilder(builder: (_, constraints) {
final t = shrinkOffset / maxExtent;
final width = constraints.maxWidth;
final itemMaxWidth = width / 4;
double xFactor = -.4;
return ColoredBox(
color: Colors.cyanAccent.withOpacity(.3),
child: Stack(
children: [
Align(
alignment:
Alignment.lerp(Alignment.center, Alignment(xFactor, -.2), t)!
..x,
child: buildRow(
color: Colors.deepPurple, itemMaxWidth: itemMaxWidth, t: t),
),
Align(
alignment: Alignment.lerp(
Alignment.centerRight, Alignment(xFactor, 0), t)!,
child:
buildRow(color: Colors.red, itemMaxWidth: itemMaxWidth, t: t),
),
Align(
alignment: Alignment.lerp(
Alignment.centerLeft, Alignment(xFactor, .2), t)!,
child: buildRow(
color: Colors.amber, itemMaxWidth: itemMaxWidth, t: t),
),
],
),
);
});
}
Container buildRow(
{required Color color, required double itemMaxWidth, required double t}) {
return Container(
width: lerpDouble(itemMaxWidth, itemMaxWidth * .3, t),
height: lerpDouble(itemMaxWidth, itemMaxWidth * .3, t),
color: color,
);
}
/// you need to increase when it it not pinned
#override
double get maxExtent => 400;
#override
double get minExtent => 300;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}

Flutter - Draggable AND Scaling Widgets

So for this application (Windows, Web) I have 2 requirements:
User can drag around widgets on the screen (drag and drop) to any location.
The app must scale to screen/window size
For (1) I used this answer.
For (2) I used this solution.
As mentioned in the code comment below I can't have both:
If I set logicWidth and logicHeight dynamically depending on the window size, the dragging works fine but the draggable widgets won't scale but instead stay the same size regardless of the window size.
If I set logicWidth and logicHeight to a constant value (the value of the current cleanHeight ) the dragging will be messed up for other screen sizes but then the draggable widgets will scale correctly with the window size.
In other words: for the dragging to work nicely these values need to be matching the window size at any time. But by changing these values I ruin the scaling I need.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
//containing widgets to drag around
const List<Widget> draggableWidgets = [
DraggableWidget(
draggableWidget: CircleAvatar(
backgroundColor: Colors.green,
radius: 32,
)),
DraggableWidget(
draggableWidget: CircleAvatar(
backgroundColor: Colors.red,
radius: 24,
)),
];
class FrontPageWidget extends ConsumerWidget {
const FrontPageWidget({Key? key}) : super(key: key);
static const routeName = '/frontPage';
#override
Widget build(BuildContext context, WidgetRef ref) {
//screen height and padding
final height = MediaQuery.of(context).size.height;
final padding = MediaQuery.of(context).viewPadding;
// Height (without status and toolbar)
final cleanHeight = height - padding.top - kToolbarHeight;
//either make those values dynamic (cleanHeight updates depending on screen size / window size) OR constant (961px is the cleanHeight on full screen)
//if values are dynamic => the draggable widgets not scaling to screen size BUT dragging works fine
//if values are constant => the draggable widgets do scale to screen size BUT dragging is messed
final logicWidth = cleanHeight; //961
final logicHeight = cleanHeight; //961
return Scaffold(
appBar: AppBar(
title: const Text('Main Page'),
),
body: SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: Container(
color: Colors.grey,
width: logicWidth,
height: logicHeight,
child: Stack(
children: draggableWidgets,
),
))),
);
}
}
class DraggableWidget extends StatelessWidget {
final Widget draggableWidget;
const DraggableWidget({Key? key, required this.draggableWidget})
: super(key: key);
#override
Widget build(BuildContext context) {
final ValueNotifier<Matrix4> notifier = ValueNotifier(Matrix4.identity());
return Center(
child: MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
},
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: [draggableWidget],
),
),
);
},
),
),
);
}
}
One way of doing it is wrapping the draggableWidget in a Transform widget and set the scale factor in relation to the dimensions:
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
final height = MediaQuery.of(context).size.height;
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: [
Transform.scale(
scale: height / 1000,
child: draggableWidget)
],
),
),
);
},
),
I had a similar issue, instead of getting the height from the MediaQuery get it from the LayoutBuilder, I noticed it is working much better when resizing the window.
body: LayoutBuilder(
builder: (context, constraints) {
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: Container(
color: Colors.grey,
width: constraints.maxWidth,
height: constraints.maxHeight,
child: Stack(
children: draggableWidgets,
),
)
)
);
}
);
Another way of achieving this:
To drag around widgets on the screen (drag and drop) to any location.
Draggable Widget
Check Flutter Draggable class
And to scale screen/window size.
Relative Scale
FlutterScreenUtil

Flutter - Detect when finger enter in a container

In my interface I have a row of containers like this
.
The idea is that when I pass my finger on these containers, the one under my finger gets bigger (and other changes but that's not the point).
I know how to use GestureDetector and get it bigger when I tap on the container with "onTap". But if you keep your finger down and drag it to another container nothing change. Idealy I'd like to be able to detect when the user pass his finger hover a container while touching the screen.
Appreciate if someone can advise. Thank you in advance!
You can use onVerticalDragUpdate on GestureDetector.
class DraUILien extends StatefulWidget {
const DraUILien({super.key});
#override
State<DraUILien> createState() => _DraUILienState();
}
class _DraUILienState extends State<DraUILien> {
int? activeIndex;
final double containerWidth = 30;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onVerticalDragUpdate: (details) {
activeIndex =
details.localPosition.dx ~/ (containerWidth + 16); //16 padding
setState(() {});
},
child: SizedBox(
height: 200,
child: Row(
children: List.generate(
10,
(index) => Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
color: index == activeIndex ? Colors.blue : Colors.grey,
width: containerWidth,
height: index == activeIndex ? 200 : 100,
),
),
),
),
),
)),
);
}
}
Play with the logic for more customization. If you need onTap functionality try including onPanDown

Resizing parent widget to fit child post 'Transform' in Flutter

I'm using Transforms in Flutter to create a scrolling carousel for selecting from various options.
This uses standard elements such as ListView.builder, which all works fine, aside from the fact that the parent widget of the Transform doesn't scale down to fit the content as seen here:
Here's the code used to generate the 'card' (there was actually a Card in there, but I've stripped it out in an attempt to get everything to scale correctly):
return Align(
child: Transform(
alignment: Alignment.center,
transform: mat,
child: Container(
height: 220,
color: color,
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
offset.toString(),
style: TextStyle(color: Colors.white, fontSize: 12.0),
),
),
),
);
}
Even if I remove the 'height' parameter of the Container (so everything scales to fit the 'Text' widget), the boxes containing the Transform widgets still have the gaps around them.
Flutter doesn't seem to have any documentation to show how to re-scale the parent if the object within is transformed - anyone here knows or has any idea of a workaround?
EDIT: The widget returned from this is used within a build widget in a Stateful widget. The stack is Column > Container > ListView.builder.
If I remove the Transform, the Containers fit together as I'd like - it seems that performing a perspective transform on the Container 'shrinks' it's content (in this case, the color - check the linked screen grab), but doesn't re-scale the Container itself, which is what I'm trying to achieve.
I have a tricky solution for this: addPostFrameCallback + overlay.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
// ignore: must_be_immutable
class ChildSizeWidget extends HookWidget {
final Widget Function(BuildContext context, Widget child, Size size) builder;
final Widget child;
final GlobalKey _key = GlobalKey();
OverlayEntry _overlay;
ChildSizeWidget({ this.child, this.builder });
#override
Widget build(BuildContext context) {
final size = useState<Size>(null);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
_overlay = OverlayEntry(
builder: (context) => Opacity(
child: SingleChildScrollView(
child: Container(
child: child,
key: _key,
),
),
opacity: 0.0,
),
);
Overlay.of(context).insert(_overlay);
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
size.value = _key.currentContext.size;
_overlay.remove();
});
});
return () => null;
}, [child]);
if (size == null || size.value == null) {
return child;
} else {
return builder(context, child, size.value);
}
}
}
Usage:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class HomeView extends HookWidget {
#override
Widget build(BuildContext context) {
final change = useState<bool>(false);
final normal = Container(
color: Colors.blueAccent,
height: 200.0,
width: 200.0,
);
final big = Container(
color: Colors.redAccent,
height: 300.0,
width: 200.0,
);
return Column(
children: [
Container(
alignment: Alignment.center,
child: ChildSizeWidget(
child: change.value ? big : normal,
builder: (context, child, size) => AnimatedContainer(
alignment: Alignment.center,
child: SingleChildScrollView(child: child),
duration: Duration(milliseconds: 250),
height: size.height,
),
),
color: Colors.grey,
),
FlatButton(
child: Text('Toggle child'),
onPressed: () => change.value = !change.value,
color: Colors.green,
),
],
);
}
}
I have a menu with several options, they have different height and with the help of the animations this is ok, it's working really nice for me.
Why are you using Align, as much as I can see in your code, there is no property set or used, to align anything. So try removing Align widget around Transform.
Because according to the documentation, Transform is such a widget that tries to be the same size as their children. So that would satisfy your requirement.
For more info check out this documentation: https://flutter.dev/docs/development/ui/layout/box-constraints
I hope it helps!

Flutter - How to flip the previous card back using FlipCard

After days of search I'm getting help.
I work on a flutter application.
Context:
A grid view feeded with Json
-childs : GridTile with Flipcard in (https://pub.dev/packages/flip_card)
-On tap on GridTile there is a callback to get the selected Item and an animation because of the flipcard onTap
What I would:
When an item is aleready selected (flipcard flipped so we show the back of the card),
And I selected another item of the grid te(so flipcard of this itme also flipped)
I would like to flip back the old selected item Flipcard without rebuild the tree because I would lost the state of the new selected item.
I tried many thing. For example I tried to use GlobalKey on GridTiles to interract with after build but currentState is always null when I want to interact with.
I wonder what is the good practice in this case ?
I hope I was clear :) (I'm french)
Thank you the community!
.
Something to know...
It is possible to interract with the flipcard (child of gridtile) like this
(GlobalKey)
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
#override
Widget build(BuildContext context) {
return FlipCard(
key: cardKey,
flipOnTouch: false,
front: Container(
child: RaisedButton(
onPressed: () => cardKey.currentState.toggleCard(),
child: Text('Toggle'),
),
),
back: Container(
child: Text('Back'),
),
);
}
I'm not sure if I understood your question, but here is an example of how you could use a GridView with FlipCards:
var cardKeys = Map<int, GlobalKey<FlipCardState>>();
GlobalKey<FlipCardState> lastFlipped;
Widget _buildFlipCard(String text, Color color, int index) {
return SizedBox(
height: 120.0,
child: Card(
color: color,
child: Center(
child:
Text(text, style: TextStyle(color: Colors.white, fontSize: 20.0)),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FlipCards")),
body: GridView.builder(
itemCount: 20,
itemBuilder: (context, index) {
cardKeys.putIfAbsent(index, () => GlobalKey<FlipCardState>());
GlobalKey<FlipCardState> thisCard = cardKeys[index];
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlipCardWithKeepAlive(
child: FlipCard(
flipOnTouch: false,
key: thisCard,
front: _buildFlipCard("$index", Colors.blue, index),
back: _buildFlipCard("$index", Colors.green, index),
onFlip: () {
if (lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
},
),
),
RaisedButton(
child: Text("Flip Card"),
onPressed: () => cardKeys[index].currentState.toggleCard(),
)
],
);
},
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
),
);
}
class FlipCardWithKeepAlive extends StatefulWidget {
final FlipCard child;
FlipCardWithKeepAlive({Key key, this.child}) : super(key: key);
#override
State<StatefulWidget> createState() => FlipCardWithKeepAliveState();
}
class FlipCardWithKeepAliveState extends State<FlipCardWithKeepAlive>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
#override
bool get wantKeepAlive => true;
}
You need to use a different key for each element of the list, I used a Map in this case.
I also wrapped the FlipCard with a custom FlipCardWithKeepAlive stateful widget that uses AutomaticKeepAliveClientMixin to keep alive the FlipCard while scrolling.
Edit: I updated the code so when you flip one card, the previous card flipped gets flipped back. Basically you need to save the last flipped card and when a new one is flipped, flip the last one and put the new one as last flipped.
The code will make both cards flip at the same time, if you want one card to wait the other use onFlipDone() instead of onFlip(), like this:
onFlipDone: (isFront) {
bool isFlipped = !isFront;
if (isFlipped && lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
}