Эквивалент RelativeLayout во Flutter

86

Есть ли способ реализовать что-то похожее на то, что RelativeLayoutесть на Android?

В частности , я ищу что - то подобное centerInParent, layout_below:<layout_id>, layout_above:<layout_id>иalignParentLeft

Дополнительные сведения о RelativeLayout: https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html.

РЕДАКТИРОВАТЬ: вот пример макета, основанного на RelativeLayout

Скриншот

Итак, на изображении выше текст «Песни тофу» выровнен так, как будто он находится centerInParentвнутри RelativeLayout. В то время как другие 2 alignParentLeftиalignParentRight

В каждой ячейке, где находится значок огня, количество лайков внизу выровнено по центру значка пламени. Кроме того, верхний и нижний заголовки в каждой ячейке выравниваются по правому краю, а также по верхнему и нижнему краям аватара изображения соответственно.

user3217522
источник

Ответы:

214

Флаттер макеты, как правило , построены с использованием дерева Column, Rowи Stackвиджетов. Эти виджеты принимают аргументы конструкторы , которые определяют правила для того, как дети раскладывают по отношению к родителю, и вы можете также влиять на расположение отдельных детей, окружив их Expanded, Flexible, Positioned, Alignили Centerвиджеты.

Также возможно создание сложных макетов с использованием CustomMultiChildLayout. Вот как Scaffoldэто реализовано внутри, и пример того, как использовать это в приложении, представлен в демонстрации Shrine . Вы также можете использовать LayoutBuilderили CustomPaint, или перейти на уровень ниже и расширить, RenderObjectкак показано в примере сектора . Выполнение ваших макетов вручную, как это, требует больше работы и создает больше возможностей для ошибок в крайних случаях, поэтому я бы попытался обойтись примитивами макета высокого уровня, если вы можете.

Чтобы ответить на ваши конкретные вопросы:

  • Используйте leadingи trailingаргументы , AppBarчтобы расположить штриховые элементы приложения. Если вы хотите использовать Rowвместо этого, используйте a mainAxisAlignmentof MainAxisAlignment.spaceBetween.
  • Используйте a Rowс a crossAxisAlignmentили CrossAxisAlignment.centerдля размещения значка огня и числа под ним.
  • Используйте a Columnс a mainAxisAlignmentили, MainAxisAlignment.spaceBetweenчтобы разместить верхний и нижний заголовок. (Вам следует подумать об использовании ListTileдля размещения плиток списка, но вы потеряете контроль над точным расположением, если сделаете это.)

Вот фрагмент кода, который реализует предоставленный вами дизайн. В этом примере я использовал IntrinsicHeightдля определения высоты фрагментов песни, но вы можете улучшить производительность, жестко установив для них фиксированную высоту.

Скриншот

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

Последнее примечание: в этом примере я использовал обычный AppBar, но вы также можете использовать a CustomScrollViewс закрепленным SliverAppBar, имеющим elevation0,0. Это сделает контент видимым при прокрутке за панелью вашего приложения. Сложно заставить это хорошо играть PageView, потому что ожидается, что область фиксированного размера будет располагаться в ней.

Коллин Джексон
источник
Я бы не рекомендовал опускать IntrinsicHeight, поскольку размер шрифта может быть изменен пользователем, а макет может легко сломаться.
Лукаш Частко
25

Вы можете использовать Stackи иметь его дочерние элементы как Positionedили Align.

Пример # 1 (ИспользованиеPositionedinStack)

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")),
  ],
)

Пример # 2 (ИспользованиеAligninStack)

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

Скриншот:

введите описание изображения здесь

CopsOnRoad
источник
1
Действительно полезно, я думаю, что большинство разработчиков Android ищут такой макет, как Constraint Layout. Есть ли что-нибудь подобное во флаттере?
user3833732
1
@ user3833732 Вы можете добиться чего угодно, используя встроенный виджет Flutter. Если у вас есть макет и вы думаете, что не можете реализовать его с помощью Flutter, опубликуйте его как вопрос и напишите мне, я постараюсь ответить на него.
CopsOnRoad
3

Вот еще один пример, показывающий, как можно использовать Stackвместе с Positioned, чтобы заставить его работать как RelativeLayout.

введите описание изображения здесь

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
источник
2

Похож на Android RelativeLayout(и на самом деле более мощный) AlignPositionedвиджет из align_positionedпакета:

https://pub.dev/packages/align_positioned

Из его документов:

Когда желаемый макет кажется слишком сложным для столбцов и строк, AlignPositioned действительно спасает жизнь. Flutter очень легко компонуем, что хорошо, но иногда излишне сложно преобразовать некоторые требования к макету в композицию более простых виджетов.

AlignPositioned выравнивает, позиционирует, размеры, вращает и трансформирует свой дочерний элемент по отношению как к контейнеру, так и к самому дочернему элементу. Другими словами, он позволяет вам легко и напрямую определять, где и как один виджет должен располагаться по отношению к другому.

Например, вы можете указать ему разместить левый верхний угол своего дочернего элемента на 15 пикселей слева от верхнего левого угла контейнера, плюс переместить его на две трети высоты дочернего элемента вниз плюс 10 пикселей, а затем повернуть на 15 градусов. Вы даже знаете, как начать делать это, составляя базовые виджеты Flutter? Возможно, но с AlignPositioned это намного проще, и для этого нужен единственный виджет.

Однако конкретный пример в вопросе довольно прост, я бы все равно использовал Rows, Columns и т. Д. Примечание: я являюсь автором этого пакета.

MarcG
источник