The App Store card transition in Flutter: tap a card in the list and it becomes the screen. Drag down and it goes back to its slot, pixel perfect. One shared element, one progress value, zero bounce.
Ported from the React Native original in rs-4/labs.
- Measure the card. On tap,
RenderBox.localToGlobalgives the card's rect in the container's coordinate space. That rect is the whole shared-element contract: the clone starts there and must land back there. - Mount a clone, interpolate the rect. The overlay renders the same
CardFacewidget, and a singleAnimationControllerinterpolates its rect from the slot to fullscreen. The corner radius travels from 28 to 55, so fullscreen still hugs the phone corners. - One progress drives everything. A calm curve,
Cubic(0.32, 0.72, 0, 1), no spring. The body fade, the label safe-area offset, the close button and the background list zoom all read the same value, so nothing can desync and the close lands exactly on the origin card. - Drag to dismiss. A vertical drag translates and shrinks the sheet. Past 140px or a 900px/s flick it releases into the close animation, reversed on the same curve. Under the threshold it settles back to fullscreen.
CardExpand(
topInset: MediaQuery.paddingOf(context).top + 110,
cards: const [
ExpandCard(
id: 'focus',
label: 'Featured',
title: 'Deep Focus',
subtitle: 'One line under the title.',
body: 'The long text revealed once the card is fullscreen.',
colors: [Color(0xFF3B2F2A), Color(0xFF8A5A3B)],
),
],
)Place CardExpand in a full-screen Stack so the overlay can cover the header.
kOpenDuration(440ms) andkCloseDuration(380ms), same cubic.kCardRadius(28) andkFullRadius(55).- Dismiss thresholds in
_onDragEnd: 140px distance or 900px/s velocity. - The list zoom (
scale 0.95,opacity 0.65) inCardExpand.build.
flutter run