flutterflutter-animationframer-motion

Flutter Animate Widget Transition


Need help with simulating transition of widgets from one location to another. The new location is the location of another widget.

If anyone is familiar with Framer Motion's layoutid, it's similar to that but I can't find anything flutter. For those who are not familiar with it, layoutId is like a key on a widget that detects old and new location of the widget; when the widget location changes in the widget tree Framer Motion will moves the widget to its new location where the layoutId is.

I have looked into Hero Animation which uses tags but it's for transition between routes. Position in flutter requires us to specify the location. Can anyone guide me in the right direction?

If the question is not clear enough feel free to ask questions!

Framer Motion code using layoutId

interface CardProp {
  card: TCard;
  isFlipped: boolean;
  playCard?: (c: TCard, p: Player) => void;
  player?: Player;
}

export default function Card({
  card,
  isFlipped,
  playCard,
  player,
}: CardProp) {
  const isFace = !isFlipped && card !== undefined;
  const src = isFace ? createCardSVGPath(card!) : CARD_BACK_SVG_PATH;

  return (
    <Box
      data-testid={`card-${card}-div`}
      id={player == null ? "" : `player${player}-card${card}`}
      margin={{base: "1%"}}
    >
      <motion.img
        data-testid={`card-${card}`}
        initial={{ x: 0, y: 0, opacity: 0, scale: 0.5 }}
        animate={{ opacity: 1, scale: 1 }}
        onClick={() => {
          if (player != null && playCard && !isFlipped) playCard(card, player);
        }}
        src={src}
        layoutId={card?.toString()} // Framer motion use layoutId to animate the image transition whne Card is removed from one component to another component
        className="size-40 inline"
      />
    </Box>
  );
}

Current Flutter card Widget

class Card extends StatelessWidget {
  final int cardNumber;
  final double padding;
  final bool isFlipped;

  const Card({
    super.key,
    required this.cardNumber,
    required this.padding,
    required this.isFlipped,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: GestureDetector(
        onTap: () {
          // transition the widget to new location
        },
        child: Padding(
          padding: EdgeInsets.all(padding),
          child: isFlipped
              ? FittedBox(
                  fit: BoxFit.contain,
                  child: Image(image: AssetImage(cardBackSVGPath)),
                )
              : FittedBox(
                  fit: BoxFit.contain,
                  child: Image(
                    image: AssetImage(createCardSVGPath(cardNumber)),
                  ),
                ),
        ),
      ),
    );
  }
}

Edit:

This is the page I have now picture of the page. I want the cards in the bottom to move into one of the two center piles. Upon clicking the card in the bottom, the server will check if the action is permitted and send back new game state. The new game state will have the the bottom cards and the center piles updated.


Solution

  • You might be looking for a combination of GlobalKey, RenderBox, AnimationController, and Tween

    This demo illustrates the usage of those classes.

    When a user card and center card are selected, those cards are given GlobalKey and their RenderBox is used to find global positions. The AnimationController is then provided with a Tween.

      void _startAnimation() {
        // Determine the key of the selected user card
        GlobalKey selectedCardKey = _isTopPlayerSelected
            ? _topPlayerCardKeys[_selectedUserCardIndex!]
            : _bottomPlayerCardKeys[_selectedUserCardIndex!];
    
        // Determine the key of the selected center pile
        GlobalKey targetPileKey = _centerPileKeys[_selectedCenterPileIndex!];
    
        // Get the RenderBox of the selected user card
        final RenderBox userCardBox =
            selectedCardKey.currentContext?.findRenderObject() as RenderBox;
        _startOffset = userCardBox.localToGlobal(Offset.zero);
    
        // Get the RenderBox of the target center pile
        final RenderBox targetPileBox =
            targetPileKey.currentContext?.findRenderObject() as RenderBox;
        _endOffset = targetPileBox.localToGlobal(Offset.zero);
    
        setState(() {
          // Store the asset of the card that will be animated
          _animatingCardAsset = _isTopPlayerSelected
              ? _topPlayerCards[_selectedUserCardIndex!]
              : _bottomPlayerCards[_selectedUserCardIndex!];
        });
    
        _animation = Tween<Offset>(begin: _startOffset, end: _endOffset).animate(
          CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
        );
    
        _animationController.forward(from: 0.0);
      }