相当于Flutter中的RelativeLayout


84

有没有办法实现类似于RelativeLayoutAndroid上的功能?

特别是我在寻找类似的东西来centerInParentlayout_below:<layout_id>layout_above:<layout_id>,和alignParentLeft

有关RelativeLayout的更多参考:https : //developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html

编辑:这是一个依赖于布局的示例 RelativeLayout

屏幕截图

因此,在上图中的顶部,“豆腐的歌曲”文本与centerInParent内对齐RelativeLayout。而其他两个是alignParentLeftalignParentRight

在每个带有火图标的单元格上,其底部的点赞次数围绕着火图标的中心对齐。同样,每个单元格的顶部和底部标题分别与图像头像的右侧和顶部和底部对齐。

Answers:


213

扑布局正在使用的树通常建立ColumnRowStack小部件。这些部件采取的孩子是如何相对于父布局指定规则构造函数的参数,你也可以通过在包装他们影响个别儿童的布局ExpandedFlexiblePositionedAlign,或Center部件。

也可以使用构建复杂的布局CustomMultiChildLayout。这是Scaffold内部实现的方式,Shrine演示中显示了如何在应用程序中使用它的示例。您也可以使用LayoutBuilderCustomPaint,或向下一层进行扩展RenderObject,如扇区示例所示。像这样手动进行布局是一项更多的工作,并且在出现极端情况时更容易出错,因此,如果可以的话,我将尝试使用高级布局基元。

要回答您的特定问题:

  • 使用leadingtrailing参数来AppBar放置应用栏元素。如果要改用a Row,请使用mainAxisAlignmentMainAxisAlignment.spaceBetween
  • 使用Row带有的crossAxisAlignmentofCrossAxisAlignment.center将火图标和数字放置在下面。
  • 使用Column带有mainAxisAlignmentof的aMainAxisAlignment.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,但你也可以使用CustomScrollView一个钉SliverAppBar有一个elevation0.0。当内容滚动到您的应用栏后面时,这将使内容可见。PageView使它与完美配合是很难的,因为它期望将固定大小的区域布置在其中。


我不建议省略IntrinsicHeight,因为用户可以更改字体大小并且布局很容易中断。
Lukasz Ciastko

23

您可以使用Stack,也可以将其子作为PositionedAlign

Example#1Positioned在中使用Stack

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

Example#2Align在中使用Stack

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

屏幕截图:

在此处输入图片说明


1
确实有帮助,我认为大多数来自Android的开发人员正在寻找的是Constraint Layout之类的布局。颤动中有类似的东西吗?
user3833732'1

1
@ user3833732使用Flutter内置窗口小部件几乎可以实现任何目的。如果您有任何布局,并且认为您无法使用Flutter来实现它,请将其作为问题发布,并给我发送消息,我将尝试回答。
CopsOnRoad

3

这是另一个示例,说明如何StackPositioned一起使它像一样工作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(),
            ],
          ),
        ),
      ],
    ),
  );
}

1

与Android类似RelativeLayout(并且实际上更强大)的是AlignPositionedalign_positioned包中的小部件:

https://pub.dev/packages/align_positioned

从其文档:

当您想要的布局对于“列”和“行”而言过于复杂时,AlignPositioned就是一个真正的节省程序。Flutter具有很好的可组合性,这很好,但是有时将一些布局要求转换为更简单的小部件的组成不必要地复杂。

AlignPositioned相对于容器和子项本身对齐,定位,调整大小,旋转和变换其子项。换句话说,它使您可以轻松,直接地定义一个小部件相对于另一个应出现的位置和方式。

例如,您可以告诉它将其子项的左上角放置在容器左上角左方15像素处,然后将其子项的高度的三分之二移到底部再加上10像素,然后旋转15度。您甚至不知道如何通过编写基本的Flutter小部件来开始执行此操作?也许可以,但是使用AlignPositioned会容易得多,并且只需要一个小部件。

但是,问题中的特定示例非常简单,无论如何我只会使用Rows,Columns等。注意:我是该软件包的作者。


这确实是颤振中所缺少的。
user5381191
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.