5 votes

Mimer le formulaire de contact d'iOS AppBar

J'essaie d'imiter la barre d'application du formulaire de contact d'iOS.

élargi

enter image description here

effondré

enter image description here

Voici où j'en suis pour l'instant

enter image description here

Écran principal

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

MISE A JOUR : Tout fonctionne mais j'ai un problème pour ajouter le texte sous l'image (Add Photo) et faire disparaître ce texte lorsqu'il est réduit. Avec cette solution, si j'enroule l'image dans une colonne, l'image déborde et n'est pas mise à l'échelle.

Exigences :

  1. L'AppBar et la zone de flexion doivent être situés dans une zone sûre.

  2. Le widget avec image doit avoir un texte en bas qui peut être modifié dynamiquement (Ajouter une image ou Changer l'image) et il doit être cliquable.

  3. Le texte sous la zone d'image doit disparaître lorsque la zone de flexion est réduite avec une certaine transition.

  4. Possibilité d'ajouter un titre dans la barre d'application alignée avec les boutons d'action

  5. Lorsque le titre de la barre d'application est fourni, la zone de flexion doit être mise à l'échelle en dessous du titre, sinon la zone de flexion doit être mise à l'échelle dans la zone de titre, comme sur l'image ci-dessus.

Toute aide à ce sujet sera grandement appréciée.

4voto

LonelyWolf Points 3019

J'ai essayé Je ne suis pas un expert en matière d'éclats, il se peut donc que cette solution ne soit pas parfaite. J'ai pris votre code comme point de départ. La colonne semble désactiver toute mise à l'échelle, j'ai donc mis à l'échelle tous les éléments manuellement.

Voici votre barre d'application

MISE À JOUR J'ai un peu modifié la barre d'application pour qu'elle ressemble plus à celle d'iOS et j'ai ajouté une fonction supplémentaire.

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

et voici l'utilisation

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(),
            ]),
          )
        ],
      ),
    );

Certaines choses m'ont également surpris. Tout d'abord, la taille du texte. Il semble que la taille du texte n'est pas une taille réelle, j'ai donc ajouté _unknownTextValue en échange d'une compensation. De plus, même si la taille du texte est réglée sur 0, le widget Texte a toujours une taille de 1px, j'ai donc compensé cela dans le code commenté. Une autre chose est que je voulais utiliser CircularAvatar pour l'image mais apparemment le CircularAvatar Le widget dispose d'une animation intégrée lors du changement de taille qui interfère avec l'animation de la barre d'application, j'ai donc créé un avatar personnalisé.

MISE À JOUR : pour que la hauteur du texte soit identique à la taille de la police, j'ai ajouté la propriété height 1 à TextStyle. Cela semble fonctionner, mais il y a encore des débordements occasionnels sur le champ de texte jusqu'à 1px, donc j'ai gardé _unknownTextValue à 1px.

Comme je l'ai dit, je ne suis pas un expert en matière d'argent liquide et il existe peut-être de meilleures solutions.

NOTE : Je ne l'ai testé que sur 2 appareils iOS, vous devez donc faire d'autres tests avant de l'utiliser.

Avec titre

enter image description here

Sans titre

enter image description here

Avec Titre et flexToTop activés

enter image description here

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X