如何在Flutter中建立像这样的GIF动画标题?

时间:2018-08-31 21:50:15

标签: dart flutter

在iOS中,我编写了一个稍微复杂的自定义UIViewController,用于处理唯一的子控制器之间的转换。最值得注意的是,每个视图顶部都有一个特殊的标题视图。我仍在努力地在Flutter的头到尾体系结构中全神贯注,并希望获得一些有关如何实现此目标的建议。页眉有两种类型:“弧形”和“轮廓”,并且每一种都随着用户滚动而从展开状态变为折叠状态。此外,在类型和状态的任意组合之间进行导航都可以定义一个过渡。

enter image description here

这是例如在TabBar中使用时的外观。无论是否嵌套在Tab / NavigationControllers中,都可以优雅地处理过渡。

enter image description here

1 个答案:

答案 0 :(得分:4)

这就是我的想法,希望对您有所帮助(点击观看视频)

Video

注意:

  • 最好减少动画控制器的数量,最好是同时控制标头范围和弧度的单个控制器
  • 标题下方的内容没有动画,但是我敢肯定您也可以添加它。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Anim playground',
      theme: ThemeData(
        brightness: Brightness.dark,
      ),
      home: AnimatedPageTest(),
    );
  }
}

class AnimatedPageTest extends StatefulWidget {
  @override
  _AnimatedPageTestState createState() => _AnimatedPageTestState();
}

class _AnimatedPageTestState extends State<AnimatedPageTest> {
  bool _arc = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(child: AnimatedPage(
        appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
        backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
        children: List.generate(30, (index) => ListTile(title: Text('index'),)),
      ),),
      persistentFooterButtons: <Widget>[
        FlatButton(
          child: Text('Switch'),
          onPressed: () {
            setState(() {
              _arc = !_arc;
            });
          },
        )
      ],
    );
  }
}


enum HeaderAppearance { arc, profile }

double _getTargetMaxExtent(HeaderAppearance appearance) {
  if (appearance == HeaderAppearance.arc) {
    return 150.0;
  } else {
    return 75.0;
  }
}

double _getTargetArcAnimationValue(HeaderAppearance appearance) {
  if (appearance == HeaderAppearance.arc) {
    return 1.0;
  } else {
    return 0.0;
  }
}

class AnimatedPage extends StatefulWidget {
  AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);

  final HeaderAppearance appearance;
  final String backgroundImage;
  final List<Widget> children;

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

class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
  AnimationController _maxExtentAnimation;

  @override
  void initState() {
    super.initState();
    _maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
  }

  @override
  void didUpdateWidget(AnimatedPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.appearance != oldWidget.appearance) {
      _maxExtentAnimation.animateTo(
        _getTargetMaxExtent(widget.appearance),
        duration: Duration(milliseconds: 600),
        curve: Curves.easeInOut,
      );
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _maxExtentAnimation,
      builder: (context, child) {
        return CustomScrollView(
          slivers: <Widget>[
            SliverPersistentHeader(
              pinned: true,
              delegate: AnimatedHeaderDelegate(
                appearance: widget.appearance,
                backgroundImage: widget.backgroundImage,
                minExtent: 50.0,
                maxExtent: _maxExtentAnimation.value,
              ),
            ),
            child,
          ],
        );
      },
      child: SliverList(delegate: SliverChildListDelegate(widget.children)),
    );
  }
}

class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
  AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});

  final HeaderAppearance appearance;

  final String backgroundImage;

  @override
  final double minExtent;
  @override
  final double maxExtent;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
    return AnimatedHeader(
      appearance: appearance,
      backgroundImage: backgroundImage,
      curvatureMultiplier: 1.0 - shrinkRelative,
    );
  }

  @override
  bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
    return appearance != oldDelegate.appearance ||
        minExtent != oldDelegate.minExtent ||
        maxExtent != oldDelegate.maxExtent;
  }
}

class AnimatedHeader extends StatefulWidget {
  AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);

  final HeaderAppearance appearance;

  final String backgroundImage;

  final double curvatureMultiplier;

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

class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
  AnimationController _arcAnimation;

  @override
  void initState() {
    super.initState();
    _arcAnimation = AnimationController(
      vsync: this,
      value: _getTargetArcAnimationValue(widget.appearance),
      duration: Duration(milliseconds: 600),
    );
  }

  @override
  void didUpdateWidget(AnimatedHeader oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.appearance != oldWidget.appearance) {
      _arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
      builder: (context, child) {
        return ClipPath(
          clipper: ArcClipper(
            curvature: _arcAnimation.value * widget.curvatureMultiplier,
          ),
          clipBehavior: Clip.antiAlias,
          child: child,
        );
      },
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          AnimatedSwitcher(
            duration: Duration(milliseconds: 600),
            child: Container(
              key: ValueKey(widget.backgroundImage),
              decoration: BoxDecoration(
                image: DecorationImage(
                  image: AssetImage(widget.backgroundImage),
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
          Center(
            child: Text(
              'TITLE',
              style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
            ),
          ),
        ],
      ),
    );
  }
}

class ArcClipper extends CustomClipper<Path> {
  ArcClipper({this.curvature});

  final double curvature;

  @override
  Path getClip(Size size) {
    if (curvature == 0.0) {
      return Path()..addRect(Offset.zero & size);
    } else {
      return Path()
        ..moveTo(0.0, 0.0)
        ..lineTo(size.width, 0.0)
        ..lineTo(size.width, size.height)
        ..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
        ..close();
    }
  }

  @override
  bool shouldReclip(ArcClipper oldClipper) {
    return curvature != oldClipper.curvature;
  }
}