【问题标题】:Equivalent of RelativeLayout in FlutterFlutter中RelativeLayout的等价物
【发布时间】:2017-11-07 19:53:10
【问题描述】:

有没有办法实现类似于RelativeLayout 在 Android 上所做的事情?

特别是我正在寻找类似于 centerInParentlayout_below:<layout_id>layout_above:<layout_id>alignParentLeft 的内容

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

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

所以在上图中,“tofu's Songs”文本在RelativeLayout 内对齐为centerInParent。而另外两个是alignParentLeftalignParentRight

在每个单元格上,火图标所在的位置,其底部的点赞数围绕火焰图标的中心对齐。此外,每个单元格的顶部和底部标题分别与图像头像的右侧和顶部和底部对齐。

【问题讨论】:

    标签: android dart flutter android-relativelayout


    【解决方案1】:

    Flutter 布局通常使用 ColumnRowStack 小部件的树构建。这些小部件采用构造函数参数,指定子级相对于父级的布局规则,您还可以通过将它们包装在ExpandedFlexiblePositionedAlignCenter 小部件。

    也可以使用CustomMultiChildLayout 构建复杂的布局。这就是Scaffold 在内部实现的方式,如何在应用程序中使用它的示例出现在Shrine demo 中。您也可以使用LayoutBuilderCustomPaint,或者向下一层扩展RenderObject,如sector example 所示。像这样手动进行布局需要更多的工作,并且会在极端情况下产生更多错误的可能性,所以如果可以的话,我会尝试使用高级布局原语。

    回答您的具体问题:

    • 使用AppBarleadingtrailing 参数来定位您的应用栏元素。如果您想改用Row,请使用mainAxisAlignmentMainAxisAlignment.spaceBetween
    • 使用 RowcrossAxisAlignmentCrossAxisAlignment.center 将火图标和数字放在下方。
    • 使用ColumnmainAxisAlignmentMainAxisAlignment.spaceBetween 来定位您的顶部和底部标题。 (您应该考虑使用ListTile 来布置列表图块,但如果这样做,您将失去对精确定位的控制。)

    这是实现您提供的设计的代码 sn-p。在此示例中,我使用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,但您也可以使用带有固定 SliverAppBarCustomScrollView,其 elevation 为 0.0。这将使内容在您的应用栏后面滚动时可见。让它与PageView 很好地配合是很棘手的,因为它需要一个固定大小的区域来布局。

    【讨论】:

    • 我不建议省略 IntrinsicHeight,因为用户可以更改字体大小并且布局很容易中断。
    【解决方案2】:

    您可以使用Stack,并且可以将其子级设置为PositionedAlign

    示例 #1(在 Stack 中使用 Positioned

    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(在 Stack 中使用 Align

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

    截图:

    【讨论】:

    • 真的很有帮助,我认为大多数来自 Android 的开发者都在寻找像 Constraint Layout 这样的布局。颤振中有这样的东西吗?
    • @user3833732 使用 Flutter 内置小部件几乎可以实现任何目标。如果您有任何布局,并且您认为您无法使用 Flutter 实现它,请将其作为问题发布,并给我留言,我会尽力回答。
    • 我知道这是一篇旧帖子,但有人能解释一下“宽度”和“高度”的来源吗?你怎么知道父母的大小?
    • @JunInoue 使用MediaQuery.of(context).size.width/height.
    【解决方案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(),
                ],
              ),
            ),
          ],
        ),
      );
    }
    

    【讨论】:

      【解决方案4】:

      类似于 Android 的 RelativeLayout(实际上更强大)是来自 align_positioned 包的 AlignPositioned 小部件:

      https://pub.dev/packages/align_positioned

      来自其文档:

      当您想要的布局对于 Columns 和 Rows 来说过于复杂时, AlignPositioned 是一个真正的救星。 Flutter 非常可组合, 这很好,但有时翻译起来过于复杂 将一些布局要求整合到更简单的小部件的组合中。

      AlignPositioned 对齐、位置、大小、旋转和变换 它的孩子相对于容器和孩子本身。在 换句话说,它可以让您轻松直接地定义在哪里以及如何 小部件应该相对于另一个出现。

      例如,您可以告诉它将其子元素的左上角定位在 容器左上角左侧 15 像素,加上 将孩子身高的三分之二移到底部加上 10 个像素, 然后旋转 15 度。你甚至知道如何开始这样做吗 通过编写基本的 Flutter 小部件?也许,但使用 AlignPositioned 这要容易得多,而且只需要一个小部件。

      但是,问题中的具体示例非常简单,无论如何我都会使用Rows、Columns 等。 注意:我是这个包的作者。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-01-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-01-03
        • 2018-07-11
        相关资源
        最近更新 更多