flutter基础学习.md
Flutter基础
学习来源:《Flutter实战·第二版》
介绍
Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。
技术特点:
- 跨平台自绘引擎。底层使用 Skia 作为其 2D 渲染引擎。
- 高性能;
- dart开发; 开发效率高、 高性能、 快速内存分配、 类型安全和空安全。
Flutter
框架图:
安装
# 使用archlinuxcn源安装
yay -S flutter
# 追加flutter组
sudo gpasswd -a {user} flutterusers
# 重载配置
source /etc/profile
# 更换组
newgrp flutterusers
# 其他
# 配置仓库 (加入环境变量)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# 检查
flutter doctor
运行
# 1. 创建新项目
flutter create hello
# 2. 运行
flutter run
Dart语言
变量声明
var
关键字
// var被赋值之后,类型将确定,不可再更改类型
var t = 'hello'; // t的类型为String
t = 100; // 错误
dynamic
和Object
// Object是所有对象的根基类。
// 所以任何数据类型都可以赋值给Object对象
dynamic t;
Object c;
t = 'as'; c = 'world';
t = 100; c = 200; // 正确
// 两者不同之处在于 dynamic可以使用所有可能的方法。而 Object只能使用Object的方法
dynamic a = '';
Object b = '';
// 正常
print(a.length);
// 报错 The getter 'length' is not defined for the class 'Object'
print(b.length);
final
和const
// final和const都是常量,不可变
// 区别在于 const是编译期初始化。而final为运行是初始化
空安全
int i = 9; // 默认不为空,定义时必须初始化
int j?; // 定义可为空类型,可以不初始化
// 预期变量之后初始化,但在定义时无法确定值,可以使用late (编译器提示,要求使用前必须初始化)
late int k;
k = 9;
// 可以显式指定已经初始化完成,使用`!`
int i?;
if(i!=null) {
print(i! * 8); //因为已经判过空,所以能走到这 i 必不为null,如果没有显式申明,则 IDE 会报错
}
// 如果函数变量可空时,调用的时候可以用语法糖
fun?.call();
函数
// 函数声明
bool isNobel(int a) {
return a > 0;
}
// 不指定返回类型,将当作 dynamic处理
isNobel(int a) {
return a > 0;
}
typedef bool CALLBACK();
void test(CALLBACK b) {
print(b());
}
test(isNobel); // 错误:类型不一致
// 简写
bool isNobel(int a) => true;
// 函数作为变量
var say = (str) {
print(str);
};
say("asda");
// 函数作为参数
void exe(CALLBACK cb) {
cb();
}
// 可选的位置参数
String say(String f, String msg, [String? device]) {
var result = '$f says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
// 可选的命名参数
void enable({bool display, bool hidden}) {
// ...
}
enable(bold: true, hidden: true);
mixin
组合
dart不支持多继承。使用mixin
进行组合。类似interface
。\
如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。
class Person {
say() {
print('say');
}
}
mixin Eat {
eat() {
print('eat');
}
}
mixin Walk {
walk() {
print('walk');
}
}
mixin Code {
code() {
print('key');
}
}
class Dog with Eat, Walk{}
class Man extends Person with Eat, Walk, Code{}
异步支持
Future
与JavaScript中的Promise
非常相似。
// Future.then
Future.delayed(Duration(seconds: 2), (){
return 'hi';
}).then((data) {
print(data)
});
// Future.catchError
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}).then((data){
print("success");
}).catchError((e){
print(e);
});
// then函数有个onError错误参数来捕获异常
Future.delayed(Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
// whenComplete 无论成功失败,都做这个事情
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
// Future.wait 等待事件完成之后,再运行之后代码
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
// async 和 await 只是个Future的语法糖
Stream
Stream
也可以进行接收异步事件,但它可以同时接收多个异步操作的结果。
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
/*
结果为:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
*/
基本介绍
Widget
flutter
中几乎所有的对象都是一个 widget
。用来表示组件
和功能
。
widget
是描述一个UI元素的配置信息。
StatelessWidget
和StatefulWidget
继承了Widget
。
flutter
框架的的处理流程是这样的:
- 根据
widget
树生成Element
树。Element
树的节点都继承Element
类; - 根据
Element
树生成Render
树,渲染树的节点都继承RenderObject
类; - 根据
Render
树生成Layer
树,然后显式,Layer
树的节点都继承Layer
类。
StatelessWidget
StatelessWidget
用于不需要维护状态的场景。在build
方法中嵌套其他widget
来构建UI
。
StatefulWidget
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
在StatefulWidget 中,State 对象和StatefulElement
具有一一对应的关系。当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例。
State
一个 StatefulWidget 类会对应一个 State 类。State
信息可以:
- 在
widget
被构建时同步读取; - 调用
setState
通知重新构建widgte
树。
State
有两个常用属性:
widget
:表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置State. widget 为新的 widget 实例。context
:StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。
状态管理
管理状态的最常见的方法:
-
Widget 管理自己的状态。
-
Widget 管理子 Widget 状态。
-
混合管理(父 Widget 和子 Widget 都管理状态)。
-
如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
-
如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
-
如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。
自身管理
import 'package:flutter/material.dart';
void main() {
runApp(const TapboxA());
}
class TapboxA extends StatefulWidget {
const TapboxA({super.key});
@override
_TapboxAState createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
debugPrint("active: $_active");
}
@override
Widget build(BuildContext context) {
final tb = TextButton(
onPressed: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightBlue[700] : Colors.grey[600]),
child: Center(
child: Text(
_active ? "active" : "inactive",
style: const TextStyle(fontSize: 32.0, color: Colors.white),
textDirection: TextDirection.ltr,
),
),
),
);
return Directionality(textDirection: TextDirection.ltr, child: tb);
}
}
父Widget管理子Widget
import 'package:flutter/material.dart';
void main() {
runApp(const ParentWidget());
}
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTap(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: TapBoxB(active: _active, onChanged: _handleTap));
}
}
class TapBoxB extends StatelessWidget {
final bool active;
final ValueChanged<bool> onChanged;
const TapBoxB({Key? key, this.active = false, required this.onChanged})
: super(key: key);
void _handleTap() {
onChanged(!active);
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightBlue[700] : Colors.grey[600]),
child: Center(
child: Text(
active ? "Active" : "Inactive",
style: const TextStyle(fontSize: 32.0, color: Colors.white),
),
),
));
}
}
路由管理
路由在移动开发中通常指页面。
简单路由:
// 路由
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("New Route"),
),
body: const Center(
child: Text("新页面"),
));
}
}
// 导航代码
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return NewRoute();
}));
},
child: const Text("跳转新路由")
)
MaterialPageRoute
MaterialPageRoute
继承PageRoute
类。PageRoute
定义路由构建及切换时过度动画相关接口及属性。
MaterialPageRoute({
WidgetBuilder builder, // 回调函数。构建路由页面的具体内容,返回值是一个widget
RouteSettings settings, // 包含路由的配置信息,如路由名称、是否初始路由
bool maintainState = true, // 表示原来的路由是否被保存。如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false
bool fullscreenDialog = false, // 表示新的路由页面是否是一个全屏的模态对话框
})
Navigator
Navigator
是一个路由管理的组件。
- Future push(BuildContext context, Route route) :将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。
- bool pop(BuildContext context, [ result ]): 将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。
路由传值
// 新页面
class TipRoute extends StatelessWidget {
final String text;
const TipRoute({super.key, required this.text});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("提示"),
),
body: Padding(
padding: const EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(text),
ElevatedButton(
onPressed: () => Navigator.pop(context, "返回值是我"), // 弹出时,在这里返回参数
child: const Text("返回"),
)
],
),
),
),
);
}
}
class RouterTestRoute extends StatelessWidget {
const RouterTestRoute({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () async {
var result = await Navigator.push(context, // 异步等待返回信息
MaterialPageRoute(builder: (context) {
return const TipRoute(text: "传入123"); // 构造参数
}));
debugPrint("$result");
},
child: const Text("打开新页面"),
),
);
}
}
命名路由
使用路由表
进行命名路由管理。
// 1. 路由表
Map<String, WidgeBuilder> routes;
// 2. 注册路由表
MaterialApp (
// .....
routes: {
"new_paget" : (context) => NewRoute(),
"/" : (context) => MyHome() // 注册路由首页
}
);
// 3. 打开路由
Navigator.pushNamed(context, "new_page");
// 4. 获取路由参数
var args = ModalRoute.of(context).settings.arguments;
路由钩子
MaterialApp(
// ..........
onGenerateRoute: (RouteSettings settings) { // 仅对命名路由生效
return MaterialPageRoute(builder: (context) {
String routeName = settings.name;
// 判读
return xxx();
});
}
)
包管理
flutter
的项目默认配置为: pubspec.yaml
可以在https://pub.dev
查找依赖。
name: study1 # 项目名称
description: A new Flutter project. # 项目描述
publish_to: 'none' # 阻止使用 flutter pub publish
version: 1.0.0+1 # 版本
environment:
sdk: '>=3.0.3 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter: # flutter相关配置
uses-material-design: true
资源管理
Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。
使用pubspec.yaml
文件来管理应用程序所需的资源。
flutter:
assets:
- assets/icon1.png # flutter会加载相邻子目录的同命文件。如果有资源 assets/a/icon1.png,它也会被加载
- assets/icon2.png
加载assets
-
加载文本资源
- 使用
rootBundle
访问。位于package:flutter/services.dart
- 使用
DefaultAssetBundle
:用于运行时动态替换不同的AssetBundle
- 使用
-
加载图片资源
-
声明分辨率相关的图片
- .../2.0x/icon.png .../3.0x/icon.png
-
加载图片
AssetImage("assets/background.png"); Image.asset("assets/background.png"); // 返回widget // 使用默认asset bundle,内部会自动处理分辨率
-
调试
-
debugger()
debugger(when a > 20)
-
debug
debugPrint
flutter logs
-
assert
异常捕获
dart
为单线程,出现异常后不会导致进程的退出。
dart有两个队列:
- 微任务队列: 优先级别高。主要来源于dart内部。
- 事件队列:所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等。,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡。可以在事件队列中插入新的微任务和事件任务。
-
flutter
框架异常捕获:flutter
框架对build
添加了异常捕获。所以build
出错后会弹出错误提示:@override void performRebuild() { ... try { //执行build方法 built = build(); } catch (e, stack) { // 有异常时则弹出错误提示 built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); } ... } static void reportError(FlutterErrorDetails details) { ... if (onError != null) onError(details); //调用了onError回调 } // 自己上报异常,只需要提供一个自定义的错误处理回调即可 FlutterError.onError = (FlutterErrorDetails details) { reportError(details); };
-
其他异常捕获与日志收集
-
同步异常:使用
try
、catch
-
异步异常:使用
runZone()
的handleUncaughtError
runZoned( () => runApp(MyApp()), zoneSpecification: ZoneSpecification( // 拦截print print: (Zone self, ZoneDelegate parent, Zone zone, String line) { parent.print(zone, "Interceptor: $line"); }, // 拦截未处理的异步错误 handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { parent.print(zone, '${error.toString()} $stackTrace'); }, ), );
-
布局类组件
布局原理和约束
尺寸限制类用于限制容器大小。比如ConstrainedBox
、SizedBox
、UnconstrainedBox
、AspectRatio
等。
布局模型
- 基于
RenderBox
的盒模型布局 - 基于
Sliver
(RenderSliver
)按需加载列表布局
两者细节略有不同,但大体一致。布局流程如下:
- 上层向下层组件传递约束条件(
BoxConstraints
) - 下层确定自己的大小,然后通知上层。下层必须符合上层的约束
- 上层组件确定下层组件相对于自身的偏移和确定自身的大小
ConstrainedBox
用于对子组件添加约束。如果想让子组件最小高度为80,则使用const ConstraintsBox(minHeight: 80.0)
@override
Widget build(BuildContext context) {
Widget redBox = DecoratedBox(decoration: BoxDecoration(color: Colors.red));
return Column(
children: [
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 50.0,
minHeight: 50.0
),
child: Container(
height: 5.0, // 即使指定高度为5,由于约束,高度实际为50
child: redBox,
),
)
],
);
当多重限制时,取父组件数值较大的值。
SizedBox
用于给子元素指定固定的宽高。
SizedBox(
width: 20.0,
height: 20.0,
child: redBox,
)
// SizedBox由BoxConstraints实现
UnconstrainedBox
UnconstrainedBox
可以”去除“约束。
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),
child: UnconstrainedBox( // '去除'限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: redBox,
),
),
)
实际上,子组件不可能违反父组件的约束(如果违反父组件的约束,则无法绘制)。
线性布局
沿水平或垂直排列子组件的布局。使用Row
和Column
来实现线性布局(两者均继承Flex
)。
- 主轴: 布局沿水平方向 主轴对齐
MainAxisAlignment
- 纵轴:布局沿垂直方向 纵轴对齐
CrossAxisAlignment
Row
的定义:
Row({
// ...
TextDirection textDirection, // 文字方向。默认为从左往右
MainAxisSize mainAxisSzie = MainAxisSize.max, // Row在主轴方向占用的空间。默认是max,即尽可能多,不管子组件宽度,Row的宽度始终等于水平方向的最大宽度。min表示尽可能少的占用空间,此时Row的宽度等于所有子组件的宽度
MainAxisAligment mainAxisAligment = MainAxisAligment.start //表示子组件在Row内的水平空间对齐方式。只有size为max时才有意义。start表示沿textDirection的初始方向对齐。center表示居中对齐
VerticalDirection verticalDirection = VerticalDirection.down // 表示Row纵轴的对齐方向,默认为从上到下
CrossAxisAligment crossAxisAligment = CrossAxisAligment.center, // 表示子组件在纵轴的对齐方式。Row的高度为子组件最高的高度。当verticalDirection为down时,start表示顶部对齐。当verticalDirection为up时,start表示底部对齐
List<Widget> children = const <Widget>[],
})
Column
的定义同Row
当Row
里面嵌套Row
,或者Column
里面嵌套Column
时,只有外面的Row
或Column
会占用尽可能大的空间。里面的为实际大小。可以使用Expanded
组件抵销此影响
Flex 弹性布局
Flex
组件可以沿着水平或垂直方向排列子组件。Row
和Column
都继承Flex
,参数基本相同。
// Flex定义
Flex({
// ........
required this.direction, // 默认为水平方向
children: const <Widget>[]
})
Expanded
只能作为Flex
的孩子。可以按照比例扩展子组件占用的空间。
// Expanded定义
Expanded({
int flex = 1, child
})
流式布局
Wrap
用于折行
// wrap定义
Wrap({
// ...
direction = Axis.horizontal,
alignment = start,
spacing = 0.0, // 主轴方向子Widget的间距
runAlignment = start, // 纵轴方向的对齐方式
runSpacing = 0.0, // 纵轴方向的间距
crossAxisAlignment = start,
textDirection,
certicalDirection = down,
children
})
Flow
层叠布局
层叠布局与Web中的绝对定位相似。子组件可以根据父容器的四个角位置来确定自身位置。
// Stack定义
Stack({
alignment = topStart, // 决定没有定位 或部分定位的子组件
textDirection,
fit = losse, // 没有定位的子组件任何适应Stack的大小。loose表示使用子组件的大小,expand表示扩展到Stack的大小
clipBehavior = hardEdge, // 决定超出stack的如何裁剪。hardEdge表示直接裁剪,不使用抗抗锯齿
children
});
// Positioned
Positioned({
left, // 表示距离stack左、上、右、底四边的距离
top,
right,
bottom,
width, // left right width 只能使用2个,其余一个会计算出来
height, // 用于配合top bottom的
child
})
对齐和相对定位Align
// Align定义
Align({
alignment: center, // 表示子组件在父组件的启始位置
widthFactor, // 用于确定Align组件本身宽高的属性:乘以子组件的宽高,就是Align组件的宽高
heightFactor,
child
})
容器类组件
容器类组件和布局类Widget都作用于其子Widget,不同的是:
- 布局类子组件为children,容器类子组件为child
- 布局类组件对其子组件进行排列,容器类组件对其子组件进行包装。
Padding填充
// Padding
Padding({
//...
padding, // 定义填充的方法
child
})
DecoratedBox 装饰容器
// DecoratedBox
DecoratedBox({
decoration, // 代表要绘制的装饰
position = DecorationPosition.background, // 决定在哪里装饰,默认为背景。foreground为前景
child
})
// BoxDecoration定义
BoxDecoration({
color, // 颜色
image, // 图片
border, // 边框
borderRadius, // 圆角
boxShadow, // 阴影
gradient, // 渐变
backgroundBlendMode, // 背景混合模式
shape = BoxShape.rectangle // 形状
});
// 示例
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.red, Colors.orange.shade200]), // 背景渐变
boxShadow: [
BoxShadow(
color: Colors.black87,
offset: Offset(2.0, 2.0),
blurRadius: 4.0
)
]
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
child: Text("Login", style: TextStyle(color: Colors.white)),
),
);
}
Tranform变换
Transform
可以实现子组件绘制时的特效。Matrix4
时一个4D矩阵。
// 变换
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Transform(
alignment: Alignment.topLeft, // 相对于原点的对齐方式
transform: Matrix4.skewY(0.3), // 沿Y轴倾斜0.3的弧度
child: Container(
padding: const EdgeInsets.all(8.0),
color: Colors.deepPurple,
child: Text("dsaas"),
),
),
);
}
// 平移
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.red,
),
child: Transform.translate(
offset: Offset(-20.0, -5.0),
child: Text('hello'),
),
);
}
// 旋转
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.red,
),
child: Transform.rotate(
angle: pi / 2 ,
child: Text('hello'),
),
);
// 缩放
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.red,
),
child: Transform.scale(
scale: 2.0,
child: Text('hello'),
),
);
}
Container容器
Container
组件是DecpratedBox
、ConstrainedBox
、Transform
、Padding
、Align
等组件的一个多功能容器。
// container定义
Container({
this.alignment,
this.padding,
color,
decoration,
foregroundDecoration,
width,
height,
constraints,
margin,
transform,
child
})
Clip裁剪
裁剪Widget | 默认行为 |
---|---|
ClipOval | 子组件为正方形时裁剪为内贴圆形;为矩形时裁剪为内贴椭圆 |
ClipRRect | 将子组件裁剪为圆角矩形 |
ClipRect | 默认裁剪掉子组件布局外的绘制内容 |
ClipPath | 按照自定义路径裁剪 |
Widget build(BuildContext context) {
Widget avatar = Image.network('xxx', width: 60);
return Column(
children: [
avatar, // 不裁剪
ClipOval(child: avatar,), // 裁剪为圆形
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: avatar,
), // 裁剪为圆角矩形
],
);
}
空间适配 FittedBox
遇到子组件大小超过父组件时,父组件会将自身最大空间作为约束传递给子组件。当子组件原始大小超过父组件的约束区域,则需要进行一些缩放、裁剪或其他处理。不同组件的处理方式是特定的。Text
组件默认为换行。如果我们想改变此行为,需要使用FittedBox
。本质为子组件如何适配父组件的空间。
// FittedBox定义
FittedBox({
fit = BoxFit.contain, // 适配方式
alignment = Alignment.center, // 对齐方式
clipBehavior = Clip.none, // 是否裁剪
child
})
页面骨架 Scaffold
可滚动组件
简介
Flutter
有两种布局模型:
- 基于
RenderBox
的盒模型布局 - 基于
Sliver
按需加载列表布局
Sliver
模型是按需加载模型,只有在viewport中才会加载。ListView
和GridView
都是。
Scrollable
:用于处理滑动手势、确定滑动偏移ViewPort
:显示的视窗Sliver
:视窗里显示的元素
// Scrollable
Scrollable({
// ...
axisDirection = AxisDirection.down, // 滚动方向
controller, // 控制滚动位置及监听滚动事件
physics, // 定义如何响应用户的操作
viewportBuilder // 构建Viewport的回调
});
// Viewport
Viewport({
axisDirection - AxisDirection.down,
crossAxisDirection,
anchor = 0.0,
offset, // 用户的滚动偏移
center,
cacheExtent, // 预渲染区域
cacheExtentStyle = CacheExtentStyle.pixel, // 描述cacheExtent的含义
clipBehavior = Clip.hardEdge,
slivers
});
SingleChildScrollView
只能接收一个子组件。它没有延迟加载模型,所以只应当在期望的内容不会超出屏幕太多时使用。
SingleChildScrollView({
scrollDirection = Axis.vertical, //滚动方向,默认是垂直
reverse = false,
padding,
primary,
physics,
controllrt,
child
})
Widget build(BuildContext context) {
String s = "ahsdksadhksahdkashdskahdakhdak";
return Scrollbar( // 显示进度条
child: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Center(
child: Column(
children: s.split("").map((e)=> Text(e, textScaleFactor: 2.0,)).toList(),
),
),
),
);
}
ListView
ListView
可以沿一个方向线性排列所有子组件,也支持懒加载。
// ListView定义
ListView({
// ...
scrollDirection = Axis.vertical, // 滚动方向
reverse = false,
controller,
bool primary,
physics,
padding,
itemExtent, // 参数不为null时,强制children的‘长度’为此值。指定它会有更好的性能,因为不需要构建 子组件时再去计算
prototypeItem, // 如果知道列表每一项长度都相同,但不知道具体长度,可以指定它。滚动组件会在layout时,计算它的长度。所以和itemExtent性能一样好,但两者互斥。
shrinkWrap = false, // 是否根据使所有子组件长度设置ListView的长度。默认会尽可能占用更多的空间。当滚动方向无边界时,必须设置为true。
addAutomaticKeepAlives = true,
addRepaintBoundaries = true, // 是否包含在repaint组件中,此组件重绘开销特别小。
cacheExtent, // 预渲染区域长度
children // 适合少量的子组件数量。否则,应该使用ListView.builder
})
Widget build(BuildContext context) {
List<Text> texts = "ahsdksadhksahdkashdskahdasssssssssssssssssssssssssssssssssssssskhdak".split("")
.map((e) => Text(e)).toList();
return Scrollbar(child:ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(10.0),
children: texts,
));
}
// 默认为懒加载,但使用children仍然要提前创建Widget
ListViewBuilder
// ListView.builder
ListView.builder({
// ...
IndexedWidgetBuilder itemBuilder, // 列表的构建项,返回具体index项时的子组件
int itemCount, // 列表项的数量
// ....
})
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.builder(
itemCount: 50,
itemExtent: 50,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
),
);
ListView.separated
可以在列表项之间添加一个分割组件
Widget build(BuildContext context) {
Widget divider1 = Divider(color: Colors.blue);
Widget divider2 = Divider(color: Colors.red);
return Scrollbar(
child: ListView.separated(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"));
},
separatorBuilder: (context, index) {
return index%2 == 0? divider1 : divider2;
},
),
);
滚动监听及控制
可以使用ScrollController
来控制可滚动组件的滚动位置。
ScrollController
// ScrollController
ScrollController({
initialScrollOffest = 0.0,// 初始滚动位置
keepScrollOffset = true
});
offset // 可滚动组件的当前位置
jumpTo() // 无动画
animateTo() // 动画滚动
_MyScrollState createState() => _MyScrollState();
}
class _MyScrollState extends State<MyScroll> {
ScrollController _controller = ScrollController();
bool showBtn = false;
@override
void initState() {
super.initState();
// 监听滚动事件,打印滚动位置
_controller.addListener(() {
print(_controller.offset);
if (_controller.offset < 1000 && showBtn) {
setState(() {
showBtn = false;
});
} else if (_controller.offset >= 1000 && showBtn == false) {
setState(() {
showBtn = true;
});
}
});
}
@override
void dispose() {
// 销毁
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("滚动控制"),),
body: Scrollbar(
child: ListView.builder(
itemCount: 150,
itemExtent: 50.0,
controller: _controller,
itemBuilder: (cobtext, index) {
return ListTile(title: Text("现在是$index"),);
},
),
),
floatingActionButton: !showBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(
.0,
duration: Duration(seconds: 1),
curve: Curves.ease,
);
},
),
) ;
}
}
PageStorage
是一个保存页面相关数据 的组件,并不会参与UI。每次滚动结束,可滚动组件都将滚动位置存储至PostStorage
中。
bitsdojo_window
功能:自定义窗口大小及位置。
使用:flutter pub add bitsdojo_window
// main.dart
import 'package:flutter/material.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
void main() {
// 处理原生与flutter通信
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
doWhenWindowReady(() {
const initialSize = Size(800, 600);
appWindow.minSize = initialSize; // 最小窗口
appWindow.size = initialSize; // 默认窗口
appWindow.maxSize = const Size(800, 800); // 最大窗口
appWindow.alignment = Alignment.topLeft; // 打开位置
appWindow.show();
});
}
自定义窗口导航栏:
https://pub.dev/packages/bitsdojo_window#for-linux-apps
import 'package:flutter/material.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
class WindowButtons extends StatefulWidget {
const WindowButtons({super.key});
@override
State<WindowButtons> createState() => _WindowButtonsState();
}
class _WindowButtonsState extends State<WindowButtons> {
final buttonColors = WindowButtonColors(
iconNormal: Colors.grey[600],
mouseOver: Colors.grey[400],
mouseDown: Colors.grey[400],
iconMouseOver: Colors.grey[600],
iconMouseDown: Colors.grey[600]
);
void maximizeOrRestore() {
appWindow.maximizeOrRestore();
}
@override
Widget build(BuildContext context) {
return Row(children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: MinimizeWindowButton(colors: buttonColors),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: appWindow.isMaximized
? RestoreWindowButton(
colors: buttonColors,
onPressed: maximizeOrRestore,
)
: MaximizeWindowButton(
colors: buttonColors,
onPressed: maximizeOrRestore,
),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: CloseWindowButton(colors: buttonColors),
)
],);
}
}