flutterdartflutter-layoutflutter-gridview

why do widgets in GridView act as one (in terms of animation)


Hy,

I'm new to dart and flutter thus why you will find my code messy (i don't really know when to use classes vs widgets).

I'm trying to generate clickable cards with images from assets that will turn around when clicked- the problem is they act as one, they all turn when one is clicked. And when i click on the second time nothing happens.

Here it shows what it does

import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'assetsLists.dart';
import 'dart:math';

// AudioPlayer player = AudioPlayer();

void main() => runApp(const MaterialApp(
      home: Home(),
    ));

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {

  AudioPlayer player = AudioPlayer();
  bool isFront = true;
  double angle = 0;

  void _flip() {
    setState(() {
      angle = (angle + pi) % (2 * pi);
    });
    if(isFront) {
      isFront = false;
    }
    else {
      isFront = true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("tap card to play a sound"),
        centerTitle: true,
        elevation: 5.0,
        backgroundColor: Colors.green,
      ),
      body: Padding(
          padding: const EdgeInsets.fromLTRB(10, 12, 10, 12),
          child: cardGridBuilder()),
    );
  }

  Widget cardGridBuilder() => GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
        padding: const EdgeInsets.fromLTRB(5, 5, 5, 5),
        itemCount: cardAssets.length,
        itemBuilder: (context, index) {
          return cardBuilder(index);
          
          },
  );

  Widget cardBuilder(int number) {
    return Container(
      child: GestureDetector(
        onTap: _flip,
        child: Center(
          child: TweenAnimationBuilder(
                  tween: Tween<double>(begin: 0, end: angle),
                  duration: Duration(milliseconds: 500),
                  builder: (BuildContext context, double val, __) {
                    String pictureAsset = cardAssets[number][0];
                    String soundAsset = cardAssets[number][1];
                    return (Transform(
                      transform: Matrix4.identity()
                        ..setEntry(3, 2, 0.001)
                        ..rotateY(val),
                      child: Container(
                        width: 200,
                        height: 350,
                        child: isFront 
                        ? Container(
                            decoration: BoxDecoration(
                              image: DecorationImage(
                                image: AssetImage('assets/Images/$pictureAsset')
                              )
                            )
                          )
                        : Container(
                            decoration: BoxDecoration(
                              image: DecorationImage(
                                image: AssetImage('assets/Images/$pictureAsset')
                              )
                            )
                          )
      
                          // String audioasset = 'assets/Sounds/$soundAsset';
                          // ByteData bytes = await rootBundle.load(audioasset); //load sound from assets
                          // Uint8List soundbytes = bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes);
                          // int result = await player.playBytes(soundbytes);
                          
                      )
                      )
                    );
                  }
                ),
        ),
      )
    );
  }
}

For now i have only a few samples but on the end there will be 104:

List<List<String>> cardAssets = [
  ['IceWizardCard.webp','IceWizardSound.mp3','6'],
  ['MegaKnightCard.webp','MegaKnightSound.mp3','6'],
  ['WizardCard.webp','WizardSound.mp3', '4'],
];

I am aware of the mapping function but i went with gridview builder since i have an array of arrays with assets addresses saved in it. Also don't mind the commented code, it's used for playing the specific audio on specific card.

I thank you all!


Solution

  • To specifically animate only a tapped grid cell you can use the index parameter provided by the GridView.builder.

    For instance, when the user taps a given cell, the index of this cell can be saved in a local variable and then trigger the state to update. And use this value as a condition against the index provided by the builder.

    The animation controller will then only run when this condition is true (the example below provides more clarity).

    Also: **I have updated to separate the animation logic into a separate class, which I recommend to declutter the widget.

    **I have used setState(){} method for the state management in this example, however, I recommend you research other options (there are a lot of good packages). One popular solution is Provider

    **Lastly, to your first point - generally speaking widgets in Flutter are just classes.

    I hope this helps you! Happy Fluttering.

    import 'dart:math';
    import 'package:flutter/material.dart';
    
    class CardTurnExample extends StatefulWidget {
      const CardTurnExample({Key? key}) : super(key: key);
    
      @override
      _CardTurnExampleState createState() => _CardTurnExampleState();
    }
    
    class _CardTurnExampleState extends State<CardTurnExample> {
      /// [_index] temporarily holds the index that is tapped (set by [GestureDetector]).
      int? _index;
    
      @override
      Widget build(BuildContext context) {
        return Material(
          child: GridView.builder(
              gridDelegate:
                  SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
              itemBuilder: (context, index) {
                return GestureDetector(
                  onTap: () {
                    _index = index;
    
                    /// Important to call setState(){} here
    
                    setState(() {});
                  },
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: TurnAnimation(
                      runAnimation: _index == index,
                      child: Container(
                        color: Colors.red,
                        child: Center(child: Text(index.toString())),
                      ),
                    ),
                  ),
                );
              }),
        );
      }
    }
    
    /// Animation is separated into its own class for clarity.
    /// It also means this logic can also be reused easily in your app.
    class TurnAnimation extends StatefulWidget {
      const TurnAnimation(
          {required this.child, required this.runAnimation, Key? key})
          : super(key: key);
    
      final Widget child;
      final bool runAnimation;
    
      @override
      _TurnAnimationState createState() => _TurnAnimationState();
    }
    
    class _TurnAnimationState extends State<TurnAnimation>
        with SingleTickerProviderStateMixin {
      late final AnimationController _controller;
    
      @override
      void initState() {
        _controller =
            AnimationController(duration: Duration(milliseconds: 500), vsync: this);
        super.initState();
      }
    
      /// Important to set the [Controller.forward] in it's own local method, so it only runs
      /// when the passed in boolean is true
      _runAnimation() {
        if (widget.runAnimation) {
          _controller.forward();
        }
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        _runAnimation();
        return AnimatedBuilder(
          animation: _controller,
          builder: (_, child) => Transform(
            alignment: Alignment.center,
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.001)
              ..rotateY(_controller.value * pi),
            child: widget.child,
          ),
        );
      }
    }
    
    
    

    enter image description here

    Example