1

I'm developing a Flutter app where I need a scroll card screen with a smooth animation. I tried using the multiple card package, but the animation isn't matching my reference video. Specifically, the card rotation, scaling, and stacking effects aren't as fluid and natural as in the video.

enter image description here

5
  • can you share your reference video. Commented Mar 26 at 10:33
  • @MunsifAli cdn.dribbble.com/userupload/12055746/file/… Commented Mar 26 at 10:39
  • For the card effect you need to use either a Transform or a MatrixTransition. Commented Mar 27 at 20:44
  • @SrPanda Can you provide me any example code for better understanding? Commented Apr 3 at 5:43
  • @MohammadFaizan The example is not quite as in the video but it should be close enough. If you want to use a Transform you just need to use the alignment and the matrix with the rotation. Commented Apr 3 at 17:52

1 Answer 1

0

This example is just to illustrates the card effect and a way of animating it. There are multiple ways to apply transforms to a widget, the ideal one will depend on how may of those effects you want to mimic. The main reason reason I'm using Flow is because using it yields a shorter example.

The matrix transform just scales, shifts and rotates (axis and perspective) the cards. The list of widgets will be painted in order regardless of what the delegate does, i.e, the list index is the "z-index". I don't use transforms that often so my knowledge is quite basic. If you want a simpler version, check out this article.

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: FlowCardStack(
          children: List.generate(5, (i) => MyCard(index: i)), //
        ),
      ),
    );
  }
}

class MyCard extends StatelessWidget {
  const MyCard({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onVerticalDragDown: (_) => print('Card drag $index'),
      // This gesture will win the arena as expected, the pointers will try
      // to hit the correct shape (the one you see in the screen)
      onVerticalDragUpdate: (details) {
        FlowCardStackNotification(details.delta.dy, index).dispatch(context);
      },
      child: Container(
        height: 200,
        width: double.infinity,
        padding: EdgeInsets.symmetric(horizontal: 25),
        decoration: BoxDecoration(
          boxShadow: kElevationToShadow[index],
          borderRadius: BorderRadius.all(Radius.circular(10)),
          color: index.isEven ? Colors.blue : Colors.blueGrey,
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Text('Card $index'),
            CircularProgressIndicator(), //
          ],
        ),
      ),
    );
  }
}

// Bare-bone basic notification
class FlowCardStackNotification extends Notification {
  const FlowCardStackNotification(this.offset, [this.identifier]);

  final double offset;
  final Object? identifier;
}

class FlowCardStack extends StatefulWidget {
  const FlowCardStack({super.key, required this.children});

  final List<Widget> children;

  @override
  FlowCardStackState createState() => FlowCardStackState();
}

class FlowCardStackState extends State<FlowCardStack> with SingleTickerProviderStateMixin {
  final notifier = ValueNotifier<double>(30);

  @override
  Widget build(BuildContext context) {
    return NotificationListener<FlowCardStackNotification>(
      onNotification: (notification) {
        notifier.value = clampDouble(
          // n - 1 scale down to match the last card, not the second one
          notifier.value + (notification.offset / widget.children.length),
          10,
          200, //
        );
        return true;
      },
      child: Flow(
        delegate: FlowCardStackDelegate(notifier: notifier),
        children: widget.children, //
      ),
    );
  }
}

class FlowCardStackDelegate extends FlowDelegate {
  FlowCardStackDelegate({required this.notifier}) : super(repaint: notifier);

  final ValueNotifier<double> notifier;

  @override
  bool shouldRepaint(FlowCardStackDelegate old) {
    return notifier != old.notifier;
  }

  // These values can be dynamic if a fancy-er animation is needed

  final double scale_step = 0.05;
  final double perspective_y_rotation = 0.001;

  @override
  void paintChildren(FlowPaintingContext context) {
    final count = context.childCount;

    for (int i = 0; i < count; ++i) {
      final size = context.getChildSize(i)!;
      final translation = FractionalOffset.center.alongSize(size);

      context.paintChild(
        i,
        transform:
            Matrix4.identity()
              ..setTranslationRaw(
                (context.size.width - size.width) / 2,
                notifier.value * i,
                0, //
              )
              ..translate(translation.dx, translation.dy)
              ..multiply(
                Matrix4.zero()
                  ..setIdentity()
                  ..rotateX((pi / 180) * 10 * (count - i)) // +10 degrees per card
                  ..setEntry(
                    3,
                    1,
                    // Incremental shift towards 0, the first value is
                    // one step less than the base value.
                    // If the +1 is removed the last card will have
                    // on step of rotation.
                    (perspective_y_rotation / count) * (count - (i + 1)),
                  )
                  ..scale(
                    clampDouble(1 - scale_step * (count - i), 0, 1),
                    clampDouble(1 - scale_step * (count - i), 0, 1), //
                  ),
              )
              ..translate(-translation.dx, -translation.dy),
      );
    }
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.