Flutter - 翻转动画 - 根据点击的位置将卡片翻转到右侧或左侧

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

我已经开始玩 Flutter,现在正在考虑实现卡片翻转动画的最佳方式。

我发现这个 flip_card package and I'm trying to adjust its source code 符合我的需要。

这是我现在拥有的应用程序:

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

这是它的样子:

我想要实现的是 如果点击卡片的右半边,卡片会翻转到右侧,如果点击左半边,卡片会翻转到左侧。如果在同一半边连续点击几次,它应该翻转到同一侧(而不是像现在这样来回翻转)。

因此,所需动画的行为应与 Quizlet 应用中的以下动画相同。

试试这个代码 我对你的代码做了一些修改,现在 GestureDetector 在小部件上的宽度是平均分配的,所以当你点击框的左侧时,它会反转动画,如果你点击右侧部分它会转发动画。

 Widget _tapDetectionControls() {
    return Flex(
      direction: Axis.horizontal,
      children: <Widget>[
        Expanded(
          flex: 1,
          child: GestureDetector(
            onTap: _leftRotation,
          ),
        ),
        Expanded(
          flex: 1,
          child: GestureDetector(
            onTap: _rightRotation,
          ),
        ),
      ],
    );
  }

  void _leftRotation() {
    controller.reverse();
  }

  void _rightRotation() {
    controller.forward();
  }

您应该知道何时点击右侧或左侧以动态更改动画,为此您可以使用标志 isRightTap。然后,如果 Tweens 必须旋转到一侧或另一侧,则应反转它的值。

你应该旋转的一侧是:

  • 如果正面可见并且您点击了左侧,则向左旋转,或者,因为背面动画是反转的,如果背面可见并且您点击了右侧
  • 否则向右旋转

以下是我在 _WidgetFlipperState 中根据问题中的代码所做的更改:

_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;
  }
}

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage();

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

class _MyHomePageState extends State<MyHomePage> {
  bool _toggler = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(actions: [
        TextButton(
          onPressed: _onFlipCardPressed,
          child: const Text('change', style: TextStyle(color: Colors.white)),
        )
      ]),
      body: Center(
        child: SizedBox.square(
          dimension: 140,
          child: FlipCard(
            toggler: _toggler,
            frontCard: AppCard(title: 'Front'),
            backCard: AppCard(title: 'Back'),
          ),
        ),
      ),
    );
  }

  void _onFlipCardPressed() {
    setState(() {
      _toggler = !_toggler;
    });
  }
}

class AppCard extends StatelessWidget {
  final String title;

  const AppCard({
    required this.title,
  });

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20.0),
        color: Colors.deepPurple[400],
      ),
      child: Center(
        child: Text(
          title,
          style: const TextStyle(
            fontSize: 40.0,
            color: Colors.white,
          ),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

class FlipCard extends StatelessWidget {
  final bool toggler;
  final Widget frontCard;
  final Widget backCard;

  const FlipCard({
    required this.toggler,
    required this.backCard,
    required this.frontCard,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: AnimatedSwitcher(
        duration: const Duration(milliseconds: 800),
        transitionBuilder: _transitionBuilder,
        layoutBuilder: (widget, list) => Stack(children: [widget!, ...list]),
        switchInCurve: Curves.ease,
        switchOutCurve: Curves.ease.flipped,
        child: toggler
            ? SizedBox(key: const ValueKey('front'), child: frontCard)
            : SizedBox(key: const ValueKey('back'), child: backCard),
      ),
    );
  }

  Widget _transitionBuilder(Widget widget, Animation<double> animation) {
    final rotateAnimation = Tween(begin: pi, end: 0.0).animate(animation);
    return AnimatedBuilder(
      animation: rotateAnimation,
      child: widget,
      builder: (context, widget) {
        final isFront = ValueKey(toggler) == widget!.key;
        final rotationY = isFront ? rotateAnimation.value : min(rotateAnimation.value, pi * 0.5);
        return Transform(
          transform: Matrix4.rotationY(rotationY)..setEntry(3, 0, 0),
          alignment: Alignment.center,
          child: widget,
        );
      },
    );
  }
}