Tương đương với RelativeLayout trong Flutter


84

Có cách nào để triển khai một cái gì đó tương tự như những gì RelativeLayouttrên Android không?

Đặc biệt tôi đang tìm kiếm một cái gì đó tương tự như centerInParent, layout_below:<layout_id>, layout_above:<layout_id>, vàalignParentLeft

Để tham khảo thêm về RelativeLayout: https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html

CHỈNH SỬA: đây là một ví dụ về bố cục dựa vào RelativeLayout

Ảnh chụp màn hình

Vì vậy, trong hình trên, ở trên cùng, văn bản "Bài hát của đậu phụ" được căn chỉnh như centerInParentbên trong a RelativeLayout. Trong khi 2 cái kia là alignParentLeftalignParentRight

Trên mỗi ô, nơi có biểu tượng ngọn lửa, số lượt thích ở dưới cùng của ô đó được căn chỉnh xung quanh tâm của biểu tượng ngọn lửa. Ngoài ra, tiêu đề trên cùng và dưới cùng của mỗi ô được căn chỉnh tương ứng ở bên phải và ở trên cùng và dưới cùng của hình đại diện.

Câu trả lời:


213

Bố trí Flutter thường được xây dựng sử dụng một cây Column, RowStackcác widget. Những vật dụng một vài tham số constructor mà xác định quy tắc cho cách những đứa trẻ được đặt ra liên quan đến các phụ huynh, và bạn cũng có thể ảnh hưởng đến bố cục của từng trẻ bằng cách gói chúng trong Expanded, Flexible, Positioned, Align, hoặc Centercác widget.

Nó cũng có thể xây dựng các bố cục phức tạp bằng cách sử dụng CustomMultiChildLayout. Đây là cách Scaffoldđược triển khai nội bộ và một ví dụ về cách sử dụng nó trong một ứng dụng sẽ xuất hiện trong bản demo của Shrine . Bạn cũng có thể sử dụng LayoutBuilderhoặc CustomPaint, hoặc đi xuống một lớp và mở rộng RenderObjectnhư trong ví dụ về ngành . Việc thực hiện các bố cục của bạn theo cách thủ công như thế này sẽ tốn nhiều công sức hơn và tạo ra nhiều khả năng xảy ra lỗi hơn trong các trường hợp ở góc, vì vậy tôi sẽ cố gắng xử lý các nguyên bản bố cục cấp cao nếu bạn có thể.

Để trả lời các câu hỏi cụ thể của bạn:

  • Sử dụng các đối số leadingtrailingđể AppBarđịnh vị các phần tử thanh ứng dụng của bạn. Nếu bạn muốn sử dụng một Rowthay thế, hãy sử dụng một mainAxisAlignmenttrong số MainAxisAlignment.spaceBetween.
  • Sử dụng Rowvới một crossAxisAlignmentsố CrossAxisAlignment.centervị trí biểu tượng lửa và số ở phía dưới.
  • Sử dụng Columnvới mainAxisAlignmentcác MainAxisAlignment.spaceBetweenvị trí hàng đầu của bạn và tiêu đề dưới. (Bạn nên cân nhắc sử dụng ListTileđể sắp xếp các ô danh sách, nhưng bạn sẽ mất quyền kiểm soát vị trí chính xác nếu làm điều này.)

Đây là đoạn mã triển khai thiết kế bạn đã cung cấp. Trong ví dụ này, tôi đã sử dụng một IntrinsicHeightđể xác định chiều cao của các ô bài hát, nhưng bạn có thể cải thiện hiệu suất bằng cách mã hóa chúng thành một chiều cao cố định.

ảnh chụp màn hình

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

Lưu ý cuối cùng: Trong ví dụ này, tôi đã sử dụng một thông thường AppBar, nhưng bạn cũng có thể sử dụng một CustomScrollViewvới một ghim SliverAppBarcó giá trị là elevation0,0. Điều đó sẽ làm cho nội dung hiển thị khi nó cuộn sau thanh ứng dụng của bạn. Thật khó để làm cho điều này trở nên độc đáo PageView, bởi vì nó mong đợi một khu vực có kích thước cố định để tự bố trí vào đó.


Tôi không khuyên bạn nên bỏ qua IntriualityHeight vì kích thước phông chữ có thể được thay đổi bởi người dùng và bố cục có thể dễ dàng bị phá vỡ.
Lukasz Ciastko

23

Bạn có thể sử dụng Stackvà có thể có con của nó là Positionedhoặc Align.

Ví dụ # 1 (Sử dụngPositionedtrongStack)

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

Ví dụ # 2 (Sử dụngAligntrongStack)

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

Ảnh chụp màn hình:

nhập mô tả hình ảnh ở đây


1
Thực sự hữu ích, tôi nghĩ những gì hầu hết các nhà phát triển đến từ Android đang tìm kiếm là một bố cục như Constraint Layout. Có điều gì như thế trong rung động?
user3833732

1
@ user3833732 Bạn có thể đạt được hầu hết mọi thứ bằng cách sử dụng tiện ích có sẵn Flutter. Nếu bạn có bất kỳ bố cục nào và bạn nghĩ rằng bạn không thể triển khai nó bằng Flutter, hãy đăng nó dưới dạng câu hỏi và nhắn tin cho tôi, tôi sẽ cố gắng trả lời.
CopsOnRoad

3

Đây là một ví dụ khác cho thấy cách dùng Stackcùng với Positionedcó thể làm cho nó hoạt động như thế nào RelativeLayout.

nhập mô tả hình ảnh ở đây

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

Tương tự như của Android RelativeLayout(và trên thực tế là mạnh hơn) là AlignPositionedtiện ích con từ align_positionedgói:

https://pub.dev/packages/align_positioned

Từ tài liệu của nó:

Khi bố cục mong muốn của bạn cảm thấy quá phức tạp đối với Cột và Hàng, AlignPositioned là một công cụ tiết kiệm trong cuộc sống thực. Flutter rất có thể kết hợp, điều này là tốt, nhưng đôi khi nó phức tạp không cần thiết khi chuyển một số yêu cầu về bố cục thành một thành phần của các widget đơn giản hơn.

AlignPositioned căn chỉnh, vị trí, kích thước, xoay và biến đổi con của nó liên quan đến cả vùng chứa và bản thân con. Nói cách khác, nó cho phép bạn dễ dàng và trực tiếp xác định vị trí và cách một tiện ích sẽ xuất hiện trong mối quan hệ với tiện ích khác.

Ví dụ: bạn có thể yêu cầu nó đặt vị trí trên cùng bên trái của con của nó là 15 pixel ở bên trái của góc trên cùng bên trái của vùng chứa, cộng với việc di chuyển nó hai phần ba chiều cao của con xuống dưới cùng cộng với 10 pixel, sau đó xoay 15 độ. Bạn có biết cách bắt đầu thực hiện việc này bằng cách soạn các widget Flutter cơ bản không? Có thể, nhưng với AlignPositioned thì việc này dễ dàng hơn nhiều và chỉ cần một widget duy nhất.

Tuy nhiên, ví dụ cụ thể trong câu hỏi khá đơn giản, tôi chỉ sử dụng Rows, Columns , v.v. Lưu ý: Tôi là tác giả của gói này.


Đây thực sự là những gì còn thiếu trong rung động.
user5381191
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.