Bố trí Flutter thường được xây dựng sử dụng một cây Column
, Row
và Stack
cá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 Center
cá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 LayoutBuilder
hoặc CustomPaint
, hoặc đi xuống một lớp và mở rộng RenderObject
như 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ố
leading
và trailing
để 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 Row
thay thế, hãy sử dụng một mainAxisAlignment
trong số MainAxisAlignment.spaceBetween
.
- Sử dụng
Row
với một crossAxisAlignment
số CrossAxisAlignment.center
vị trí biểu tượng lửa và số ở phía dưới.
- Sử dụng
Column
với mainAxisAlignment
các MainAxisAlignment.spaceBetween
vị 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.
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: () {
},
),
),
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: () {
},
),
),
],
),
),
);
}
}
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: () {
},
),
],
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 CustomScrollView
với một ghim SliverAppBar
có giá trị là elevation
0,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 đó.