flutter: cómo animar la posición de los elementos en un SliverAppBar para moverlos alrededor del título cuando está cerrado

CorePress2024-01-24  10

Tengo estos requisitos para una Appbar y no encuentro la manera de solucionarlos.

Cuando se estira, AppBar debe mostrar las dos imágenes una encima de la otra y el título debe estar oculto. Cuando está cerrada, AppBar debe mostrar el título y dos imágenes deben reducirse al desplazarse y moverse a ambos lados del título. El título se vuelve visible al desplazarse.

Creé un par de maquetas para ayudar con el resultado necesario.

Esta es la barra de aplicaciones cuando está estirada:

Esta es la barra de aplicaciones cuando está cerrada:



------------------------------------

Puedes crear tu propia SliverAppBar extendiendo SliverPersistentHlíderDelegate.

Los cambios de traducción, escala y opacidad se realizarán en el método build(...) porque se llamará durante los cambios de extensión (mediante desplazamiento), minExtent <-> extensiónmax.

Aquí hay un código de muestra.

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

9

¡Buena! Está bastante cerca de lo que quiero, pero quiero que los cuadros, cuando la barra de aplicaciones esté estirada, estén cerca del título, no en las esquinas. No importa la extensión que tenga el título.

-Mou

30 de marzo de 2021a las 10:16

Lo siento, no lo entiendo del todo. "cuando la barra de aplicaciones está estirada" ¿Qué significa durante el tiempo en que el título es visible? "justo cerca del título" ¿Significa que se deben calcular los valores reducidosLeftPos y reducidosRightPos?

-rickimaru

30 de marzo de 2021 a las 10:34

Lo siento, es culpa mía, escribía rápido en el móvil. Quise decir "encogido", no "estirado". Lo que quería decir es que el diseño final, cuando AppBar estáen minExtent, tiene que ser una especie de (pseudocódigo) Fila (alinear: centro, niños: [cuadro, pequeño relleno, título, pequeño relleno, cuadro]). Pero en su solución, los cuadros se colocarán en las esquinas de la barra de aplicaciones.

-Mou

30/03/2021 a las 11:01

OK2. Comprendido. Modificaré la respuesta más tarde. (cena primero :D)

-rickimaru

30 de marzo de 2021 a las 11:07

@Mou Respuesta actualizada. Consulte la PosHorizontal reducida.

-rickimaru

30/03/2021 a las 11:20



------------------------------------

Las fórmulas son un poco complicadas, pero así es como puedes hacer todos los cálculos sobre la animación:

UPD: agregado a la variable de código para compensar el eje Y de las imágenes cuando se extiende.

Código completo para reproducir:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Body(),
    );
  }
}

