Mimic iOS contact form AppBar

后端 未结 1 1076
深忆病人
深忆病人 2020-12-17 04:22

I\'m trying to mimic iOS contact form app bar.

expanded

collapsed

Here is where I get so far

Main Screen

1条回答
  •  难免孤独
    2020-12-17 04:55

    I gave it a try.. I'm not an expert on slivers so this solution might not be perfect. I have taken your code as starting point. The column seems to deactivate all scaling so I scaled all manually.

    here is your app bar

    UPDATE I have tweaked it a little so it feels more like iOS app bar plus I've added extra feature

    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: [
                    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: [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;
      }
    }
    

    and here is usage

    Scaffold(
          body: CustomScrollView(
            slivers: [
              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(),
                ]),
              )
            ],
          ),
        );
    

    There are some things which took me by surprise as well. First is text size. It seems like text size is not an actual text size so I've added _unknownTextValue there for compensation. Also even if text size is set to 0 then the Text widget has still 1px size so I've compensated that in commented code. Another thing is I wanted to use CircularAvatar for the image but apparently the CircularAvatar widget has built in animation when changing the size which interfere with app bar animation so I've built custom avatar.

    UPDATE: To make actual text height same as font size, I have added height property 1 to TextStyle. It seems to work however there is still occasional overflow on the textfield of up to 1px so I've kept _unknownTextValue at 1px

    As I said I'm not sliver expert so there might be a better solutions out there so I would suggest you to wait for other answers

    NOTE: I only tested it on 2 iOS devices so you should test further to use it

    With Title

    Without Title

    With Title and flexToTop activated

    0 讨论(0)
提交回复
热议问题