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;
}
}
Related
Do you guys know how can I update a single item of a sliver list without having to invoke a setState() ?
In my case I have a SliverList and i want to click into an item and change it's color, the problem using setState() is that it rebuilds the whole UI in a not smooth way and also mess up with Custom Scroll position.
The funny thing is that this SliverList behaviour does not occur when using normal ListView, when use setState() on a ListView the load is smooth and it doesn't break the scroll state. Looks like the ListView can implicitely handle state better than SliverList.
But since I have a Custom Scroll I can't use ListVew it has to be SliverList
Any options ? Providers ?Notifiers ? Stream ? Bloc ?
Ok, after all, I could solve my need using a simple ChangeNotifier combined with an AnimatedBuilder inside each item of the SliverList, I'll post some high-level code of the solution - it works just fine for my need!
class ChangeColorSliverListItemNotifier extends ChangeNotifier {
int index;
Color current_label_color;
ChangeColorSliverListItemNotifier()
{
this.current_label_color = Colors.white;
}
void onTap(int selected_index)
{
index = selected_index;
this.current_label_color = Colors.yellow;
notifyListeners();
}
}
// code block inside SliverList items binding- changing only the color of the selected Widget
SliverList(
delegate: SliverChildBuilderDelegate((context, index)
{
return GestureDetector(
onTap:() {
changeColorSliverListItemNotifier.onTap(index);
},
child:AnimatedBuilder(
animation: changeColorSliverListItemNotifier,
builder: (_, __) =>
Row(children:[
Container(
margin: EdgeInsets.only(right:8),
child: Icon(Icons.edit,color:changeColorSliverListItemNotifier.index==index?changeColorSliverListItemNotifier.current_label_color:default_color,size: 20,)
),
])
)
)
....
Just in case you want to use the Riverpod state management library, I made the following.
class SliverScreen extends StatelessWidget {
const SliverScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
_appBar(),
_list(),
],
),
);
}
SliverFixedExtentList _list() {
return SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListItem(index: index),
),
itemExtent: 100,
);
}
SliverAppBar _appBar() {
return SliverAppBar(
title: Text("Slivering..."),
backgroundColor: Colors.teal[900],
expandedHeight: 200,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.teal[100]!,
Colors.teal[600]!,
],
),
),
),
),
);
}
}
final colorStateProvider = StateProvider.family<Color, int>((ref, key) {
return Colors.blue[100]!;
});
class ListItem extends HookConsumerWidget {
final int index;
const ListItem({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final colorState = ref.watch(colorStateProvider(index));
return InkWell(
onTap: () => colorState.state = Colors.blue,
child: Container(
color: colorState.state,
child: Center(
child: Text("Item $index"),
),
),
);
}
}
If you click an item, it updates the color without rebuilding the whole list, only the item itself.
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()
I need help to do the following: when I press List 1, the screen focuses on List 1; I need the same for the rest of the options
This is the code for the example:
code
This behavior already exists in web pages but I haven't found this same behavior at the mobile app level. Thank you
Here is a small code snippet of something similar which might help you achieve you desired results.
By clicking the fab icon it will scroll down to item 35 within the ListView.
class MyHomePage extends StatelessWidget {
final _scrollController = ScrollController();
final _cardHeight = 200.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.orange,
onPressed: () => _animateToIndex(35),
child: Icon(Icons.add),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 100,
itemBuilder: (_, i) => Container(
height: _cardHeight,
child: Card(
color: Colors.lightBlue,
child: Center(
child: Text("Scroll Item $i", style: TextStyle(fontSize: 28.0),),
),
),
),
),
);
}
_animateToIndex(index) {
_scrollController.animateTo(_cardHeight * index,
duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
}
}
You'll need to have a scrollable Widget (like ListView, SingleScrollableWidget) instead of a Column in ListSecondPage.
Then add a ScrollController to it and ListSecondPage should receive which button was tapped. Based on that selection you can scroll to the desired location with the ScrollController
I want refresh my page without having a scrollable content, i.e. without having a ListView et al.
When I want use RefreshIndicator, the documentation says it needs a scrollable widget like ListView.
But if I want to refresh and want to use the refresh animation of RefreshIndicator without using a ListView, GridView or any other scorllable widget, how can i do that?
You can simply wrap your content in a SingleChildScrollView, which will allow you to use a RefreshIndicator. In order to make the pull down to refresh interaction work, you will have to use AlwaysScrollableScrollPhysics as your content will most likely not cover more space than available without a scroll view:
RefreshIndicator(
onRefresh: () async {
// Handle refresh.
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: /* your content */,
),
);
You can just use GestureDetector, I have created a sample for you, but it's not perfect, you can customize it to your own needs, it just detects when you swipe from the top.
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
var refresh=false;
void refreshData(){
if(!refresh){
refresh=true;
print("Refreshing");
Future.delayed(Duration(seconds: 4),(){
refresh =false;
print("Refreshed");
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Test"),
centerTitle: true,
),
body: GestureDetector(
child: Container(
color: Colors.yellow,
height: double.infinity,
width: double.infinity,
child: Center(child: Text('TURN LIGHTS ON')),
),
onVerticalDragUpdate: (DragUpdateDetails details){
print("direction ${details.globalPosition.direction}");
print("distance ${details.globalPosition.distance}");
print("dy ${details.globalPosition.dy}");
if(details.globalPosition.direction < 1 && (details.globalPosition.dy >200 && details.globalPosition.dy < 250)){
refreshData();
}
},
));
}
}
Good Day,
I am making great progress on my first app using flutter, some differences for sure, but found great help here. I have successfully produced a dynamic listview from JSON api call, I am trying to take that and convert it to a 2 column potrait gridview and 3 landscape. I have looked through the flutter gallery demo and the docs and can not seem to get a handle on the flow.
Anyone have some other examples or guidance to accomplish.
What I currently have is a ListView.builder calling an itembuilder that returns widgets of a leading icon, text and trailing icon. I want to convert it using the ICON to be the grid of graphics with onTap to another page as the listview does.
Any help or guidance would be great, thought it should be a simple conversion, but it has not been so far. I will attach some of the code below.
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new RefreshIndicator(
child: new ListView.builder(
itemBuilder: _itemBuilder,
itemCount: listcount,
),
onRefresh: _onRefresh,
));
}
Widget _itemBuilder(BuildContext context, int index) {
Specialties spec = getSpec(index);
return new SpecialtyWidget(spec: spec,);
}
Specialties getSpec(int index) {
return new Specialties(
mylist[index]['id'], mylist[index]['name'], mylist[index]['details'],
new Photo(mylist[index]['image'], mylist[index]['name'],
mylist[index]['name']));
}
}
class SpecialtyWidget extends StatefulWidget {
SpecialtyWidget({Key key, this.spec}) : super(key: key);
final Specialties spec;
#override
_SpecialtyWidgetState createState() => new _SpecialtyWidgetState();
}
class _SpecialtyWidgetState extends State<SpecialtyWidget> {
#override
Widget build(BuildContext context) {
return new Container(
height: 64.0,
width: 128.0,
child: new ListTile(
trailing: new Icon(Icons.arrow_right, color: Colors.green, size: 50.0,),
leading: new Image.network('http://$baseurl:8080/getimage/'+widget.spec.pic.assetName, fit: BoxFit.cover,),
title: new Text(
widget.spec.name,
style: new TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold),
),
onTap: _onTap,
),
);
Thanks
There's an example of GridView.count usage in the Flutter Gallery. You could use a LayoutBuilder or MediaQuery to determine whether the grid is portrait or landscape, and then choose a crossAxisCount count of 2 or 3 depending on what answer you get.