class Body extends StatefulWidget {
  const Body({
    Key key,
  }) : super(key: key);

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  double _collapsedHeight = 60;
  double _expandedHeight = 200;
  double
      extentRatio; // Value to control SliverAppBar widget sizes, based on BoxConstraints and
  double minH1 = 40; // Minimum height of the first image.
  double minW1 = 30; // Minimum width of the first image.
  double minH2 = 20; // Minimum height of second image.
  double minW2 = 25; // Minimum width of second image.
  double maxH1 = 60; // Maximum height of the first image.
  double maxW1 = 60; // Maximum width of the first image.
  double maxH2 = 40; // Maximum height of second image.
  double maxW2 = 50; // Maximum width of second image.
  double textWidth = 70; // Width of a given title text.
  double extYAxisOff = 10.0; // Offset on Y axis for both images when sliver is extended.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                  collapsedHeight: _collapsedHeight,
                  expandedHeight: _expandedHeight,
                  floating: true,
                  pinned: true,
                  flexibleSpace: LayoutBuilder(
                    builder:
                        (BuildContext context, BoxConstraints constraints) {
                      extentRatio =
                          (constraints.biggest.height - _collapsedHeight) /
                              (_expandedHeight - _collapsedHeight);
                      double xAxisOffset1 = (-(minW1 - minW2) -
                              textWidth +
                              (textWidth + maxW1) * extentRatio) /
                          2;
                      double xAxisOffset2 = (-(minW1 - minW2) +
                              textWidth +
                              (-textWidth - maxW2) * extentRatio) /
                          2;
                      double yAxisOffset2 = (-(minH1 - minH2) -
                                  (maxH1 - maxH2 - (minH1 - minH2)) *
                                      extentRatio) /
                              2 -
                          extYAxisOff * extentRatio;
                      double yAxisOffset1 = -extYAxisOff * extentRatio;
                      print(extYAxisOff);
                      // debugPrint('constraints=' + constraints.toString());
                      // debugPrint('Scale ratio is $extentRatio');
                      return FlexibleSpaceBar(
                        titlePadding: EdgeInsets.all(0),
                        // centerTitle: true,
                        title: Stack(
                          children: [
                            Align(
                              alignment: Alignment.topCenter,
                              child: AnimatedOpacity(
                                duration: Duration(milliseconds: 300),
                                opacity: extentRatio < 1 ? 1 : 0,
                                child: Padding(
                                  padding: const EdgeInsets.only(top: 30.0),
                                  child: Container(
                                    color: Colors.indigo,
                                    width: textWidth,
                                    alignment: Alignment.center,
                                    height: 20,
                                    child: Text(
                                      "TITLE TEXT",
                                      style: TextStyle(
                                        color: Colors.white,
                                        fontSize: 12.0,
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                            Align(
                              alignment: Alignment.bottomCenter,
                              child: Row(
                                crossAxisAlignment: CrossAxisAlignment.end,
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Container(
                                    transform: Matrix4(
                                        1,0,0,0,
                                        0,1,0,0,
                                        0,0,1,0,
                                        xAxisOffset1,yAxisOffset1,0,1),
                                    width:
                                        minW1 + (maxW1 - minW1) * extentRatio,
                                    height:
                                        minH1 + (maxH1 - minH1) * extentRatio,
                                    color: Colors.red,
                                  ),
                                  Container(
                                    transform: Matrix4(
                                        1,0,0,0,
                                        0,1,0,0,
                                        0,0,1,0,
                                        xAxisOffset2,yAxisOffset2,0,1),
                                  
                                    width:
                                        minW2 + (maxW2 - minW2) * extentRatio,
                                    height:
                                        minH2 + (maxH2 - minH2) * extentRatio,
                                    color: Colors.purple,
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                      );
                    },
                  )),
            ];
          },
          body: Center(
            child: Text("Sample Text"),
          ),
        ),
      ),
    );
  }
}

4

¡Se ve bien! CulataEl título puede tener una longitud variable y no se debe utilizar AnimatedContainer, ya que los elementos deben ser interpolados por el propio desplazamiento, no por un elemento "externo". animación.

-Mou

30 de marzo de 2021 a las 10:57

@Mou pero los elementos se interpolan mediante el desplazamiento, calculado sobre la marcha

-Simón Sot

30/03/2021 a las 11:02

Sí, tienes razón, pero podemos evitar ese AnimatedContainer, ya que la interpolación ya se genera mediante desplazamiento. Además, todavía tienes una longitud de título fija :)

-Mou

30 de marzo de 2021 a las 11:17

Sí, tenías razón sobre los contenedores, lo arreglaste, sobre el título dejado como está. Quizás alguien lo necesite así. Ese requisito no estaba en la pregunta.

-Simón Sot

30 de marzo de 2021 a las 11:33

randomThread
javascript - ¿Cuál es la diferencia entre claves de objeto con y sin comillas?php: no se puede cambiar el modo de archivo en bin: operación no permitidac# - Matriz - encuentra el número que se repite con más frecuenciaResalte variables de Javascript no definidas en Visual Studio Codesql: ¿Por qué recibo este error cuando quiero que el empleado NO tenga menos de 18 años ni más de 60?Python: comprueba si la cadena contiene una palabra en alguna variaciónlaravel - Foreach dentro de Foreach según el valor de la columnaSwiftui, cómo cambiar el radio de sombra de la vista al tocarjavascript - ¿Cómo llamar a un php con parámetro de un