Equivalente de RelativeLayout en Flutter

84

¿Hay alguna forma de implementar algo similar a lo que RelativeLayouthace en Android?

En particular, estoy buscando algo similar a centerInParent, layout_below:<layout_id>, layout_above:<layout_id>, yalignParentLeft

Para obtener más referencia sobre RelativeLayout: https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html

EDITAR: aquí hay un ejemplo de un diseño que se basa en RelativeLayout

Captura de pantalla

Entonces, en la imagen de arriba, en la parte superior, el texto "Canciones de tofu" está alineado como centerInParentdentro de a RelativeLayout. Mientras que los otros 2 son alignParentLeftyalignParentRight

En cada celda, donde está el ícono de fuego, el número de Me gusta en la parte inferior se alinea alrededor del centro del ícono de llama. Además, el título superior e inferior de cada celda están alineados a la derecha y a la parte superior e inferior del avatar de imagen, respectivamente.

user3217522
fuente

Respuestas:

213

Aleteo diseños se construyen generalmente usando un árbol de Column, Rowy Stackwidgets. Estos widgets toman argumentos de constructor que especifican las reglas de cómo se establecen en relación con los padres de los niños, y también puede influir en el diseño de los niños individuales envolviéndolos en Expanded, Flexible, Positioned, Align, o Centerwidgets.

También es posible crear diseños complejos utilizando CustomMultiChildLayout. Así es como Scaffoldse implementa internamente, y aparece un ejemplo de cómo usarlo en una aplicación en la demostración de Shrine . También puede usar LayoutBuildero CustomPaint, o bajar una capa y extender RenderObjectcomo se muestra en el ejemplo del sector . Hacer sus diseños manualmente de esta manera es más trabajo y crea más posibilidades de errores en los casos de esquina, por lo que trataría de arreglármelas con las primitivas de diseño de alto nivel si puede.

Para responder a sus preguntas específicas:

  • Utilice el leadingy trailingargumentos para AppBarposicionar los elementos de barra de la aplicación. Si desea utilizar un Rowen su lugar, utilice un mainAxisAlignmentde MainAxisAlignment.spaceBetween.
  • Utilice un Rowcon un crossAxisAlignmentde CrossAxisAlignment.centerpara colocar el icono de fuego y el número debajo.
  • Utilice un Columncon una mainAxisAlignmentde MainAxisAlignment.spaceBetweenpara colocar su título superior e inferior. (Debería considerar usar ListTilepara diseñar los mosaicos de la lista, pero perderá el control sobre la posición exacta si hace esto).

Aquí hay un fragmento de código que implementa el diseño que proporcionó. En este ejemplo utilicé un IntrinsicHeightpara determinar la altura de los mosaicos de la canción, pero puede mejorar el rendimiento codificándolos de forma rígida a una altura fija.

