Search code examples
flutterflutter-animation

Flutter - Flip animation - Flip a card over its right or left side based on the tap's location


I've started playing with Flutter and now thinking about the best way how I can implement a card's flipping animation.

I found this flip_card package and I'm trying to adjust its source code to my needs.

Here is the app which I have now:

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

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

class FlipAnimationApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("Flip animation"),
        ),
        body: Center(
          child: Container(
            width: 200,
            height: 200,
            child: WidgetFlipper(
              frontWidget: Container(
                color: Colors.green[200],
                child: Center(
                  child: Text(
                    "FRONT side.",
                  ),
                ),
              ),
              backWidget: Container(
                color: Colors.yellow[200],
                child: Center(
                  child: Text(
                    "BACK side.",
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class WidgetFlipper extends StatefulWidget {
  WidgetFlipper({
    Key key,
    this.frontWidget,
    this.backWidget,
  }) : super(key: key);

  final Widget frontWidget;
  final Widget backWidget;

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

class _WidgetFlipperState extends State<WidgetFlipper> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> _frontRotation;
  Animation<double> _backRotation;
  bool isFrontVisible = true;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(duration: Duration(milliseconds: 500), vsync: this);
    _frontRotation = TweenSequence(
      <TweenSequenceItem<double>>[
        TweenSequenceItem<double>(
          tween: Tween(begin: 0.0, end: pi / 2).chain(CurveTween(curve: Curves.linear)),
          weight: 50.0,
        ),
        TweenSequenceItem<double>(
          tween: ConstantTween<double>(pi / 2),
          weight: 50.0,
        ),
      ],
    ).animate(controller);
    _backRotation = TweenSequence(
      <TweenSequenceItem<double>>[
        TweenSequenceItem<double>(
          tween: ConstantTween<double>(pi / 2),
          weight: 50.0,
        ),
        TweenSequenceItem<double>(
          tween: Tween(begin: -pi / 2, end: 0.0).chain(CurveTween(curve: Curves.linear)),
          weight: 50.0,
        ),
      ],
    ).animate(controller);
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        AnimatedCard(
          animation: _backRotation,
          child: widget.backWidget,
        ),
        AnimatedCard(
          animation: _frontRotation,
          child: widget.frontWidget,
        ),
        _tapDetectionControls(),
      ],
    );
  }

  Widget _tapDetectionControls() {
    return Stack(
      fit: StackFit.expand,
      children: <Widget>[
        GestureDetector(
          onTap: _leftRotation,
          child: FractionallySizedBox(
            widthFactor: 0.5,
            heightFactor: 1.0,
            alignment: Alignment.topLeft,
            child: Container(
              color: Colors.transparent,
            ),
          ),
        ),
        GestureDetector(
          onTap: _rightRotation,
          child: FractionallySizedBox(
            widthFactor: 0.5,
            heightFactor: 1.0,
            alignment: Alignment.topRight,
            child: Container(
              color: Colors.transparent,
            ),
          ),
        ),
      ],
    );
  }

  void _leftRotation() {
    _toggleSide();
  }

  void _rightRotation() {
    _toggleSide();
  }

  void _toggleSide() {
    if (isFrontVisible) {
      controller.forward();
      isFrontVisible = false;
    } else {
      controller.reverse();
      isFrontVisible = true;
    }
  }
}

class AnimatedCard extends StatelessWidget {
  AnimatedCard({
    this.child,
    this.animation,
  });

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        var transform = Matrix4.identity();
        transform.setEntry(3, 2, 0.001);
        transform.rotateY(animation.value);
        return Transform(
          transform: transform,
          alignment: Alignment.center,
          child: child,
        );
      },
      child: child,
    );
  }
}

Here is how it looks like:

What I'd like to achieve is to make the card flip over its right side if it was tapped on its right half and over its left side if it was tapped on its left half. If it is tapped several times in a row on the same half it should flip over the same side (not back and forth as it is doing now).

So the desired animation should behave as the following one from Quizlet app.


Solution

  • You should know when you tap on the right or left to change the animations dynamically, for that you could use a flag isRightTap. Then, you should invert the values of the Tweens if it has to rotate to one side or to the other.

    And the side you should rotate would be:

    • Rotate to left if the front is visible and you tapped on the left, or, because the back animation is reversed, if the back is is visible and you tapped on the right
    • Otherwise, rotate to right

    Here are the things I changed in _WidgetFlipperState from the code in the question:

    _updateRotations(bool isRightTap) {
      setState(() {
        bool rotateToLeft = (isFrontVisible && !isRightTap) || !isFrontVisible && isRightTap;
        _frontRotation = TweenSequence(
          <TweenSequenceItem<double>>[
            TweenSequenceItem<double>(
              tween: Tween(begin: 0.0, end: rotateToLeft ? (pi / 2) : (-pi / 2))
                  .chain(CurveTween(curve: Curves.linear)),
              weight: 50.0,
            ),
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(rotateToLeft ? (-pi / 2) : (pi / 2)),
              weight: 50.0,
            ),
          ],
        ).animate(controller);
        _backRotation = TweenSequence(
          <TweenSequenceItem<double>>[
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(rotateToLeft ? (pi / 2) : (-pi / 2)),
              weight: 50.0,
            ),
            TweenSequenceItem<double>(
              tween: Tween(begin: rotateToLeft ? (-pi / 2) : (pi / 2), end: 0.0)
                  .chain(CurveTween(curve: Curves.linear)),
              weight: 50.0,
            ),
          ],
        ).animate(controller);
      });
    }
    
    @override
    void initState() {
      super.initState();
      controller =
          AnimationController(duration: Duration(milliseconds: 500), vsync: this);
      _updateRotations(true);
    }
    
    void _leftRotation() {
      _toggleSide(false);
    }
    
    void _rightRotation() {
      _toggleSide(true);
    }
    
    void _toggleSide(bool isRightTap) {
      _updateRotations(isRightTap);
      if (isFrontVisible) {
        controller.forward();
        isFrontVisible = false;
      } else {
        controller.reverse();
        isFrontVisible = true;
      }
    }