Search code examples
flutterflutter-layoutflutter-animation

3D Carousel Animation in Flutter


Example gif

How to create similar effect using Flutter?


Solution

  • I made a quick demo for you, tweak the Matrix4 params. (blur/depth of field doesn't work well on browser).

    import 'dart:math';
    import 'dart:ui';
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          darkTheme:
              ThemeData(platform: TargetPlatform.iOS, brightness: Brightness.dark),
          home: RotationScene(),
        );
      }
    }
    
    class RotationScene extends StatefulWidget {
      @override
      _RotationSceneState createState() => _RotationSceneState();
    }
    
    class _RotationSceneState extends State<RotationScene> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(
              'carrousel',
              style: TextStyle(fontSize: 13),
            ),
            centerTitle: false,
            elevation: 12,
            backgroundColor: Colors.transparent,
          ),
          body: Container(
            decoration: BoxDecoration(
                gradient: LinearGradient(
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
              colors: [Color(0xff74ABE4), Color(0xffA892ED)],
              stops: [0, 1],
            )),
            child: Center(child: MyScener()),
          ),
        );
      }
    }
    
    class CardData {
      Color color;
      double x, y, z, angle;
      final int idx;
      double alpha = 0;
    
      Color get lightColor {
        var val = HSVColor.fromColor(color);
        return val.withSaturation(.5).withValue(.8).toColor();
      }
    
      CardData(this.idx) {
        color = Colors.primaries[idx % Colors.primaries.length];
        x = 0;
        y = 0;
        z = 0;
      }
    }
    
    class MyScener extends StatefulWidget {
      @override
      _MyScenerState createState() => _MyScenerState();
    }
    
    class _MyScenerState extends State<MyScener>
        with SingleTickerProviderStateMixin {
      AnimationController _animationController;
    
      List<CardData> cardData = [];
      int numItems = 9;
      double radio = 200.0;
      double radioStep;
      int centerIdx = 1;
    
      @override
      void initState() {
        cardData = List.generate(numItems, (index) => CardData(index)).toList();
        radioStep = (pi * 2) / numItems;
    
        _animationController =
            AnimationController(vsync: this, duration: Duration(seconds: 1));
    
        _animationController.addListener(() => setState(() {}));
        _animationController.addStatusListener((status) async {
          if (status == AnimationStatus.completed) {
            _animationController.value = 0;
            _animationController.animateTo(1);
            ++centerIdx;
          }
        });
        _animationController.forward();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        var ratio = _animationController.value;
        double animValue = centerIdx + ratio;
        // process positions.
        for (var i = 0; i < cardData.length; ++i) {
          var c = cardData[i];
          double ang = c.idx * radioStep + animValue;
          c.angle = ang + pi / 2;
          c.x = cos(ang) * radio;
    //      c.y = sin(ang) * 10;
          c.z = sin(ang) * radio;
        }
    
        // sort in Z axis.
        cardData.sort((a, b) => a.z.compareTo(b.z));
    
        var list = cardData.map((vo) {
          var c = addCard(vo);
          var mt2 = Matrix4.identity();
          mt2.setEntry(3, 2, 0.001);
          mt2.translate(vo.x, vo.y, -vo.z);
          mt2.rotateY(vo.angle + pi);
          c = Transform(
            alignment: Alignment.center,
            origin: Offset(0.0, -0.0),
            transform: mt2,
            child: c,
          );
    
          // depth of field... doesnt work on web.
    //      var blur = .4 + ((1 - vo.z / radio) / 2) * 2;
    //      c = BackdropFilter(
    //        filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
    //        child: c,
    //      );
    
          return c;
        }).toList();
    
        return Container(
          alignment: Alignment.center,
          child: Stack(
            alignment: Alignment.center,
            children: list,
          ),
        );
      }
    
      Widget addCard(CardData vo) {
        var alpha = ((1 - vo.z / radio) / 2) * .6;
        Widget c;
        c = Container(
          margin: EdgeInsets.all(12),
          width: 120,
          height: 80,
          alignment: Alignment.center,
          foregroundDecoration: BoxDecoration(
            borderRadius: BorderRadius.circular(12),
            color: Colors.black.withOpacity(alpha),
          ),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
              stops: [0.1, .9],
              colors: [vo.lightColor, vo.color],
            ),
            borderRadius: BorderRadius.circular(12),
            boxShadow: [
              BoxShadow(
                  color: Colors.black.withOpacity(.2 + alpha * .2),
                  spreadRadius: 1,
                  blurRadius: 12,
                  offset: Offset(0, 2))
            ],
          ),
          child: Text('ITEM ${vo.idx}'),
        );
        return c;
      }
    }