【问题标题】:Mimic iOS contact form AppBar模仿 iOS 联系表单 AppBar
【发布时间】:2020-10-08 05:12:34
【问题描述】:

我正在尝试模仿 iOS 联系表单应用栏。

展开

折叠

这是我到目前为止的地方

主屏幕

class CompanyScreen extends StatefulWidget {
  @override
  _CompanyScreenState createState() => _CompanyScreenState();
}

class _CompanyScreenState extends State<CompanyScreen> {
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverPersistentHeader(
            pinned: true,
            floating: true,
            delegate: SafeAreaPersistentHeaderDelegate(
                expandedHeight: 200,
                flexibleSpace:
                    SafeArea(child: Image.asset('assets/images/user.png'))),
          ),
          SliverList(
            delegate: SliverChildListDelegate([
              TextField(),
            ]),
          )
        ],
      ),
    );
  }
}

SliverHeader

class SafeAreaPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  final Widget title;

  final Widget flexibleSpace;

  final double expandedHeight;

  SafeAreaPersistentHeaderDelegate(
      {this.title, this.flexibleSpace, this.expandedHeight});

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    final Widget appBar = FlexibleSpaceBar.createSettings(
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: max(minExtent, maxExtent - shrinkOffset),
      toolbarOpacity: 1,
      child: AppBar(
          actions: <Widget>[
            Container(
              height: 60,
              child: FlatButton(
                child: Text('Done'),
              ),
            )
          ],
          backgroundColor: Colors.blue,
          automaticallyImplyLeading: false,
          title: title,
          flexibleSpace: (title == null && flexibleSpace != null)
              ? Semantics(child: flexibleSpace, header: true)
              : flexibleSpace,
          centerTitle: true,
          toolbarOpacity: 1,
          bottomOpacity: 1.0),
    );
    return appBar;
  }

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => 80;

  @override
  bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
    if (old.flexibleSpace != flexibleSpace) {
      return true;
    }
    return false;
  }
}

更新:一切正常,但我在图像下添加文本(添加照片)并在折叠时使该文本消失时遇到问题。使用此解决方案,如果我将图像包装到列中,则图像会扩展溢出并且不会缩放。

要求:

  1. AppBar 和 flex 区域必须在安全区域内

  2. 带图片的小部件底部必须有文字,可以动态更改(添加图片或更改图片),并且必须是可点击的

  3. 当弹性区域通过一些过渡折叠时,图像区域下方的文本必须消失

  4. 能够在与操作按钮对齐的应用栏中添加标题

  5. 如果在应用栏中提供标题,则弹性区域应缩放到标题下方,如果没有,则弹性区域应缩放到标题区域,如上图所示

非常感谢任何帮助

【问题讨论】:

  • 对于被剪切的图像,我建议放置一个 minHeight 属性。要在图像下方添加文本,我认为您可以检查包含图像的小部件的高度,并在到达时调用 setState() 。我认为这可以使用 streambuilder 和 LimitedBox 小部件的 maxheight 属性来实现,但我不确定如何获得limitedbox的当前高度。
  • @Uni LimitedBox 不会对图像的大小做任何事情。我什至将 maxHeight 设置为 50px 并且都是一样的
  • 您可以在这种情况下使用受限盒和装配盒。看看这个:youtube.com/watch?v=T4Uehk3_wlY&vl=en
  • 尝试使用 LimitedBox 和 FittedBox 解决在该图像下方添加该文本的问题。我不确定这是否是最好的解决方案。祝你好运。
  • 一种方法是使用onStretchTrigger 来了解SliverAppBar 何时完全展开,然后我们可以切换VisibilityText 小部件。

标签: flutter dart


【解决方案1】:

我试了一下。我不是条子方面的专家,所以这个解决方案可能并不完美。我已将您的代码作为起点。该列似乎停用了所有缩放,所以我手动缩放。

这是你的应用栏

更新我对其进行了一些调整,使其感觉更像 iOS 应用栏,而且我添加了额外的功能

import 'dart:math';

import 'package:flutter/material.dart';

