Odpowiednik RelativeLayout w Flutter

84

Czy istnieje sposób na zaimplementowanie czegoś podobnego do tego, co RelativeLayoutrobi się na Androidzie?

W szczególności szukam czegoś podobnego do centerInParent, layout_below:<layout_id>, layout_above:<layout_id>, ialignParentLeft

Więcej informacji na temat RelativeLayout: https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html

EDYCJA: oto przykład układu, na którym się opieram RelativeLayout

Zrzut ekranu

Tak więc na powyższym obrazku, na górze, tekst „tofu's Songs” jest wyrównany jak centerInParentwewnątrz RelativeLayout. Podczas gdy pozostałe 2 to alignParentLeftialignParentRight

W każdej komórce, w której znajduje się ikona ognia, liczba polubień na dole jest wyrównana wokół środka ikony płomienia. Ponadto górny i dolny tytuł każdej komórki są wyrównane odpowiednio do prawej oraz do góry i dołu awatara obrazu.

user3217522
źródło

Odpowiedzi:

213

Trzepotanie układy są zwykle budowane za pomocą drzewa Column, Rowi Stackwidgety. Te wzory przyjąć argumenty konstruktora, które określają zasady jak dzieci są określone w stosunku do rodziców, a także można wpływać na rozmieszczenie poszczególnych dzieci przez owijanie ich w Expanded, Flexible, Positioned, Align, lub Centerwidgetów.

Możliwe jest również budowanie złożonych układów przy użyciu CustomMultiChildLayout. Tak jest Scaffoldzaimplementowany wewnętrznie, a przykład użycia go w aplikacji pojawia się w wersji demonstracyjnej Shrine . Możesz także użyć LayoutBuilderlub CustomPaint, lub zejść w dół warstwy i rozszerzyć, RenderObjectjak pokazano na przykładzie sektora . Wykonywanie układów ręcznie, jak to, wymaga więcej pracy i stwarza więcej potencjalnych błędów w przypadkach narożnych, więc jeśli możesz, spróbuję poradzić sobie z prymitywami układu wysokiego poziomu.

Aby odpowiedzieć na Twoje pytania:

  • Użyj argumentów leadingi, aby ustawić elementy paska aplikacji. Jeśli chcesz użyć zamiast tego, użyj z .trailingAppBarRowmainAxisAlignmentMainAxisAlignment.spaceBetween
  • Użyj z Rowz crossAxisAlignmentlub, CrossAxisAlignment.centeraby umieścić ikonę ognia i liczbę pod spodem.
  • Użyj z Columnz mainAxisAlignmentlub, MainAxisAlignment.spaceBetweenaby ustawić tytuł na górze i na dole. (Powinieneś rozważyć użycie ListTiledo rozłożenia kafelków listy, ale stracisz kontrolę nad dokładnym pozycjonowaniem, jeśli to zrobisz.)

Oto fragment kodu, który implementuje dostarczony przez Ciebie projekt. W tym przykładzie użyłem an IntrinsicHeightdo określenia wysokości kafelków utworu, ale możesz poprawić wydajność, zakodując je na stałe do stałej wysokości.

zrzut ekranu

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

Uwaga końcowa: w tym przykładzie użyłem zwykłego AppBar, ale możesz też użyć CustomScrollViewz przypiętą, SliverAppBarktóra ma elevation0,0. Dzięki temu treść byłaby widoczna, gdy przewija się za paskiem aplikacji. Trudno jest sprawić, by gra była przyjemna PageView, ponieważ oczekuje się, że obszar o stałym rozmiarze będzie się układał.

Collin Jackson
źródło
Nie radziłbym pomijać IntrinsicHeight, ponieważ użytkownik może zmienić rozmiar czcionki, a układ może się łatwo zepsuć.
Łukasz Ciastko
23

Możesz używać Stacki mieć swoje elementy podrzędne jako Positionedlub Align.

Przykład nr 1 (używaniePositionedwStack)

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

Przykład nr 2 (używanieAlignwStack)

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

Zrzut ekranu:

wprowadź opis obrazu tutaj

CopsOnRoad
źródło
1
Naprawdę pomocny, myślę, że to, czego szuka większość programistów wywodzących się z Androida, to układ taki jak Constraint Layout. Czy jest coś takiego w trzepotaniu?
user3833732
1
@ user3833732 Dzięki wbudowanemu widgetowi Flutter możesz osiągnąć praktycznie wszystko. Jeśli masz jakiś układ i uważasz, że nie jesteś w stanie go zaimplementować za pomocą Fluttera, zamieść to jako pytanie i napisz do mnie, spróbuję odpowiedzieć.
CopsOnRoad
3

Oto kolejny przykład pokazujący, jak Stackwraz z Positionedmożna wykorzystać, aby działał RelativeLayout.

wprowadź opis obrazu tutaj

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
źródło
1

Podobny do Androida RelativeLayout(a właściwie mocniejszy) jest AlignPositionedwidget z align_positionedpakietu:

https://pub.dev/packages/align_positioned

Z jego dokumentów:

Gdy żądany układ wydaje się zbyt złożony dla kolumn i wierszy, funkcja AlignPositioned to prawdziwy ratunek dla życia. Flutter można bardzo komponować, co jest dobre, ale czasami niepotrzebnie skomplikowane jest przetłumaczenie niektórych wymagań dotyczących układu na kompozycję prostszych widżetów.

AlignPositioned wyrównuje, pozycjonuje, zmienia rozmiar, obraca i przekształca jego element podrzędny zarówno w odniesieniu do kontenera, jak i samego elementu podrzędnego. Innymi słowy, pozwala łatwo i bezpośrednio określić, gdzie i jak widżet powinien wyglądać w stosunku do innego.

Na przykład możesz powiedzieć mu, aby umieścił lewy górny róg swojego dziecka w odległości 15 pikseli na lewo od lewego górnego rogu kontenera, a także przesunął go o dwie trzecie wzrostu dziecka w dół plus 10 pikseli, a następnie obrócić o 15 stopni. Czy w ogóle wiesz, jak zacząć to robić, komponując podstawowe widżety Fluttera? Może, ale dzięki AlignPositioned jest to znacznie łatwiejsze i wymaga jednego widżetu.

Jednak konkretny przykład w pytaniu jest dość prosty, i tak użyłbym po prostu Rows, Columns itp. Uwaga: jestem autorem tego pakietu.

MarcG
źródło
Właśnie tego brakuje w trzepotaniu.
user5381191