captura de pantalla

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        brightness: Brightness.dark,
        primaryColorBrightness: Brightness.dark,
      ),
      home: new HomeScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class Song extends StatelessWidget {
  const Song({ this.title, this.author, this.likes });

  final String title;
  final String author;
  final int likes;

  @override
  Widget build(BuildContext context) {
    TextTheme textTheme = Theme
      .of(context)
      .textTheme;
    return new Container(
      margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
      padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade200.withOpacity(0.3),
        borderRadius: new BorderRadius.circular(5.0),
      ),
      child: new IntrinsicHeight(
        child: new Row(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            new Container(
              margin: const EdgeInsets.only(top: 4.0, bottom: 4.0, right: 10.0),
              child: new CircleAvatar(
                backgroundImage: new NetworkImage(
                  'http://thecatapi.com/api/images/get?format=src'
                    '&size=small&type=jpg#${title.hashCode}'
                ),
                radius: 20.0,
              ),
            ),
            new Expanded(
              child: new Container(
                child: new Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    new Text(title, style: textTheme.subhead),
                    new Text(author, style: textTheme.caption),
                  ],
                ),
              ),
            ),
            new Container(
              margin: new EdgeInsets.symmetric(horizontal: 5.0),
              child: new InkWell(
                child: new Icon(Icons.play_arrow, size: 40.0),
                onTap: () {
                  // TODO(implement)
                },
              ),
            ),
            new Container(
              margin: new EdgeInsets.symmetric(horizontal: 5.0),
              child: new InkWell(
                child: new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    new Icon(Icons.favorite, size: 25.0),
                    new Text('${likes ?? ''}'),
                  ],
                ),
                onTap: () {
                  // TODO(implement)
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class Feed extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ListView(
      children: [
        new Song(title: 'Trapadelic lobo', author: 'lillobobeats', likes: 4),
        new Song(title: 'Different', author: 'younglowkey', likes: 23),
        new Song(title: 'Future', author: 'younglowkey', likes: 2),
        new Song(title: 'ASAP', author: 'tha_producer808', likes: 13),
        new Song(title: '🌲🌲🌲', author: 'TraphousePeyton'),
        new Song(title: 'Something sweet...', author: '6ryan'),
        new Song(title: 'Sharpie', author: 'Fergie_6'),
      ],
    );
  }
}

class CustomTabBar extends AnimatedWidget implements PreferredSizeWidget {
  CustomTabBar({ this.pageController, this.pageNames })
    : super(listenable: pageController);

  final PageController pageController;
  final List<String> pageNames;

  @override
  final Size preferredSize = new Size(0.0, 40.0);

  @override
  Widget build(BuildContext context) {
    TextTheme textTheme = Theme
      .of(context)
      .textTheme;
    return new Container(
      height: 40.0,
      margin: const EdgeInsets.all(10.0),
      padding: const EdgeInsets.symmetric(horizontal: 20.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade800.withOpacity(0.5),
        borderRadius: new BorderRadius.circular(20.0),
      ),
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: new List.generate(pageNames.length, (int index) {
          return new InkWell(
            child: new Text(
              pageNames[index],
              style: textTheme.subhead.copyWith(
                color: Colors.white.withOpacity(
                  index == pageController.page ? 1.0 : 0.2,
                ),
              )
            ),
            onTap: () {
              pageController.animateToPage(
                index,
                curve: Curves.easeOut,
                duration: const Duration(milliseconds: 300),
              );
            }
          );
        })
          .toList(),
      ),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => new _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  PageController _pageController = new PageController(initialPage: 2);

  @override
  build(BuildContext context) {
    final Map<String, Widget> pages = <String, Widget>{
      'My Music': new Center(
        child: new Text('My Music not implemented'),
      ),
      'Shared': new Center(
        child: new Text('Shared not implemented'),
      ),
      'Feed': new Feed(),
    };
    TextTheme textTheme = Theme
      .of(context)
      .textTheme;
    return new Stack(
      children: [
        new Container(
          decoration: new BoxDecoration(
            gradient: new LinearGradient(
              begin: FractionalOffset.topCenter,
              end: FractionalOffset.bottomCenter,
              colors: [
                const Color.fromARGB(255, 253, 72, 72),
                const Color.fromARGB(255, 87, 97, 249),
              ],
              stops: [0.0, 1.0],
            )
          ),
          child: new Align(
            alignment: FractionalOffset.bottomCenter,
            child: new Container(
              padding: const EdgeInsets.all(10.0),
              child: new Text(
                'T I Z E',
                style: textTheme.headline.copyWith(
                  color: Colors.grey.shade800.withOpacity(0.8),
                  fontWeight: FontWeight.bold,
                ),
              ),
            )
          )
        ),
        new Scaffold(
          backgroundColor: const Color(0x00000000),
          appBar: new AppBar(
            backgroundColor: const Color(0x00000000),
            elevation: 0.0,
            leading: new Center(
              child: new ClipOval(
                child: new Image.network(
                  'http://i.imgur.com/TtNPTe0.jpg',
                ),
              ),
            ),
            actions: [
              new IconButton(
                icon: new Icon(Icons.add),
                onPressed: () {
                  // TODO: implement
                },
              ),
            ],
            title: const Text('tofu\'s songs'),
            bottom: new CustomTabBar(
              pageController: _pageController,
              pageNames: pages.keys.toList(),
            ),
          ),
          body: new PageView(
            controller: _pageController,
            children: pages.values.toList(),
          ),
        ),
      ],
    );
  }
}

Nota final: en este ejemplo, utilicé un estándar AppBar, pero también podrías usar un CustomScrollViewcon un pin SliverAppBarque tiene un elevation0.0. Eso haría que el contenido sea visible a medida que se desplaza detrás de la barra de la aplicación. Es complicado hacer que esto funcione bien PageView, porque se espera que se extienda un área de tamaño fijo.

Collin Jackson
fuente
No recomendaría omitir IntrinsicHeight ya que el usuario puede cambiar el tamaño de fuente y el diseño se puede romper fácilmente.
Lukasz Ciastko
23

Puede usar Stacky puede tener sus hijos como Positionedo Align.

Ejemplo # 1 (usandoPositionedenStack)

Stack(
  children: <Widget>[
    Positioned(left: 0.0, child: Text("Top\nleft")),
    Positioned(bottom: 0.0, child: Text("Bottom\nleft")),
    Positioned(top: 0.0, right: 0.0, child: Text("Top\nright")),
    Positioned(bottom: 0.0, right: 0.0, child: Text("Bottom\nright")),
    Positioned(bottom: 0.0, right: 0.0, child: Text("Bottom\nright")),
    Positioned(left: width / 2, top: height / 2, child: Text("Center")),
    Positioned(top: height / 2, child: Text("Center\nleft")),
    Positioned(top: height / 2, right: 0.0, child: Text("Center\nright")),
    Positioned(left: width / 2, child: Text("Center\ntop")),
    Positioned(left: width / 2, bottom: 0.0, child: Text("Center\nbottom")),
  ],
)

Ejemplo # 2 (usandoAlignenStack)

Stack(
  children: <Widget>[
    Align(alignment: Alignment.center, child: Text("Center"),),
    Align(alignment: Alignment.topRight, child: Text("Top\nRight"),),
    Align(alignment: Alignment.centerRight, child: Text("Center\nRight"),),
    Align(alignment: Alignment.bottomRight, child: Text("Bottom\nRight"),),
    Align(alignment: Alignment.topLeft, child: Text("Top\nLeft"),),
    Align(alignment: Alignment.centerLeft, child: Text("Center\nLeft"),),
    Align(alignment: Alignment.bottomLeft, child: Text("Bottom\nLeft"),),
    Align(alignment: Alignment.topCenter, child: Text("Top\nCenter"),),
    Align(alignment: Alignment.bottomCenter, child: Text("Bottom\nCenter"),),
    Align(alignment: Alignment(0.0, 0.5), child: Text("Custom\nPostition", style: TextStyle(color: Colors.red, fontSize: 20.0, fontWeight: FontWeight.w800),),),
  ],
);

Captura de pantalla:

ingrese la descripción de la imagen aquí

CopsOnRoad
fuente
1
Realmente útil, creo que lo que buscan la mayoría de los desarrolladores que vienen de Android es un diseño como el diseño de restricciones. ¿Hay algo así en Flutter?
user3833732
1
@ user3833732 Puedes lograr prácticamente cualquier cosa usando el widget incorporado de Flutter. Si tiene algún diseño y cree que no puede implementarlo usando Flutter, publíquelo como una pregunta y envíeme un mensaje, intentaré responderlo.
CopsOnRoad
3

Aquí hay otro ejemplo para mostrar cómo se puede usar Stackjunto con Positionedpara que funcione como RelativeLayout.

ingrese la descripción de la imagen aquí

double _containerHeight = 120, _imageHeight = 80, _iconTop = 44, _iconLeft = 12, _marginLeft = 110;

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.white,
    body: Stack(
      children: <Widget>[
        Positioned(
          left: 0,
          right: 0,
          height: _containerHeight,
          child: Container(color: Colors.blue),
        ),
        Positioned(
          left: _iconLeft,
          top: _iconTop,
          child: Icon(Icons.settings, color: Colors.white),
        ),
        Positioned(
          right: _iconLeft,
          top: _iconTop,
          child: Icon(Icons.bubble_chart, color: Colors.white),
        ),
        Positioned(
          left: _iconLeft,
          top: _containerHeight - _imageHeight / 2,
          child: ClipOval(child: Image.asset("assets/images/profile.jpg", fit: BoxFit.cover, height: _imageHeight, width: _imageHeight)),
        ),
        Positioned(
          left: _marginLeft,
          top: _containerHeight - (_imageHeight / 2.5),
          child: Text("CopsOnRoad", style: TextStyle(color: Colors.white, fontWeight: FontWeight.w500, fontSize: 18)),
        ),
        Positioned.fill(
          left: _marginLeft,
          top: _containerHeight + (_imageHeight / 4),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Column(
                children: <Widget>[
                  Text("2", style: TextStyle(fontWeight: FontWeight.bold)),
                  Text("Gold", style: TextStyle(color: Colors.grey)),
                ],
              ),
              Column(
                children: <Widget>[
                  Text("22", style: TextStyle(fontWeight: FontWeight.bold)),
                  Text("Silver", style: TextStyle(color: Colors.grey)),
                ],
              ),
              Column(
                children: <Widget>[
                  Text("28", style: TextStyle(fontWeight: FontWeight.bold)),
                  Text("Bronze", style: TextStyle(color: Colors.grey)),
                ],
              ),
              Container(),
            ],
          ),
        ),
      ],
    ),
  );
}
CopsOnRoad
fuente
1

