在这之前原来想学先布局的,但是发现布局之前还是有很多的 widget 不熟悉,就尝试了几种基础的 widget,特此记录。
但是个人认为还是要知道一些布局的知识的,不然看起来还是相对吃力的,毕竟widget相对细节很多,布局对于app界面的设计会重要一点。
widget在flutter中是一个非常重要的概念。Flutter 从 React 中吸取灵感,通过现代化框架创建出精美的组件。它的核心思想是用 widget 来构建你的 UI 界面。 Widget 描述了在当前的配置和状态下视图所应该呈现的样子。当 widget 的状态改变时,它会重新构建其描述(展示的 UI),框架则会对比前后变化的不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。
这么讲可能有点抽象,这里放一个例子HelloWorld
:
import 'package:flutter/material.dart';
void main() {
// runApp(const MyApp());
runApp(
const Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
效果如下:
可以看出,这里的runApp持有传入的widget树,这会被作为根widget,这个树内包含一个子widget树Center,Center下包含它的子widget(Text)。
此外,widget最重要的工作之一就是提供build()方法,这可以用更低级的widget的特性来渲染自己的特性。widgets还分有状态和无状态两种形式,无状态的widgets内所有的属性都是final性质的,这意味着它的属性不可改变;相对的,有状态的widgets的属性就可以在其生命周期内改变。那怎么区分有状态和无状态?有状态一般有两个类:
StatefulWidget
类,这个类本身不变State
类,这个类本身存在于整个生命周期中在flutter中,widget还是很丰富的,我们这里就挑最常用的几个讲,其他的详见Flutter官网。
Text
widget 可以用来在应用内创建带样式的文本。这里放上两个例子,第一个例子偏向于文本各个参数编辑,第二个是多行文字和渐变色。
注释写的满详细了,图是丑了点,但是基本的几个点都用到了。
import 'package:flutter/material.dart';
void main() {
// runApp(const MyApp());
// 变量
const String _abc = "Clark";
// Text1
runApp(
Text(
// 文本; 文本加入变量
'Hello, world! \n Hello, $_abc!',
// 位置:left, right, center, justify, start, end
textAlign: TextAlign.justify,
// 处理溢出文本:clip(剪掉溢出文本), fade(透明化溢出文本), ellipsis(省略号表溢出), visible(容器外的也可以渲染)
overflow: TextOverflow.fade,
// 文字方向:ltr左到右,rtl右到左
textDirection: TextDirection.ltr,
// 文字风格,这个就很多了。
// FontWeight.bold:加粗
// FontStyle.italic:斜体
// color: Colors.black.withOpacity(0.6):颜色与透明度 => 注意此时不能使用const Text
// fontFamily: 'Raleway':字体
// height: 5:列高
// fontSize: 18:大小
style: TextStyle(
color: Colors.yellow.withOpacity(0.6),
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
fontFamily: 'Raleway',
height: 5,
fontSize: 30,
),
// 可以在默认字体下修改
// style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0)
),
);
}
为了在一句话中显示不同样式的文字。这里主要强调一个RichText。
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() {
// Text2
runApp(
// 注意:RichText一定要一个textDirection
RichText(
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text: "Hello Clark",
style: TextStyle(color: Colors.white.withOpacity(0.2), fontSize: 60),
),
TextSpan(
text: "!!!!!\n",
style: TextStyle(
color: Colors.yellow.withOpacity(0.6),
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
fontFamily: 'Raleway',
height: 5,
fontSize: 30,
),
),
TextSpan(
text: "1234567898765432345678\n",
style: TextStyle(color: Colors.white.withOpacity(1.0)),
),
],
),
),
);
}
效果如下:
这里有点点超纲,有用到Stack的知识,但是问题不大。这里的stack就是为了让这俩字体合到一起,大致思路是写一个粗一点的蓝字,然后再给它stark一个细一点的白字。
// Text3
runApp(
Stack(
alignment: Alignment.center,
children: <Widget>[
// Stroked text as border.
Text(
textDirection: TextDirection.ltr,
'SCQ CXN FOREVER!!!!!',
style: TextStyle(
fontSize: 40,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6
..color = Colors.blue[700]!,
),
),
// Solid text as fill.
Text(
textDirection: TextDirection.ltr,
'SCQ CXN FOREVER!!!!!',
style: TextStyle(
fontSize: 40,
color: Colors.grey[300],
),
),
],
)
);
这个也有点超纲,用到了ShaderMask,既然做到了,就在这里讲一下。
这个东西有点像ps里的蒙板,这里面有三种方法:LinearGradient, RadialGradient, SweepGradient。第一种是线性变化,顺着一个方向渐变颜色,可以横着也可以斜着;第二种是辐射式的变化,有点像中间淡色外边深色的那种;第三种是通过设置开始角度和结束角度,设置渐变范围,按照角度设置。
看上去这仨没啥用,实际上还是非常好用的,在图片处理的时候总有起效。
当然我们这里就是想做个渐变色,上述的一大堆操作都没啥用,就是用了个LinearGradient。
import 'package:flutter/material.dart';
void main() {
// Text4
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blue,
),
home: Gredient(),
);
}
}
class Gredient extends StatelessWidget {
// const ({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Love Story'),
),
body: Center(
child: ShaderMask(shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Colors.white, Color(0xFFFFBDE9)],
).createShader(Offset.zero & bounds.size);
},
child: Text(
"YWH MJT FOREVER!!!",
style: TextStyle(
color: Colors.white,
fontSize: 50
),
),
)
)
);
}
}
这两个 flex widgets 可以让你在水平 ( Row ) 和垂直( Column ) 方向创建灵活的布局。它是基于 web 的 flexbox 布局模型设计的。
这俩我们不会像Text那样详细尝试,毕竟是个布局相关的组件,细节没多少。主要以Row为主。
顾名思义,Row就是为了横向排列的,以下图为例,我们完成了三个板块的横向排列。
代码如下,内含注释:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
// brightness: Brightness.dark,
primaryColor: Colors.yellowAccent,
),
home: TryRow(),
);
}
}
class TryRow extends StatelessWidget {
// const ({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Love Story'),
),
// 主体部分展示Row
body: Center(
child: Row(
// 这个仍然适用,这里右到左,显示顺序与代码顺序颠倒
textDirection: TextDirection.rtl,
children: const <Widget>[
Expanded(
child: Text('Deliver features faster', textAlign: TextAlign.center, style: TextStyle(fontSize: 20),),
),
Expanded(
child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
),
// Expanded的好处是不会存在溢出的情况,溢出部分将自己调整
// 如果是一个简单的const将会导致溢出的文字之类的超出空间
Expanded(
child: FittedBox(
child: FlutterLogo(),
),
),
],
)
)
);
}
}
row是横向分布的,那所有的元素就是沿着一条横着的主轴排列的,默认情况下,它们将整个轴布满,而轴一般来说是跟屏幕同宽或者同长,跟上一个案例一样。但是可以通过mainAxisSize设置轴的长度、mainAxisAlignment设置对齐关系、使用CrossAxisAlignment设置children在横轴中的定位 。column同理。
具体用法见代码:
Row(
// max, min
mainAxisSize: MainAxisSize.max,
// start, end; children从主轴起点(终点)开始对其
// center: children设置到主轴中心
// spaceBetween: children间平分额外空间
// spaceEvenly: children间,第一个children前最后一个children后评分额外空间
// spaceAround: spaceEvenly种第一个和最后一个children少一半空间
mainAxisAlignment: MainAxisAlignment.spaceAround,
// 这个仍然适用,这里右到左,显示顺序与代码顺序颠倒
textDirection: TextDirection.rtl,
// start, end, center, stretch, baseline(仅限Text类,并要求 textBaseline 属性设置为 TextBaseline.alphabetic)
crossAxisAlignment: CrossAxisAlignment.end,
children: const <Widget>[
Expanded(
child: Text('Deliver features faster', textAlign: TextAlign.center, style: TextStyle(fontSize: 20),),
),
Expanded(
child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
),
// Expanded的好处是不会存在溢出的情况,溢出部分将自己调整
// 如果是一个简单的const将会导致溢出的文字之类的超出空间
Expanded(
child: FittedBox(
child: FlutterLogo(),
),
),
],
)
一般来说是用flexible调整大小的,因为 widget 大小在设置flexible前不可变。
在losse时,方块大小取我们设定的50,出现tight则强制占据剩下的空间,若出现tight+flex的情况,则根据flex大小计算占据的空间。
Expanded widget 能够包裹一个 widget 并强制其填满剩余空间,与 Flexible 非常相似。
效果如下:
这个可以用来调整大小,也可以用来创造空间。
如果要造空位,Spacer比楼上更合适一点,它可以用flex,且放在children中效果优先级高于mainAxisAlignment。
插入图标的 widget,没什么注意点,会用就行。
放图片的控件,还是非常重要的。
在放图片前先配置下。先创建assets文件夹,并补上俩子文件夹用于存放图标和图片;再将这俩文件夹写入pubspec.yaml
里。
这边就介绍俩放图片的方法,一个是放网上图片的,一个是放项目图片的。
class TryImg extends StatelessWidget {
// const TryIcon({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Column(
children: [
// 网图
Flexible(
child: Image.network(
'https://pic2.zhimg.com/80/v2-7091ef428b0b3cd8570f6851e0a4e811_720w.webp',
),
fit: FlexFit.loose
),
// 资源图
Flexible(
child: Image.asset(
'assets/images/18.jpg'
),
fit: FlexFit.loose
),
],
);
}
}
效果如下:
顺便学习下Scaffold。
也有另一个版本,更需要布局这方面的知识
代码如下
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
),
home: Scaffold(
body: Center(
child: _Commodity()
),
// 悬浮符号
floatingActionButton:FloatingActionButton(
onPressed: (){
// 这里放点击后的事件
},
// 悬浮符号的符号
child: const Icon(Icons.send),
),
// 悬浮符号放中间
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// 侧拉部分导航
drawer: Drawer(
child: SafeArea(
child:Column( //column widget
children: const [
ListTile(
leading: Icon(Icons.home),
title: Text("Home Page"),
subtitle: Text("Subtitle menu 1"),
),
ListTile(
leading: Icon(Icons.search),
title: Text("Search Page"),
subtitle: Text("Subtitle menu 1"),
),
//put more menu items here
],
),
),
),
// 底部导航
bottomNavigationBar: BottomNavigationBar( //bottom navigation bar on scaffold
items: const [ //items inside navigation bar
BottomNavigationBarItem(
icon: Icon(Icons.add),
label: "Button 1",
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: "Button 2",
),
BottomNavigationBarItem(
icon: Icon(Icons.camera),
label: "Button 3",
),
// 要啥再写
],
),
),
);
}
}
class _Commodity extends StatefulWidget {
// const _Commodity({Key? key}) : super(key: key);
State<_Commodity> createState() => _CommodityState();
}
class _CommodityState extends State<_Commodity> {
// 存图片 + 文字
final List goods = [];
// 商品名字体
final _GoodsNameFont = const TextStyle(fontSize: 18);
// 商品简介字体
final _GoodsIntroduceFont = const TextStyle(fontSize: 18);
Widget build(BuildContext context) {
return Scaffold(
body: _buildFrame(),
);
}
Widget _buildFrame() {
final namelist = ["只因", "你", "太美", "迎面走来的你", "让我", "蠢蠢欲动", "就会", "爆炸"];
final introductionlist = ["小黑子露出鸡脚了吧,你食不食油饼", "你干嘛啊啊啊啊啊啊啊 哎呦!!!"];
return ListView.builder(
padding: const EdgeInsets.all(26.0),
// 对每一个商品都用itemBuilder
itemBuilder: (context, i) {
// 加分割线
if (i.isOdd) return const Divider();
i = i % (namelist.length * 2);
final index = i ~/ 2;
// 加货物
return _buildline(namelist[index], introductionlist[index % 2], index);
});
}
Widget _buildline(name, introduction, index) {
// 返回一个Card
// return Card(
// child: ListTile(
// leading: Image.asset("assets/images/"+ index.toString() +".jpg"),
// title: Text(name, style: _GoodsNameFont),
// subtitle: Text(introduction),
// trailing: Icon(Icons.more_vert),
// isThreeLine: true,
// ),
// );
// 也可以返回一个ListTile
return MyListTile(
thumbnail: Container(
child: Image.asset("assets/images/"+ index.toString() +".jpg"),
// 背景色
decoration: const BoxDecoration(color: Colors.pink),
),
// leading: Image.asset("assets/images/"+ index.toString() +".jpg"),
title: name,
subtitle: introduction,
author: 'Dash',
publishDate: 'Dec 28',
readDuration: '5 mins'
);
}
}
// 练布局用的,手写一个布局
class MyListTile extends StatelessWidget {
// 重构函数
const MyListTile({
super.key,
required this.thumbnail,
required this.title,
required this.subtitle,
required this.author,
required this.publishDate,
required this.readDuration,
});
final Widget thumbnail;
final String title;
final String subtitle;
final String author;
final String publishDate;
final String readDuration;
Widget build(BuildContext context) {
// 给一个padding,用来处理容器和组件之间的距离的。
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
// 定死每个栏目的高度
child: SizedBox(
height: 100,
// 先放方块(图片),再放文字堆叠
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
// 分两块,一块放图片,一块放_ArticleDescription => 那堆文字的合集
children: <Widget>[
AspectRatio(
// 长宽比
aspectRatio: 1.0,
child: thumbnail,
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(20.0, 0.0, 2.0, 0.0),
child: _ArticleDescription(
title: title,
subtitle: subtitle,
author: author,
publishDate: publishDate,
readDuration: readDuration,
),
),
),
],
),
),
);
}
}
// 那一堆文字的合集
class _ArticleDescription extends StatelessWidget {
const _ArticleDescription({
required this.title,
required this.subtitle,
required this.author,
required this.publishDate,
required this.readDuration,
});
final String title;
final String subtitle;
final String author;
final String publishDate;
final String readDuration;
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
// 放一小段空白
const Padding(padding: EdgeInsets.only(bottom: 2.0)),
Text(
subtitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14.0,
color: Colors.white70,
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(
author,
style: const TextStyle(
fontSize: 12.0,
color: Colors.white70,
),
),
Text(
'$publishDate - $readDuration',
style: const TextStyle(
fontSize: 12.0,
color: Colors.white70,
),
),
],
),
),
],
);
}
}
Container 这里跟 bootstrap 里面真的很像,在布局方面,个人感觉官网的这个图讲的还是比较清楚的,其实跟 html 大差不差,都是在调这些东西。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildImageColumn();
}
}
// Container框架
Widget _buildImageColumn() {
return Container(
height: 350,
decoration: const BoxDecoration(
color: Colors.white30,
),
child: Column(
children: [
_buildImageRow(1),
_buildImageRow(3),
],
),
);
}
Widget _buildDecoratedImage(int imageIndex) => Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 10, color: Colors.white70),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.all(4),
child: Image.asset('assets/images/$imageIndex.jpg'),
),
);
Widget _buildImageRow(int imageIndex) => Row(
children: [
_buildDecoratedImage(imageIndex),
_buildDecoratedImage(imageIndex + 1),
],
);
强调几个特点:
最基础的操作,顺便提一下 Theme.of(context).textTheme.headline n!
和 transform
。
代码如下
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildInline(context);
}
}
// 斜体 Container
Widget _buildInline(BuildContext context){
return Center(
child: Container(
// 调用主题标题 Theme.of(context).textTheme.headline4!
// 限制大小
constraints: BoxConstraints.expand(
height: Theme.of(context).textTheme.headline4!.fontSize! * 1.1 + 200.0,
),
padding: const EdgeInsets.all(8.0),
color: Colors.blue[600],
// 内容居中
alignment: Alignment.center,
// 旋转效果
transform: Matrix4.rotationZ(0.1),
child: Text('Hello World',
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white)),
)
);
}
这个就是外面的那个壳子。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildBox(context);
}
}
// box例子
Widget _buildBox(BuildContext context){
return Center(
child: Container(
height: Theme.of(context).textTheme.headline1!.fontSize! * 3.5,
decoration: BoxDecoration(
color: const Color(0xff7c94b6),
image: const DecorationImage(
// 这里跟正常的image操作有点不一样
image: AssetImage("assets/images/4.jpg"),
// BoxFit.fill:充满父容器,所以会变形、拉伸
// BoxFit.contain:尽可能大,保持图片分辨率,适应宽度或长度,会有留白
// BoxFit.fitWidth:图片填满宽度,高度可能会被截断
// BoxFit.fitHeight:图片填满高度,宽度可能会被截断
// BoxFit.cover:充满容器,可能会被截断
fit: BoxFit.fitHeight,
),
border: Border.all(
width: 8,
),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
// 这几个参数自己搞,我是真没感觉,会搞就好了
BoxShadow(
color: Colors.purple,
offset: Offset(5.0, 5.0),
blurRadius: 10.0,
spreadRadius: 2.0,
),
BoxShadow(
color: Colors.white,
offset: Offset(100.0, 50.0),
blurRadius: 200.0,
spreadRadius: 10.0,
),
],
),
)
);
}
这个倒是跟正常安卓程序的网格布局大差不差。它将 widget 作为二维列表展示,提供两个预制的列表,或者你可以自定义网格。当检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。最经典的网格布局的案例就是相册了:
几个注意点:
尝试一个相册实例,注意图片的标题操作和网格操作的几个参数,不熟悉面向对象编程的读者还可以注意下面向对象的类创建与使用。这段代码是我从 gallery 上扒下来的,稍作修改。类似的样式可以复现。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
body: GridGallery(type: GridListDemoType.header),
)
);
}
}
// 一共三种模式可供选择
enum GridListDemoType {
imageOnly,
header,
footer,
}
// 网格
class GridGallery extends StatelessWidget {
// const GridGallery({Key? key}) : super(key: key);
const GridGallery({super.key, required this.type});
final String Title = "你干嘛啊啊啊啊啊~~~";
final String Subtitle = "小鸡子露出黑脚了吧,你食不食油饼,我抱井惹";
final GridListDemoType type;
// 存一个类的列表
List<_Photo> _photos(BuildContext context) {
return [
_Photo(assetName: "assets/images/1.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/2.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/3.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/4.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/5.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/6.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/7.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/8.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/9.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/10.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/11.jpg", title: Title, subtitle: Subtitle),
_Photo(assetName: "assets/images/0.jpg", title: Title, subtitle: Subtitle),
];
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text("< 我家giegie"),
),
body: GridView.count(
restorationId: 'grid_view_demo_grid_offset',
//
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
// 格子之间的padding
padding: const EdgeInsets.all(8),
// 圆角
childAspectRatio: 1,
// 一整个输出
children: _photos(context).map<Widget>((photo) {
return _GridDemoPhotoItem(
photo: photo,
tileStyle: type,
);
}).toList(),
),
bottomNavigationBar: BottomNavigationBar( //bottom navigation bar on scaffold
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.collections),
label: "图库",
),
BottomNavigationBarItem(
icon: Icon(Icons.collections_bookmark),
label: "为您推荐",
),
BottomNavigationBarItem(
icon: Icon(Icons.photo_library),
label: "相簿",
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: "搜索",
),
],
// 被选中图标颜色
fixedColor: Colors.greenAccent,
// 底部背景色
backgroundColor: Colors.white30,
// 显示所有图标的label
showUnselectedLabels: true,
// 未选中文字颜色
unselectedItemColor:Colors.white70,
// 阴影长度
elevation: 20,
)
);
}
}
// 定义类
class _Photo {
_Photo({
required this.assetName,
required this.title,
required this.subtitle,
});
final String assetName;
final String title;
final String subtitle;
}
// 细节操作:调整图片大小适应窗口
class _GridTitleText extends StatelessWidget {
// 初始化
const _GridTitleText(this.text);
final String text;
Widget build(BuildContext context) {
return FittedBox(
// 适应大小
fit: BoxFit.scaleDown,
// 对齐方向:
// bottomCenter bottomEnd bottomStart 底部居中,底部结尾,底部开始
// center centerEnd centerStart
// topCenter topEnd topStart
alignment: AlignmentDirectional.centerStart,
child: Text(text),
);
}
}
class _GridDemoPhotoItem extends StatelessWidget {
const _GridDemoPhotoItem({
required this.photo,
required this.tileStyle,
});
final _Photo photo;
final GridListDemoType tileStyle;
Widget build(BuildContext context) {
final Widget image = Semantics(
label: '${photo.title} ${photo.subtitle}',
child: Material(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
clipBehavior: Clip.antiAlias,
child: Image.asset(
photo.assetName,
// package: 'flutter_gallery_assets',
fit: BoxFit.cover,
),
),
);
switch (tileStyle) {
case GridListDemoType.imageOnly:
return image;
case GridListDemoType.header:
return GridTile(
header: Material(
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
),
clipBehavior: Clip.antiAlias,
child: GridTileBar(
title: _GridTitleText(photo.title),
backgroundColor: Colors.black45,
),
),
child: image,
);
case GridListDemoType.footer:
return GridTile(
footer: Material(
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(4)),
),
clipBehavior: Clip.antiAlias,
child: GridTileBar(
backgroundColor: Colors.black45,
title: _GridTitleText(photo.title),
subtitle: _GridTitleText(photo.subtitle),
),
),
child: image,
);
}
}
}
有点像被定义好的下拉列表,好用的。一般常与 Divider 分割线联用。
布局那篇博客涉及了此部分内容,这里不重复,放一个例子:
代码如下
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.light,
// primaryColor: Colors.yellowAccent,
),
home: ColumnListView(),
);
}
}
class ColumnListView extends StatelessWidget {
const ColumnListView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final List<String> entries = <String>['A', 'B', 'C', 'D', 'E', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'];
final List<int> colorCodes = <int>[600, 500, 400, 300, 200, 100];
return Scaffold(
body: Column(
children: [
Column(
children: [
Text("1111111111111111111111111111111111111111111111111111111111111111"),
Text("222222"),
],
),
ListView.separated(
// 这个参数绝对不能省略
shrinkWrap: true,
padding: const EdgeInsets.all(8),
// 自带的迭代器,可以实现循环类似的功能
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
color: Colors.amber[colorCodes[index]],
child: Center(child: Text('Entry ${entries[index]}')),
);
},
// 分割符号
separatorBuilder: (BuildContext context, int index) => const Divider(),
// 提前告诉ListView下有几个items
itemCount: entries.length
),
]
),
);
}
}
堆叠。顾名思义,就是把几个控件放到一起,有点像PS里各个图层叠到一起,因此有些妙用。
最经典的案例就是头像,在图片上套一个圆形的边框就好了:
几个注意点:
简单实现一个:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
// body: GridGallery(type: GridListDemoType.header),
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildStack();
}
}
// 头像
Widget _buildStack() {
return Center(
child: Stack(
alignment: const Alignment(0.6, 0.6),
children: [
const CircleAvatar(
backgroundImage: AssetImage('assets/images/2.jpg'),
radius: 100,
),
Container(
decoration: const BoxDecoration(
color: Colors.black45,
),
child: const Text(
'Olsen',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
)
);
}
有一说一这玩意还是被好用的。
注意点:
这边尝试探索下 Card 的几个参数,做一个列表,因为可能会在之后的大作业中用到:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
// body: GridGallery(type: GridListDemoType.header),
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildCard2();
}
}
// Card
Widget _buildCard2() {
return Center(
child: MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('Card Examples')),
body: Column(
children: const <Widget>[
Spacer(),
ElevatedCardExample(),
FilledCardExample(),
OutlinedCardExample(),
MyCardExample(),
Spacer(),
],
),
),
),
);
}
class ElevatedCardExample extends StatelessWidget {
const ElevatedCardExample({super.key});
Widget build(BuildContext context) {
return const Center(
child: Card(
// Card 没大小,要靠里面的东西撑起来
child: SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Elevated Card')),
),
),
);
}
}
class FilledCardExample extends StatelessWidget {
const FilledCardExample({super.key});
Widget build(BuildContext context) {
return Center(
child: Card(
// 阴影
elevation: 1,
color: Theme.of(context).colorScheme.surfaceVariant,
child: const SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Filled Card')),
),
),
);
}
}
class OutlinedCardExample extends StatelessWidget {
const OutlinedCardExample({super.key});
Widget build(BuildContext context) {
return Center(
child: Card(
elevation: 0,
// 边框
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
// 圆角
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: const SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Outlined Card')),
),
),
);
}
}
class MyCardExample extends StatelessWidget {
const MyCardExample({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: Card(
child: SizedBox(
height: 500,
width: 300,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(5, 5, 5, 0),
child: Container(
width: 280,
height: 190,
// child: Image.asset("assets/images/3.jpg"),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage('assets/images/3.jpg')),
borderRadius:const BorderRadius.all(Radius.circular(10))
)
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(20, 20, 6, 0),
child: Text("Headline", style: TextStyle(fontSize: Theme.of(context).textTheme.headline4!.fontSize))
),
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
child: Text("SubHead", style: TextStyle(fontSize: Theme.of(context).textTheme.headline6!.fontSize))
),
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
child: Text("Supporting text Supporting text Supporting text Supporting text Supporting text", style: TextStyle(fontSize: Theme.of(context).textTheme.bodyText1!.fontSize))
),
SizedBox(
height: 30,
),
ButtonBar(
children: <Widget>[
FloatingActionButton.extended(
onPressed: () {},
backgroundColor: Colors.purple.shade100,
icon: const Icon(Icons.save),
label: const Text("Buy"),
),
FloatingActionButton.extended(
onPressed: () {},
backgroundColor: Colors.purple.shade100,
icon: const Icon(Icons.save),
label: const Text("Save"),
)
]
)
],
),
],
),
)
),
);
}
}
图片 + 介绍卡片,也不错,可能会在大作业中用到,商品介绍之类的里面可能会有用,加个图片轮播就变成淘宝复现了。
import 'package:flutter/material.dart';
// 图片轮播的包
import 'package:flutter_swiper/flutter_swiper.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
// body: GridGallery(type: GridListDemoType.header),
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildCard2();
}
}
// Card
Widget _buildCard2() {
return Center(
child: MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('Card Examples')),
body: Column(
children: <Widget>[
Spacer(),
ElevatedCardExample(),
FilledCardExample(),
OutlinedCardExample(),
MyCardExample(),
Spacer(),
],
),
),
),
);
}
class ElevatedCardExample extends StatelessWidget {
const ElevatedCardExample({super.key});
Widget build(BuildContext context) {
return const Center(
child: Card(
// Card 没大小,要靠里面的东西撑起来
child: SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Elevated Card')),
),
),
);
}
}
class FilledCardExample extends StatelessWidget {
const FilledCardExample({super.key});
Widget build(BuildContext context) {
return Center(
child: Card(
// 阴影
elevation: 1,
color: Theme.of(context).colorScheme.surfaceVariant,
child: const SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Filled Card')),
),
),
);
}
}
class OutlinedCardExample extends StatelessWidget {
const OutlinedCardExample({super.key});
Widget build(BuildContext context) {
return Center(
child: Card(
elevation: 0,
// 边框
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
// 圆角
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: const SizedBox(
width: 300,
height: 75,
child: Center(child: Text('Outlined Card')),
),
),
);
}
}
class MyCardExample extends StatelessWidget {
// const MyCardExample({Key? key}) : super(key: key);
List<Map> imageList = [
{
"asset":"assets/images/8.jpg"
},
{
"asset":"assets/images/2.jpg"
},
{
"asset":"assets/images/3.jpg"
},
{
"asset":"assets/images/4.jpg"
}
];
Widget build(BuildContext context) {
return Center(
child: Card(
shape: RoundedRectangleBorder(//设置圆角
borderRadius: BorderRadius.circular(5),
),
child: SizedBox(
height: 500,
width: 300,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(5, 5, 5, 0),
child: Container(
width: 300,
height: 190,
// child: Image.asset("assets/images/3.jpg"),
child: Container(
decoration: const BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(5), topRight: Radius.circular(5)),
),
child: Swiper(
itemBuilder: (BuildContext context, int index){
return Image.asset(imageList[index]["asset"], fit: BoxFit.fill,);
},
itemCount: imageList.length,
pagination: SwiperPagination(),
control: SwiperControl(),
loop: true,
autoplay: true,
),
),
// decoration: BoxDecoration(
// image: DecorationImage(image: AssetImage('assets/images/3.jpg')),
// borderRadius:const BorderRadius.all(Radius.circular(10))
// )
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(0, 20, 6, 0),
child: ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage("assets/images/ji.jpg"),
),
title: Text("只因你们太美", style: TextStyle(fontSize: Theme.of(context).textTheme.headline4!.fontSize)),
)
),
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
child: Text("就会爆炸", style: TextStyle(fontSize: Theme.of(context).textTheme.headline6!.fontSize))
),
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
child: Text("只因你实在是太美 baby 只因你太美 baby 迎面走来的你让我如此蠢蠢欲动 这种感觉我从未有", style: TextStyle(fontSize: Theme.of(context).textTheme.bodyText1!.fontSize))
),
// SizedBox(
// height: 30,
// ),
Align(
alignment: Alignment.bottomRight,
child:
ButtonBar(
children: <Widget>[
FloatingActionButton.extended(
onPressed: () {},
backgroundColor: Colors.purple.shade100,
icon: const Icon(Icons.save),
label: const Text("Buy"),
),
FloatingActionButton.extended(
onPressed: () {},
backgroundColor: Colors.purple.shade100,
icon: const Icon(Icons.save),
label: const Text("Save"),
)
]
)
)
],
),
],
),
)
),
);
}
}
这个已经被多次使用了,最经典的案例就是个人名片,因为这个自带 heading 放头像, title, subtitle, text 这种可以放不同层级内容的部分。
代码如下
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Love Story',
theme: ThemeData(
brightness: Brightness.dark,
// primaryColor: Colors.yellowAccent,
),
home: const Scaffold(
// body: GridGallery(type: GridListDemoType.header),
body: layout(),
)
);
}
}
class layout extends StatelessWidget {
const layout({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return _buildCard();
}
}
// Card
Widget _buildCard() {
return Center(
child: SizedBox(
height: 210,
child: Card(
child: Column(
children: [
ListTile(
title: const Text(
'1625 Main Street',
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: const Text('My City, CA 99984'),
leading: Icon(
Icons.restaurant_menu,
color: Colors.blue[500],
),
),
const Divider(),
ListTile(
title: const Text(
'(408) 555-1212',
style: TextStyle(fontWeight: FontWeight.w500),
),
leading: Icon(
Icons.contact_phone,
color: Colors.blue[500],
),
),
ListTile(
title: const Text('[email protected]'),
leading: Icon(
Icons.contact_mail,
color: Colors.blue[500],
),
),
],
),
),
)
);
}