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

Mou*_*Mou 6 flutter flutter-sliver flutter-animation sliverappbar flutter-sliverappbar

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

  • 拉伸,AppBar具有以显示两个图像一个在另一个上方的标题必须被隐藏。
  • 关闭,AppBar有显示标题和两幅图像滚动时按比例缩小,并移至标题两侧。滚动时标题变得可见。

我创建了几个模型来帮助获得所需的结果。

这是拉伸时的 Appbar:

在此处输入图片说明

这是关闭时的 Appbar:

在此处输入图片说明

ric*_*aru 5

您可以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;
}
Run Code Online (Sandbox Code Playgroud)