Flutter Hero 多个元素反向动画从中心开始

3

我正在尝试创建一个包含多个元素的英雄动画,目前我的代码如下:

main.dart:

import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_playground/screen_two.dart';
import 'package:flutter_playground/transition/hero_dialog_route.dart';
import 'package:flutter_playground/transition/hero_page_route.dart';
import 'package:flutter_playground/transition_open_container_wrapper.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            childAspectRatio: 1,
            crossAxisSpacing: 4,
            mainAxisSpacing: 4,
          ),
          shrinkWrap: true,
          itemCount: 100,
          itemBuilder: (context, index) {
            return getChild(index);
          }),
    );
  }

  Widget getChild(int index) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          HeroDialogRoute(builder: (context) => ScreenTwo(index: index)),
        );
      },
      child: getCard(index),
    );
  }

  Widget getCard2(int index) {
    return OpenContainerWrapper(
      isRootNavigator: true,
      closedBuilder: (context, voidCallback) {
        return InkWell(
          onTap: voidCallback,
          child: getCard(index),
        );
      },
      openBuilder: (context, voidCallback) {
        return Container(height: 300, width: 300, child: ScreenTwo(index: index),);
      },
      transitionType: ContainerTransitionType.fade,
      onClosed: (value) {},
    );
  }

  Widget getCard(int index) {
    return Stack(
      children: [
        Hero(
          createRectTween: (Rect? begin, Rect? end) {
            return CurvedRectArcTween(begin: begin, end: end);
          },
          tag: 'hero_card_${index}',
          child: Card(
            child: SizedBox(
              height: double.infinity,
              width: double.infinity,
            ),
          ),
        ),
        Hero(
          tag: 'hero_image_${index}',
          createRectTween: (Rect? begin, Rect? end) {
            return CurvedRectArcTween(begin: begin, end: end);
          },
          child: Container(
            padding: EdgeInsets.all(8),
            child: Image.network('https://picsum.photos/200', fit: BoxFit.contain,),
            height: double.infinity,
            width: double.infinity,
          ),
        )
      ],
    );
  }
}

屏幕二:

import 'dart:ui';

import 'package:flutter/material.dart';

/// Created by ali on 12/19/22.
class ScreenTwo extends StatefulWidget {
  final int index;

  const ScreenTwo({super.key, required this.index});

  @override
  State<StatefulWidget> createState() {
    return _ScreenTwoState();
  }
}

class _ScreenTwoState extends State<ScreenTwo> {

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.pop(context);
      },
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: Scaffold(
          appBar: AppBar(
            automaticallyImplyLeading: false,
            elevation: 0,
            backgroundColor: Colors.transparent,
          ),
          backgroundColor: Colors.transparent,
          body: BackdropFilter(
              filter: ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Center(
            child: getContent(),
          )),
        ),
      ),
    );
  }

  Widget getContent() {
    return Stack(
      children: [
        Hero(
          tag: 'hero_card_${widget.index}',
          createRectTween: (Rect? begin, Rect? end) {
            return RectTween(begin: begin, end: end);
          },
          child: Container(
            width: 416,
            height: 416,
          ),
        ),
        Padding(
          padding: EdgeInsets.all(8),
          child: Hero(
            tag: 'hero_image_${widget.index}',
            createRectTween: (Rect? begin, Rect? end) {
              return RectTween(begin: begin, end: end);
            },
            child: Card(
              child: ListView(
                shrinkWrap: true,
                children: [
                  Padding(
                    padding: EdgeInsets.all(8),
                    child: Image.network(
                      'https://picsum.photos/200',
                      fit: BoxFit.contain,
                    ),
                  ),
                  Container(
                    height: 200,
                  )
                ],
              ),
            ),
          ),
        )
      ],
    );
  }
}

hero_page_route:

import 'package:flutter/material.dart';

/// Created by ali on 12/24/22.
class CurvedRectArcTween extends RectTween {

  late double a;
  late double b;
  late double c;
  late double d;

  CurvedRectArcTween({
    Rect? begin,
    Rect? end,
    double? a,
    double? b,
    double? c,
    double? d,
  }) : super(begin: begin, end: end) {
    this.a = a ?? 0;
    this.b = b ?? 0;
    this.c = c ?? 0;
    this.d = d ?? 0;
  }

  @override
  Rect? lerp(double t) {
    Cubic easeInOut = Cubic(a, b , c, d);
    double curvedT = easeInOut.transform(t);
    return super.lerp(curvedT);
  }
}

and hero_dialog_route.dart:

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

/// Created by ali on 11/22/22.
class HeroDialogRoute<T> extends PageRoute<T> {
  final WidgetBuilder builder;

  HeroDialogRoute({required this.builder}) : super();

  @override
  bool get opaque => false;