Similar al de Android RelativeLayout(y de hecho más poderoso) es el AlignPositionedwidget del align_positionedpaquete:

https://pub.dev/packages/align_positioned

De sus documentos:

Cuando el diseño deseado se siente demasiado complejo para columnas y filas, AlignPositioned es un verdadero salvavidas. Flutter es muy componible, lo cual es bueno, pero a veces es innecesariamente complejo traducir algunos requisitos de diseño en una composición de widgets más simples.

AlignPositioned alinea, posiciona, dimensiona, gira y transforma a su hijo en relación con el contenedor y el hijo mismo. En otras palabras, le permite definir fácil y directamente dónde y cómo debe aparecer un widget en relación con otro.

Por ejemplo, puede decirle que coloque la parte superior izquierda de su hijo a 15 píxeles a la izquierda de la esquina superior izquierda del contenedor, además de que lo mueva dos tercios de la altura del niño hacia la parte inferior más 10 píxeles, y luego girar 15 grados. ¿Sabes siquiera cómo empezar a hacer esto componiendo widgets básicos de Flutter? Tal vez, pero con AlignPositioned es mucho más fácil y se necesita un solo widget.

Sin embargo, el ejemplo específico en la pregunta es bastante simple, solo usaría Rows, Columns , etc. de todos modos. Nota: soy el autor de este paquete.

MarcG
fuente
Esto es realmente lo que falta en Flutter.
user5381191