double _defaultTextHeight = 14;
double _defaultTextPadding = 5;
double _defaultAppBarHeight = 60;
double _defaultMinAppBarHeight = 40;
double _unknownTextValue = 1;

class AppBarSliverHeader extends SliverPersistentHeaderDelegate {
  final String title;
  final double expandedHeight;
  final double safeAreaPadding;
  final Widget flexibleImage;
  final double flexibleSize;
  final String flexibleTitle;
  final double flexiblePadding;
  final bool flexToTop;
  final Function onTap;
  final Widget rightButton;
  final Widget leftButton;

  AppBarSliverHeader(
      {this.title,
      this.onTap,
      this.flexibleImage,
      @required this.expandedHeight,
      @required this.safeAreaPadding,
      this.flexibleTitle = '',
      this.flexToTop = false,
      this.leftButton,
      this.rightButton,
      this.flexibleSize = 30,
      this.flexiblePadding = 4});

  double _textPadding(double shrinkOffset) {
    return _defaultTextPadding * _scaleFactor(shrinkOffset);
  }

  double _widgetPadding(double shrinkOffset) {
    double offset;
    if (title == null) {
      offset = _defaultMinAppBarHeight * _scaleFactor(shrinkOffset);
    } else {
      if (flexToTop) {
        offset = _defaultAppBarHeight * _scaleFactor(shrinkOffset);
      } else {
        offset = (_defaultAppBarHeight - _defaultMinAppBarHeight) *
                _scaleFactor(shrinkOffset) +
            _defaultMinAppBarHeight;
      }
    }
    return offset;
  }

  double _topOffset(double shrinkOffset) {
    double offset;
    if (title == null) {
      offset = safeAreaPadding +
          (_defaultMinAppBarHeight * _scaleFactor(shrinkOffset));
    } else {
      if (flexToTop) {
        offset = safeAreaPadding +
            (_defaultAppBarHeight * _scaleFactor(shrinkOffset));
      } else {
        offset = safeAreaPadding +
            ((_defaultAppBarHeight - _defaultMinAppBarHeight) *
                _scaleFactor(shrinkOffset)) +
            _defaultMinAppBarHeight;
      }
    }

    return offset;
  }

  double _calculateWidgetHeight(double shrinkOffset) {
    double actualTextHeight = _scaleFactor(shrinkOffset) * _defaultTextHeight +
        _textPadding(shrinkOffset) +
        _unknownTextValue;

    final padding = title == null
        ? (2 * flexiblePadding)
        : flexToTop ? (2 * flexiblePadding) : flexiblePadding;

    final trueMinExtent = minExtent - _topOffset(shrinkOffset);

    final trueMaxExtent = maxExtent - _topOffset(shrinkOffset);

    double minWidgetSize =
        trueMinExtent - padding;

    double widgetHeight =
        ((trueMaxExtent - actualTextHeight) - shrinkOffset) - padding;

    return widgetHeight >= minWidgetSize ? widgetHeight : minWidgetSize;
  }

  double _scaleFactor(double shrinkOffset) {
    final ratio = (maxExtent - minExtent) / 100;
    double percentageHeight = shrinkOffset / ratio;
    double limitedPercentageHeight =
        percentageHeight >= 100 ? 100 : percentageHeight;
    return 1 - (limitedPercentageHeight / 100);
  }

