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.
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);
}