Flutter 动画 - 不使用预定义包的卡片翻转

问题描述 投票:0回答:2

我试图在不使用依赖项的情况下从前到后和从后到前翻转卡片,但无法实现。我见过人们使用预定义的翻转卡包但没有依赖性我遇到了麻烦。请帮帮我。

我希望卡在单击“图标按钮”时立即翻转到背面,并在单击“返回”按钮时立即返回。我在不使用动画的情况下尝试了这个想法,并且工作得很好,但是如何实现翻转动画是我觉得困难的地方。

class NotificationItemCard extends StatefulWidget {
  const NotificationItemCard({
    Key? key,
  }) : super(key: key);

  @override
  State<NotificationItemCard> createState() => _NotificationItemCardState();
}

class _NotificationItemCardState extends State<NotificationItemCard> {
  late bool showCardFrontSide;
  @override
  void initState() {
    showCardFrontSide = true;
    super.initState();
  }

  void onChangeView() {
    setState(() {
      showCardFrontSide = !showCardFrontSide;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Container(
          height: 140.h,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.black),
            borderRadius: BorderRadius.circular(8),
          ),
          child: showCardFrontSide
              ? const NotificationCardFrontSide()
              : NotificationCardBackSide(
                  onChangeView: onChangeView,
                ),
        ),
        showCardFrontSide
            ? Align(
                alignment: const Alignment(0.95, -1),
                child: IconButton(
                  key: const ValueKey("IconButton"),
                  onPressed: onChangeView,
                  icon: const Icon(Icons.info_outline),
                ),
              )
            : const SizedBox.shrink()
      ],
    );
  }
}

class NotificationCardFrontSide extends StatelessWidget {
  const NotificationCardFrontSide({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(
          key: const ValueKey("FrontSideSizedBox"),
          width: 126.w,
          child: Center(
            child: CircleAvatar(
              radius: 50.r,
            ),
          ),
        ),
        SizedBox(
          key: const ValueKey("FrontSideSizedTextBox"),
          width: 222.w,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                "Premium Private LOBBY",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox1"),
              ),
              Text(
                "Prediction Deadline",
                // "Prediction Deadline - ${DateConverterUtil.convert(lobby.match.start)}",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox2"),
              ),
              Text(
                "Premium Private LOBBY",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox3"),
              ),
              SizedBox(
                key: const ValueKey("FrontSideSizedButtonBox"),
                width: 150.w,
                height: 45.h,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    SizedBox(
                      key: const ValueKey("FrontSideButtonSizedBox"),
                      width: 70.w,
                      child: TextButton(
                        onPressed: () {},
                        child: Text(
                          "deny",
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 70.w,
                      child: TextButton(
                        onPressed: () {},
                        child: Text(
                          "deny",
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class NotificationCardBackSide extends StatelessWidget {
  final VoidCallback onChangeView;

  const NotificationCardBackSide({
    Key? key,
    required this.onChangeView,
  }) : super(key: key);

  Widget getTeamLogo(String image) {
    return CircleAvatar(
      backgroundColor: const Color(0xFFD9D9D9),
      radius: 30.r,
      child: Image.network(
        image,
        errorBuilder: (context, error, stackTrace) {
          return Text(
            "Error",
            style: Theme.of(context).textTheme.displayMedium?.copyWith(
                  color: Colors.red,
                ),
          );
        },
        height: 65.h,
        width: 65.w,
        loadingBuilder: (context, child, loadingProgress) {
          if (loadingProgress == null) return child;
          return Center(
            child: Text(
              "Loading...",
              style: Theme.of(context).textTheme.displayMedium,
            ),
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        SizedBox(
          height: 62.h,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              getTeamLogo(""),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    "Premium Private LOBBY",
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip),
                    key: const ValueKey("BackSideSizedText1"),
                  ),
                  Text(
                    "Prediction Deadline",
                    // "Prediction Deadline - ${DateConverterUtil.convert(lobby.match.start)}",
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip),
                    key: const ValueKey("BackSideSizedText2"),
                  ),
                ],
              ),
              getTeamLogo(""),
            ],
          ),
        ),
        SizedBox(
          key: const ValueKey("BackSideButtonBox"),
          height: 30.h,
          width: 100.w,
          child: OutlinedButton(
            onPressed: onChangeView,
            child: const Text("Go Back"),
            key: const ValueKey("BackSideButtonText"),
            style: ButtonStyle(
              shape: MaterialStateProperty.all(
                RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(
                    8.r,
                  ),
                ),
              ),
            ),
          ),
        )
      ],
    );
  }
}
flutter flutter-layout flutter-animation
2个回答
2
投票

您可以使用

AnimatedBuilder
Transform
来实现此功能,使用下面的示例:

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

void main() {
  runApp(MaterialApp(debugShowCheckedModeBanner: false, home: Scaffold(body: MyApp())));
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  Widget _front = Card1();
  Widget _back = Card2();
  late Widget _card = _front;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 600));
    _controller.addListener(() {
      if (_controller.value >= .5 && _card != _back) {
        setState(() => _card = _back);
      } else if (_controller.value < .5 && _card != _front) {
        setState(() => _card = _front);
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () {
          if (_controller.value == 1)
            _controller.reverse(from: 1);
          else
            _controller.forward(from: 0);
        },
        child: AnimatedBuilder(
          animation: _controller,
          builder: (c, anim) => Transform(
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.0025)
              ..rotateY(_controller.value * pi),
            alignment: FractionalOffset.center,
            child: _card,
          ),
        ),
      ),
    );
  }
}

