flutter - 如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们

标签 flutter flutter-animation flutter-sliver sliverappbar flutter-sliverappbar

我对 Appbar 有这些要求,但我找不到解决它们的方法。

  • 拉伸(stretch) , AppBar 必须显示 两个图像一个在另一个之上并且标题必须隐藏。
  • 关闭 , AppBar 必须显示标题和 滚动时两个图像必须按比例缩小并移动到标题的两侧 .滚动时标题变为可见。

  • 我创建了几个模型来帮助获得所需的结果。
    这是拉伸(stretch)时的 Appbar:
    enter image description here
    这是关闭时的 Appbar:
    enter image description here

    最佳答案

    您可以创建自己的SliverAppBar通过扩展 SliverPersistentHeaderDelegate .
    平移、缩放和不透明度更改将在 build(...) 中完成。方法,因为这将在范围更改期间调用(通过滚动),minExtent <-> maxExtent .
    这是一个示例代码。

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primaryColor: Colors.blue,
          ),
          home: HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: CustomScrollView(
            slivers: <Widget>[
              SliverPersistentHeader(
                delegate: MySliverAppBar(
                  title: 'Sample',
                  minWidth: 50,
                  minHeight: 25,
                  leftMaxWidth: 200,
                  leftMaxHeight: 100,
                  rightMaxWidth: 100,
                  rightMaxHeight: 50,
                  shrinkedTopPos: 10,
                ),
                pinned: true,
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (_, int i) => Container(
                    height: 50,
                    color: Color.fromARGB(
                      255,
                      Random().nextInt(255),
                      Random().nextInt(255),
                      Random().nextInt(255),
                    ),
                  ),
                  childCount: 50,
                ),
              ),
            ],
          ),
        );
      }
    }
    
    class MySliverAppBar extends SliverPersistentHeaderDelegate {
      MySliverAppBar({
        required this.title,
        required this.minWidth,
        required this.minHeight,
        required this.leftMaxWidth,
        required this.leftMaxHeight,
        required this.rightMaxWidth,
        required this.rightMaxHeight,
        this.titleStyle = const TextStyle(fontSize: 26),
        this.shrinkedTopPos = 0,
      });
    
      final String title;
      final TextStyle titleStyle;
      final double minWidth;
      final double minHeight;
      final double leftMaxWidth;
      final double leftMaxHeight;
      final double rightMaxWidth;
      final double rightMaxHeight;
    
      final double shrinkedTopPos;
    
      final GlobalKey _titleKey = GlobalKey();
    
      double? _topPadding;
      double? _centerX;
      Size? _titleSize;
    
      double get _shrinkedTopPos => _topPadding! + shrinkedTopPos;
    
      @override
      Widget build(
        BuildContext context,
        double shrinkOffset,
        bool overlapsContent,
      ) {
        if (_topPadding == null) {
          _topPadding = MediaQuery.of(context).padding.top;
        }
        if (_centerX == null) {
          _centerX = MediaQuery.of(context).size.width / 2;
        }
        if (_titleSize == null) {
          _titleSize = _calculateTitleSize(title, titleStyle);
        }
    
        double percent = shrinkOffset / (maxExtent - minExtent);
        percent = percent > 1 ? 1 : percent;
    
        return Container(
          color: Colors.red,
          child: Stack(
            children: <Widget>[
              _buildTitle(shrinkOffset),
              _buildLeftImage(percent),
              _buildRightImage(percent),
            ],
          ),
        );
      }
    
      Size _calculateTitleSize(String text, TextStyle style) {
        final TextPainter textPainter = TextPainter(
            text: TextSpan(text: text, style: style),
            maxLines: 1,
            textDirection: TextDirection.ltr)
          ..layout(minWidth: 0, maxWidth: double.infinity);
        return textPainter.size;
      }
    
      Widget _buildTitle(double shrinkOffset) => Align(
            alignment: Alignment.topCenter,
            child: Padding(
              padding: EdgeInsets.only(top: _topPadding!),
              child: Opacity(
                opacity: shrinkOffset / maxExtent,
                child: Text(title, key: _titleKey, style: titleStyle),
              ),
            ),
          );
    
      double getScaledWidth(double width, double percent) =>
          width - ((width - minWidth) * percent);
    
      double getScaledHeight(double height, double percent) =>
          height - ((height - minHeight) * percent);
    
      /// 20 is the padding between the image and the title
      double get shrinkedHorizontalPos =>
          (_centerX! - (_titleSize!.width / 2)) - minWidth - 20;
    
      Widget _buildLeftImage(double percent) {
        final double topMargin = minExtent;
        final double rangeLeft =
            (_centerX! - (leftMaxWidth / 2)) - shrinkedHorizontalPos;
        final double rangeTop = topMargin - _shrinkedTopPos;
    
        final double top = topMargin - (rangeTop * percent);
        final double left =
            (_centerX! - (leftMaxWidth / 2)) - (rangeLeft * percent);
    
        return Positioned(
          left: left,
          top: top,
          child: Container(
            width: getScaledWidth(leftMaxWidth, percent),
            height: getScaledHeight(leftMaxHeight, percent),
            color: Colors.black,
          ),
        );
      }
    
      Widget _buildRightImage(double percent) {
        final double topMargin = minExtent + (rightMaxHeight / 2);
        final double rangeRight =
            (_centerX! - (rightMaxWidth / 2)) - shrinkedHorizontalPos;
        final double rangeTop = topMargin - _shrinkedTopPos;
    
        final double top = topMargin - (rangeTop * percent);
        final double right =
            (_centerX! - (rightMaxWidth / 2)) - (rangeRight * percent);
    
        return Positioned(
          right: right,
          top: top,
          child: Container(
            width: getScaledWidth(rightMaxWidth, percent),
            height: getScaledHeight(rightMaxHeight, percent),
            color: Colors.white,
          ),
        );
      }
    
      @override
      double get maxExtent => 300;
    
      @override
      double get minExtent => _topPadding! + 50;
    
      @override
      bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
          false;
    }
    

    关于flutter - 如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66825376/

    相关文章:

    Flutter 网络图像作为谷歌地图标记

    flutter - 使用 SliverAppBar flutter 设计这个动画

    Flutter tabsView 和 NestedScrollView 滚动问题

    flutter - 使用 NestedScrollView,向上滚动会将其他选项卡重置为上方

    android - Flutter:无法加载 Assets 图像提供者:AssetImage(bundle:null,名称: "assets/images/rose.jpg")

    firebase - 如何查询 firestore 中的嵌套对象

    dart - flutter :未处理的异常:错误状态:调用关闭后无法添加新事件

    边框中的 flutter 箭头

    flutter - 如何在 BLoC 模式上更改 Widget 时添加动画过渡?

    flutter - 如何在 Flutter 中实现 Tiktok 之类的视频流和视频滚动