  @override
  bool get barrierDismissible => true;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 1000);

  @override
  bool get maintainState => true;

  @override
  Color get barrierColor => Colors.black54;

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return FadeScaleTransition(
      animation: animation,
      child: child,
    );
  }

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) {
    return builder(context);
  }

  @override
  String? get barrierLabel => null;
}

transition_open_container_wrapper.dart:

class OpenContainerWrapper extends StatelessWidget {
  const OpenContainerWrapper({
    Key? key,
    required this.isRootNavigator,
    required this.closedBuilder,
    required this.openBuilder,
    required this.transitionType,
    required this.onClosed,
  }) :super(key: key);

  final CloseContainerBuilder closedBuilder;
  final OpenContainerBuilder<bool?> openBuilder;
  final ContainerTransitionType transitionType;
  final ClosedCallback<bool?> onClosed;
  final bool isRootNavigator;

  @override
  Widget build(BuildContext context) {
    return OpenContainer<bool>(
      useRootNavigator: isRootNavigator,
      closedElevation: 0,
      openColor: Colors.transparent,
      transitionDuration: const Duration(milliseconds: 450),
      closedShape: const RoundedRectangleBorder(),
      openShape: const RoundedRectangleBorder(),
      transitionType: transitionType,
      openBuilder: openBuilder,
      onClosed: onClosed,
      tappable: false,
      closedBuilder: closedBuilder,
    );
  }
}

这里有一个GIF来展示代码的功能:

https://i.imgur.com/xRcRWzH.mp4enter image description here

正如你所看到的,当英雄从背景卡片和图像开始时,它会很好地缩放直到动画完成。但是当返回时,图像突然跳到中心位置,返回动画看起来不如开始动画那么好。如何使这两个动画在返回到网格的第一位置时一起运行,就像在第一个动画中一样?

提前致谢。


请问您能否包含 transition_open_container_wrapper.dart 文件? - Jared Anderton
是的,已添加。 - Ali Yucel Akgul
嘿 @AliYucelAkgul,我添加了我的答案:),希望有所帮助。 - diegoveloper
1个回答

7

我简化了你的示例,修复了布局的一些部分,并且没有使用animation包来制作过渡效果。

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 1,
          crossAxisSpacing: 4,
          mainAxisSpacing: 4,
        ),
        shrinkWrap: true,
        itemCount: 100,
        itemBuilder: (context, index) {
          return getChild(index);
        },
      ),
    );
  }

  Widget getChild(int index) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          PageRouteBuilder(
            opaque: false,
            barrierColor: Colors.black38,
            transitionDuration: const Duration(seconds: 1),
            reverseTransitionDuration: const Duration(seconds: 1),
            transitionsBuilder: (context, animation, _, child) =>
                FadeTransition(
              opacity: animation,
              child: child,
            ),
            pageBuilder: (context, _, __) => ScreenTwo(index: index),
          ),
        );
      },
      child: getCard(index),
    );
  }

  Widget getCard(int index) {
    return Stack(
      children: [
        Positioned.fill(
          child: Hero(
            createRectTween: (Rect? begin, Rect? end) {
              return CurvedRectArcTween(begin: begin, end: end);
            },
            tag: 'hero_card_$index',
            child: const Card(),
          ),
        ),
        Positioned.fill(
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Hero(
              tag: 'hero_image_$index',
              createRectTween: (Rect? begin, Rect? end) {
                return CurvedRectArcTween(begin: begin, end: end);
              },
              child: Image.network(
                'https://picsum.photos/200',
                fit: BoxFit.contain,
                alignment: Alignment.topCenter,
              ),
            ),
          ),
        )
      ],
    );
  }
}

class ScreenTwo extends StatefulWidget {
  final int index;

  const ScreenTwo({super.key, required this.index});

  @override
  State<StatefulWidget> createState() {
    return _ScreenTwoState();
  }
}

class _ScreenTwoState extends State<ScreenTwo> {
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: GestureDetector(
        onTap: () {
          Navigator.pop(context);
        },
        child: Center(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: SizedBox(
              height: 500,
              width: 416,
              child: BackdropFilter(
                filter: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
                child: getContent(),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget getContent() {
    return Stack(
      children: [
        Positioned.fill(
          child: Hero(
            tag: 'hero_card_${widget.index}',
            createRectTween: (Rect? begin, Rect? end) {
              return RectTween(begin: begin, end: end);
            },
            child: const Card(),
          ),
        ),
        Positioned.fill(
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Hero(
              tag: 'hero_image_${widget.index}',
              createRectTween: (Rect? begin, Rect? end) {
                return RectTween(begin: begin, end: end);
              },
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Expanded(
                    child: Image.network(
                      'https://picsum.photos/200',
                      fit: BoxFit.contain,
                      alignment: Alignment.topCenter,
                    ),
                  ),
                ],
              ),
            ),
          ),
        )
      ],
    );
  }
}

结果

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接