  Widget _builtContent(BuildContext context, double shrinkOffset) {
    _topOffset(shrinkOffset);
    return SafeArea(
      bottom: false,
      child: Semantics(
        child: Padding(
          padding: title == null
              ? EdgeInsets.symmetric(vertical: flexiblePadding)
              : flexToTop
                  ? EdgeInsets.symmetric(vertical: flexiblePadding)
                  : EdgeInsets.only(bottom: flexiblePadding),
          child: GestureDetector(
            onTap: onTap,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                LimitedBox(
                    maxWidth: _calculateWidgetHeight(shrinkOffset),
                    maxHeight: _calculateWidgetHeight(shrinkOffset),
                    child: Container(
                      decoration: BoxDecoration(
                          borderRadius: BorderRadius.all(Radius.circular(
                              _calculateWidgetHeight(shrinkOffset))),
                          color: Colors.white),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(
                            _calculateWidgetHeight(shrinkOffset)),
                        child: flexibleImage,
                      ),
                    )),
                Padding(
                  padding: EdgeInsets.only(top: _textPadding(shrinkOffset)),
                  child: Text(
                    flexibleTitle,
                    textScaleFactor: _scaleFactor(shrinkOffset),
                    style: TextStyle(
                        fontSize: _defaultTextHeight,
                        color: Colors.white
                            .withOpacity(_scaleFactor(shrinkOffset)), height: 1),
                  ),
                )
              ],
            ),
          ),
        ),
        button: true,
      ),
    );
  }

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    final Widget appBar = FlexibleSpaceBar.createSettings(
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: max(minExtent, maxExtent - shrinkOffset),
      toolbarOpacity: 1,
      child: AppBar(
          actions: <Widget>[rightButton == null ? Container() : rightButton],
          leading: leftButton == null ? Container() : leftButton,
          backgroundColor: Colors.blue,
          automaticallyImplyLeading: false,
          title: title != null
              ? Text(
                  title,
                  style: TextStyle(
                      color: flexToTop
                          ? Colors.white.withOpacity(_scaleFactor(shrinkOffset))
                          : Colors.white),
                )
              : null,
          flexibleSpace: Padding(
            padding: EdgeInsets.only(top: _widgetPadding(shrinkOffset)),
            child: _builtContent(context, shrinkOffset),
          ),
          centerTitle: true,
          toolbarOpacity: 1,
          bottomOpacity: 1.0),
    );
    return appBar;
  }

  @override
  double get maxExtent => expandedHeight + safeAreaPadding;

  @override
  double get minExtent => title == null
      ? _defaultAppBarHeight + safeAreaPadding
      : flexToTop
          ? _defaultAppBarHeight + safeAreaPadding
          : _defaultAppBarHeight + safeAreaPadding + flexibleSize;

  @override
  bool shouldRebuild(AppBarSliverHeader old) {
    if (old.flexibleImage != flexibleImage) {
      return true;
    }
    return false;
  }
}

这里是用法

Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverPersistentHeader(
            pinned: true,
            floating: true,
            delegate: AppBarSliverHeader(
                expandedHeight: 250,
                safeAreaPadding: MediaQuery.of(context).padding.top,
                title: 'New Contact',
                flexibleImage: Image.asset('assets/images/avatar.png'),
                flexibleTitle: 'Add Image',
                flexiblePadding: 6,
                flexibleSize: 50,
                flexToTop: true,
                onTap: () {
                  print('hello');
                },
                leftButton: IconButton(
                  icon: Text('Cancel'),
                  iconSize: 60,
                  padding: EdgeInsets.zero,
                  onPressed: () {},
                ),
                rightButton: IconButton(
                  icon: Text('Done'),
                  iconSize: 60,
                  padding: EdgeInsets.zero,
                  onPressed: () {},
                )),
          ),
          SliverList(
            delegate: SliverChildListDelegate([
              TextField(),
            ]),
          )
        ],
      ),
    );

有些事情也让我感到意外。首先是文字大小。似乎文本大小不是实际的文本大小,所以我在那里添加了_unknownTextValue 以进行补偿。此外,即使文本大小设置为 0,文本小部件的大小仍为 1px,因此我已在注释代码中对其进行了补偿。另一件事是我想为图像使用CircularAvatar,但显然CircularAvatar 小部件在更改影响应用栏动画的大小时内置了动画,因此我构建了自定义头像。

更新:为了使实际文本高度与字体大小相同,我在 TextStyle 中添加了 height 属性 1。它似乎可以工作,但是文本字段仍然偶尔会溢出高达 1px,所以我将 _unknownTextValue 保持在 1px

正如我所说,我不是 sliver 专家,所以可能有更好的解决方案,所以我建议您等待其他答案

注意:我只在 2 台 iOS 设备上测试过,所以你应该进一步测试才能使用它

有标题

无标题

激活了 Title 和 flexToTop

【讨论】:

    猜你喜欢
    • 2013-09-15
    • 1970-01-01
    • 2018-11-08
    • 1970-01-01
    • 1970-01-01
    • 2022-11-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多