class Card1 extends StatelessWidget {
  const Card1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 150,
      height: 300,
      color: Colors.red,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('This is Card1'),
          Text('I\'m front of the card'),
        ],
      ),
    );
  }
}

class Card2 extends StatelessWidget {
  const Card2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Transform.scale(
      scaleX: -1,
      child: Container(
        width: 150,
        height: 300,
        color: Colors.blue,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('This is Card2'),
            Text('I\'m back of the card'),
          ],
        ),
      ),
    );
  }
}

结果:

(感谢这个答案的转换代码)


1
投票

感谢amir_a14的回答。我修改了你的代码,使卡片可以垂直翻转而不是水平翻转。我还需要支持通过任意数量的边而不是仅两个边进行循环。我希望能够向任一方向翻转,具体取决于您点击卡片的上半部分还是下半部分。

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

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Scaffold(body: MyApp())));
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final sides = const [
    Card(color: Colors.red, text: "One"),
    Card(color: Colors.blue, text: "Two"),
    Card(color: Colors.green, text: "Three"),
  ];
  late Widget _card = sides[0];

  int _currentSide = 0;
  bool _flippingForward = true;
  bool _flipWhenGreater = false;

  void startFlipping(bool forward) {
    setState(() {
      _flipWhenGreater = forward;
      _flippingForward = forward;
    });
    if (forward) {
      _controller.forward(from: 0);
      _card = sides[_currentSide];
    } else {
      _controller.reverse(from: 1);
      _card = Transform.scale(
        scaleY: -1,
        child: sides[_currentSide],
      );
    }
  }

  void flip() {
    _flipWhenGreater = !_flipWhenGreater;
    setState(() {
      _currentSide = (_currentSide + (_flippingForward ? 1 : -1)) % sides.length;
      if (_flippingForward) {
        _card = Transform.scale(
          scaleY: -1,
          child: sides[_currentSide],
        );
      } else {
        _card = sides[_currentSide];
      }
    });
  }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 600));
    _controller.addListener(() {
      if (_controller.value >= .5 == _flipWhenGreater) {
        flip();
      } else if (_controller.value < .5 != _flipWhenGreater) {
        flip();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final midY = MediaQuery.sizeOf(context).height / 2;
    return Center(
      child: GestureDetector(
        onTapDown: (tapDownDetails) {
          startFlipping(tapDownDetails.globalPosition.dy > midY);
        },
        child: AnimatedBuilder(
          animation: _controller,
          builder: (c, anim) => Transform(
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.0025)
              ..rotateX(_controller.value * pi),
            alignment: FractionalOffset.center,
            child: _card,
          ),
        ),
      ),
    );
  }
}

class Card extends StatelessWidget {
  const Card({required this.color, required this.text, Key? key}) : super(key: key);

  final Color color;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 300,
      height: 300,
      color: color,
      child: Center(child: Text(text)),
    );
  }
}
  
© www.soinside.com 2019 - 2024. All rights reserved.