线程模型
Flutter
是运行Engine
上来实现跨平台的,Dart
支持通过isolate
实现异步处理的逻辑。但是线程的管理和创建并不是有Engine
负责,而是交由Embedder
去实现,除了线程之外Embedder
还负责了事件循环、平台插件等相关逻辑,Embedder
就是Flutter
和运行平台之间的中间层架构,他们的关系如下图:
在Flutter Engine
中需要运行四个任务,分别是:UI Task Runner
、Platform Task Runner
、GPU Task Runner
、I/O Task Runner
,这四个任务运行在不同的线程之上。
UI Task Runner
UI Task Runner其实就是Flutter的UI运行任务,也就是我们所说的UI线程,因为UI Task Runner会执行的Dart的root isolate去齐总运行App main方法,并且为其绑定了UI渲染提交的回调等。这也是为什么称Flutter是单线程应用的原因,这里指的就是UI Task Runner。UI Task Runner除了处理UI之外还处理了microTasks、插件消息的响应处理等相关逻辑。
谷歌官方的UI渲染绘制流程:
GPU Task Runner
当UI Task Runner生成Layer Tree之后,接下来的处理就会给GPU Task Runner处理,GPU Task Runner会把Layer Tree转化为Skia所需要的绘制指令,通过延迟调度和Buffer来保证绘制任务的流程运行,下图就是GPU的整个流程
I/O Task Runner
I/O Task Runner
就是顾名思义具备读写能力线程,比如图片数据的获取、解析成渲染数据等耗时操作,这些都属于耗时操作,交给其他三个Task Runner
处理明显不合适,所以对于需要耗时的操作,一般都是通过对I/O Task Runner
处理。
Platform Task Runner
Platform Task Runner
是Flutter Engine
的主线程,在Flutter
中所有和Engine调用都会通过Platform Task Runner
,之所以这样子做是为了保证线程安全。但是也带来了一个弊端,某个任务的处理造成了严重堵塞的时候,可能会引发应用的ANR奔溃。
那么对与一些耗时的操作,我们应该放到哪里去处理呢?答案就是isolate
isolate
isolate
运行我们开辟一个线程,由于isolate
的特性(数据不能互通),所以也不要锁。在实际开发中比如json
等一些耗时操作我们就可以使用isolate
去处理。
isolate
对比传统的线程最大区别是:
- 数据不共享
- 只能通过
port
进行通信 - 每个
isolate
都有自己的内存和任务管理
单线程运行
之说以说Flutter是单线程应用,是因为Dart
数据单线程运行机制,而这个机制主要是通过消息循环机制和任务调度处理,其中有两个任务队列:microTask queue
和Event queue
。
其中microTask
优先级高于Event queue
,从下面的运行流程图就可以很明显的看出。
实际开发中也要注意由于microTask
的特性(优先级高),如果频繁把任务插入到microTask
中去执行,就可能会造成UI卡顿和掉帧的现象。
[图片上传失败...(image-ff934f-1616915674307)]
async/await、Future
通常我们使用async/await
和Future
来实现异步的操作,但实际上这并不是真正的异步,而是在单线程上的任务调度,也称之为协程。协程是不具备线程一样并发执行的能力。简单的说就是当程序执行被标注了async
方法,运行到await
的时候,表示这个方法需要等待结果,此时就会跳过这个片段代码继续执行其他逻辑,然后在程序在下一次轮询的时候,在判断是否有了返回结果,如果就执行,没有就继续等待下一个轮询。
比如我们要在StatefulWidget
中的initState
方法中通过context
获取一些数据,如果我们直接使用,就会抛出异常,但是我们使用Future.delay(duration: Duration(seconds: 0)).then(() {...})
方法包装起来之后,则不会出现该问题。这是因为获取context
的逻辑会被放到下一个轮询中 被执行。
动画
我们知道,动画其实是由帧构成的,每一帧都是一张图片,多张图片连续执行,就形成了动画。
普通动画
之前我们说过Widget
是不可变的,那么Widget
是如何产生动画的呢?
我们用一个示例来看看,一个简单的放大动画的实现。
示例代码
class AnimationControllerPage extends StatefulWidget {
@override
_AnimationControllerPageState createState() =>
_AnimationControllerPageState();
}
class _AnimationControllerPageState extends State<AnimationControllerPage>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
double size = 100;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
lowerBound: 100,
upperBound: 200,
duration: Duration(milliseconds: 500),
);
_animationController.addListener(() {
setState(() {
size = _animationController.value;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AnimationController"),
),
body: Center(
child: InkWell(
onTap: () {
if (_animationController.value == 200) {
_animationController.reverse();
} else {
_animationController.forward();
}
},
child: Container(
width: size,
height: size,
color: Colors.yellow,
child: Center(
child: Text(_animationController.value == 200 ? "点我缩小" : "点我放大"),
),
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
实现分析
当我们要实现一个从100→200的矩形变大动画的时候,最重要的就是每一帧都让这个矩形变大一点,每一帧变大多少,就通过动画总时长和矩形大小的比(不考虑Curve),然后得到每一帧的宽高值,在把值设置给矩形。
所以我们需要三个条件:
- 可以更新
- 并且是每一帧都会有回调
- 能帮我们计算没帧的宽高值的对象
分析上面的代码
-
首先这个是
StatefulWidget
这里使用它的关键地方就是他可以使用
setState
进行跟新UI
操作 -
其次我们混入了
SingleTickerProviderStateMixin
提供了每一帧的绘制回调
Tick
-
然后我们使用了
AnimationController
通过
TickerProviderStateMixin
每一帧的回调,在使用AnimationController
内部的simulation
计算单位时间内value
的值,最后通过setState
进行更新
内部_tick
的源码示例:
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
assert(elapsedInSeconds >= 0.0);
_value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation!.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}
最后我们发现上面使用的是SingleTickerProviderStateMixin
,如果我们要在一个页面使用多个动画的时候,只需要把SingleTickerProviderStateMixin
换成TickerProviderStateMixin
。
至于具体的动画案例,后续出对应的文章。
路由动画
路由动画指的就是我们在页面进行切换的时候两个页面展示的动画。系统默认给我们实现了一套,代码在theme的pageTransitionTheme设置。我们可以在这里进行自定义,同时也可以在push的时候自定义PageRouterBuilder,在设置transitionsBuilder设置动画效果。
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Animation Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
pageTransitionsTheme: PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
}),
),
);
}
}
/// 自定义PageRouterBuilder
goNextPage(BuildContext context) {
var router = PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return Page();
},
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(position: Tween<Offset>(
begin: Offset(0.0, 0.0),
end: Offset(0.0, 0.0),
).animate(animation), child: child,);
});
return Navigator.push(context, router);
}
Hero动画
使用Hero
动画的时候我们需要注意两个Hero Widget
分别位于两个页面当中,但是两个Hero的tag必须一致。
如以下示例代码:
const String _heroTag = '_heroTag';
const String _imgUrl =
"https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1915138504,138225023&fm=111&gp=0.jpg";
class HeroAnimatedPage extends StatefulWidget {
@override
_HeroAnimatedPageState createState() => _HeroAnimatedPageState();
}
class _HeroAnimatedPageState extends State<HeroAnimatedPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Hero"),
),
body: Center(
child: InkWell(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return HeroPage();
}));
},
child: SizedBox(
width: 100,
height: 100,
child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
),
),
),
);
}
}
class HeroPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("内页")),
body: Center(
child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
),
);
}
}
可以看到使用Hero
动画的过程很简单,那么系统层如何帮助我们做到的?
这是因为我们在使用MaterialApp
的时候,其内部的_MaterialAppState
已经帮我们初始化了HeroController
的示例对象,并且HeroController
是NavigatorObserver
的子类,可以订阅路由的跳转事件,HeroController
监听到跳转之后就负责生成Hero
的动画和执行
static HeroController createMaterialHeroController() {
return HeroController(
createRectTween: (Rect begin, Rect end) {
/// 可以看到这里传入了开始和结束的Rect
return MaterialRectArcTween(begin: begin, end: end);
},
);
}
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
@override
void initState() {
super.initState();
/// 这里初始化了 HeroController
_heroController = MaterialApp.createMaterialHeroController();
}
@override
Widget build(BuildContext context) {
Widget result = _buildWidgetApp(context);
assert(() {
if (widget.debugShowMaterialGrid) {
result = GridPaper(
color: const Color(0xE0F9BBE0),
interval: 8.0,
divisions: 2,
subdivisions: 1,
child: result,
);
}
return true;
}());
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
/// 可以看到这里使用HeroControllerScope包装起来了,其内部是继承InheritedWidget
/// 当下个页面也有Hero Widget的时候,且tag一样,那么就可以开启Hero动画
child: HeroControllerScope(
controller: _heroController,
child: result,
)
);
}
}
/// 以下的代码片段截取自_HeroFlight,可以点击Hero查看源码得到
Positioned(
top: offsets.top,
right: offsets.right,
bottom: offsets.bottom,
left: offsets.left,
child: IgnorePointer(
child: RepaintBoundary(
child: Opacity(
opacity: _heroOpacity.value,
child: child,
),
),
),
);
通过对源码的查看,我们知道Hero
动画就是页面开始跳转的时候,通过计算原Rect
和结束时目标的Rect
之后,通过Positioned
改变自身的位置,最后实现动画的展示
Rive 动画
这个更多的是设计通过工具导出各种绚丽的动画给到我们,然后我们在给展示到屏幕中。
比如下面的这个动画,如果自己去使用绘制效果去处理,那么绝对是一件头疼的事情,但是引入Rive
动画就可以很好的解决这个问题,并且如果你仔细的观察下面的动画,你会发现动画是可以分别控制的(雨刮器和车子)。
当然除了这个Lottie
也有对应的Flutter package
支持。
这里把示例代码展示一下
需要注意的是资源文件你需要自行去这里下载,然后倒入rive
的package
,之后就可以实现上图的动画效果了。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
class RiveDemoPage extends StatefulWidget {
@override
_RiveDemoPageState createState() => _RiveDemoPageState();
}
class _RiveDemoPageState extends State<RiveDemoPage> with SingleTickerProviderStateMixin{
void _togglePlay() {
setState(() => _controller.isActive = !_controller.isActive);
}
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard _artboard;
RiveAnimationController _controller;
WiperAnimation _wipersController;
// flag to turn on and off the wipers
bool _wipers = false;
@override
void initState() {
_loadRiveFile();
super.initState();
}
/// Loads a Rive file
void _loadRiveFile() async {
final bytes = await rootBundle.load('assets/off_road_car.riv');
final file = RiveFile();
if (file.import(bytes)) {
setState(() => _artboard = file.mainArtboard
/// idle 控制车辆
..addController(_controller = SimpleAnimation('idle')));
}
}
void _wipersChange(bool wipersOn) {
if (_wipersController == null) {
_artboard.addController(
/// 控制雨刮器
_wipersController = WiperAnimation('windshield_wipers'),
);
}
setState(() {
if (_wipers) {
_wipersController.stop();
} else {
_wipersController.start();
}
_wipers = !_wipers;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("自定义动画"),),
body: SafeArea(
child: Column(
children: [
Expanded(child: _artboard == null
? const SizedBox()
: Rive(artboard: _artboard),),
SizedBox(
height: 50,
width: 200,
child: SwitchListTile(
title: const Text('雨刮器'),
value: _wipers,
onChanged: _wipersChange,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
tooltip: isPlaying ? 'Pause' : 'Play',
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}
class WiperAnimation extends SimpleAnimation {
WiperAnimation(String animationName) : super(animationName);
start() {
instance.animation.loop = Loop.loop;
isActive = true;
}
stop() => instance.animation.loop = Loop.oneShot;
}
手势与触摸
当我们对屏幕区域中的任意一点进行点击的时候,这个事件是如何准确找到是谁处理或者抛弃该事件的呢?由于Flutter
的跨平台特性,所以一个事件的传递需要经过原生层、中间层、Dart层。对于原生层和中间层会有各自的处理逻辑,从Dart
层开始就进入了Flutter
的领域了。
事件流程
通过上图可以看到,在Dart层中事件都是从_dispatchPointerDataPacket开始,之后会通过Zone判断环境进行回调,然后返回到GestureBinding类中的_handlePointEvent
方法,下面就是截取的_handlePointerEvent
方法的内部代码
void _handlePointerEvent(PointerEvent event) {
assert(!locked);
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
/// 检测并添加合法控件成员列表
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
// 抬起,取消事件,不用hitTest,移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// Because events that occur with the pointer down (like
// PointerMoveEvents) should be dispatched to the same place that their
// initial PointerDownEvent was, we want to re-use the path we found when
// the pointer went down, rather than do hit detection each time we get
// such an event.
hitTestResult = _hitTests[event.pointer];
}
assert(() {
if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
debugPrint('$event');
return true;
}());
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
/// 满足上面条件之后,开始分发事件
dispatchEvent(event, hitTestResult);
}
}
hitTest
hitTest
方法主要是为了获取到一个HitTestResult
,这个hitTestResult
内用一个List<HisTestEntry>
用于保存竞技对象,而每个HitTestEntry.target
都会存储每个空间对应的RenderObject
对象(因为RenderObject
实现了HitTestTarget
接口)
dispatchEvent
dispatchEvent
主要是对事件进行分发。通过上面的hitTest
方法之后,我们得到了所有的竞技对象列表,然后就挨个的进行遍历HitTestEntry
对象,调用他们的handleEvent
方法:
下面代码删除了部分error
的处理
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
if (hitTestResult == null) {
assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
}
return;
}
/// 遍历HitTestEntry对象列表
for (final HitTestEntry entry in hitTestResult.path) {
try {
/// 分别调用renderObject的handleEvent方法
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
}
}
}
这里可能会出现一种情况:如果一个区域的内的多个控件都实现了HandleEvent
方法,此时最后的处理权交给那个控件呢?这就设计了事件竞争。
事件竞争
Flutter
设计事件竞争的时候,定义了一个概念:通过一个竞技场,各个控件参与竞争,但只有符合下面两个条件中的任意一个的时候,才可以获得事件的处理权。
- 最后得到直接胜利的控件(理解为这个区域就只有他)
- 活到最后的空间中排在第一为的控件(这个区域有多个,但是列表中排第一个的才享有处理权)
也就是说当Down
事件时通过addPointer
加入GestureRecognizer
竞技场,如果区域内只有他一个响应者,那么会在Up
的时候进行_checkUp
,直接就完成竞争流程,如果这个区域内有多个响应,那么Down
的时候就决定不了谁是最终响应者,则会在Up
的时候直接把列表中的第一个作为响应者。
滑动事件
滑动事件的处理,也要求控件在Down
流程中通过addPointer
加入竞技场,然后在Move
流程中,通过PointerRouter.route
执行DragGestureRecoginzer.handleEvent
处理事件。
在PointerMoveEvent
事件里,DragGestureRecognizer.handleEvent
会通过_hasSufficientPendingDragDeltaToAccept
方法判断是否符合条件,如果符合条件直接执行resolve(GestureDisposition.accepted)
,之后流程回到竞技场执行acceptGesture
,触发onStart
和onUpdate
去通知上层处理事件。
onUpdate
事件回去更新Offset
,然后通过markNeedsLayout
是的ViewPort
重新布局,让界面看起来像滚动起来。
滑动Physic
我们都知道滑动结束后不会马上停止,而是有一个物理的减弱动画的实现,那么他是怎么实现的呢?
官方给我们定义了四种ScrollPhysics
-
BouncingScrollPhysics
允许滚动超出边界,松手之后内容会进行回弹效果 -
ClampingScrollPhysics
防止超出边界,夹住收尾区域 -
AlwaysScrollableScrollPhysics
始终响应用户的滚动,松手之后内容会进行回弹效果 -
NeverScrollableScrollPhysics
不响应用户的滚动
部分情况下我们不会特意去设置physics
,细心的你可能会发现在iOS
和Android
上面ListView
、CustomScrollView
等控件,拖拽效果会有不同,为啥子呢?这是因为在ScrollConfiguration
内部进行特别的处理。
ScrollConfiguration
实现滑动溢出的效果关键的两个雷就是ScrollConfiguration
和ScrollBehavior
。在Scrollable
的_updatePosition
源码中,里面有一个关键的判断,如果widget.physics==null
的时候,就会使用ScrollConfiguration.of(context)
的getScrollPhysics(context)
方法获取_physics
,ScrollConfiguration.of(context)
返回是一个InheritedWidget
对象ScrollBehavior
。
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
/// 这里如果不为空才使用自己的,否则就使用从ScrollConfiguration中获取的physics
if (widget.physics != null)
_physics = widget.physics.applyTo(_physics);
final ScrollController controller = widget.controller;
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);
scheduleMicrotask(oldPosition.dispose);
}
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
assert(position != null);
controller?.attach(position);
}
所以在我们不填写physics
属性的情况下,我们获取的physics
就要看getScrollPhysics(context)
这个方法的内部实现
static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _bouncingPhysics;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _clampingPhysics;
}
return null;
}
上面的源码就可以很好的展示了默认情况下的为什么列表在iOS
和Android
平台中会有不同滑动效果的原因。
然后我们去查看ScrollConfiguration.of
方法
static ScrollBehavior of(BuildContext context) {
final ScrollConfiguration configuration = context.dependOnInheritedWidgetOfExactType<ScrollConfiguration>();
return configuration?.behavior ?? const ScrollBehavior();
}
发现这个类是InheritedWidget
,我们知道InheritedWidget
的特性(数据向下传递),那么他是在哪里被初始化的呢?最终在MaterialApp
的实现中找到了,代码截图如下:
/// 此为代码片段,删除了部分无关代码
@override
Widget build(BuildContext context) {
Widget result = _buildWidgetApp(context);
/// 发现这里是ScrollConfiguration包装起来的对象
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: HeroControllerScope(
controller: _heroController,
child: result,
)
);
}
ScrollPhysics
通过查看ScrollConfiguration
的相关实现,我们知道了为什么不同的平台会有不同的拖曳处理,但是怎么去实现这个效果则主要是通过ScrollPhysics
的子类去实现,ScrollPhysics
本身只是定义了一些属性和方法,其中几个比较重要的方法如下:
class ScrollPhysics {
// 将用户拖曳距离Offset转化为需要移动的pixels
// 如果没有父类就直接返回offset,否则就调用父类的applyPhysicsToUserOffset方法
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
// 返回边界条件,如果是0,overscroll则一直就是0
// 如果没有父类,直接返回0,否则就调用父类的同名方法
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
/// 创建一个滚动的模拟器,这个处理器就是处理阻尼、滑动、回弹效果的具体实现
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
/// 最小的滑动速率
/// const double kMinFlingVelocity = 50.0 默认是50
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
/// 传输动量,返回重复滚动的速度
/// 如果父类为空就直接返回0 否则调用父类同名方法
double carriedMomentum(double existingVelocity) {
if (parent == null)
return 0.0;
return parent.carriedMomentum(existingVelocity);
}
/// 最小的开始拖曳距离
/// 如果返回空,则不执行最低阈值。
double get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
/// 滚动模拟的公差
/// 指定距离、持续时间和速度差应视为平等的差异的结构
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
/// 这个就是默认实现
static final Tolerance _kDefaultTolerance = Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing.
// TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio, // logical pixels
);
}
所以ScrollPhysics
的具体实现就依赖Scrollable
的触摸响应流程中,主要的逻辑关键地方就是下面的是三个方法:
-
applyPhysicsToUserOffset
: 通过physics将用户拖曳距离offset
转化为setPixels
(滚动)的增量 -
applyBoundaryConditions
:通过physics计算当前滚动的边界条件 -
createBallisticSimulation
:创建自动滑动的模拟器
这三个触发的时机分别在于:_handleDragUpdate
、_handleDragCancel
、_handleDragEnd
也就是拖曳的过程和结束的时机
-
applyPhysicsToUserOffset
和applyBoundaryConditions
是在_handleDragUpdate
时触发计算 -
createBallisticSimulation
是在_handleDragCancel
和_handleDragEnd
时触发
图片加载
在Flutter
加载图片一般有两种方式:Image
和DecorationImage
,其中DecorationImage
用于DecoratedBox
中。
两者不同之处是Image
是一个StatefulWidget
,其内部通过RawImage
实现图片的绘制;而DecorationImage
并不是控件,只是单纯图片绘制,内部通过DecorationImagePainter
直接绘制图片。正因如此,DecorationImage
对比Image
少了一些可定制化的配置。
两者相同的地方则是都需要ImageProvider
实现图片的加载和数据转换。只是Image
控件提供了Image.asset
方法对AssetImage
的封装,其实他们最终都会通过decoration_image.dart
下的paintImage
方法实现图片的绘制逻辑。
下表是Image的参数列表
参数 | 描述 |
---|---|
width/height | 图片区域的宽高,需要注意不是图片的宽高 |
fit | 填充模式 BoxFit.fill 铺满设置的区域,可能会拉伸 BoxFit.fitHeight 填充高度,可能会裁剪、拉伸 BoxFit.fitWidth 填充宽度、可能会裁剪、拉伸 BoxFit.contain 居中显示,显示原比例,可能不会填满 BoxFit.cover 原比例进行裁剪并填满容器 BoxFit.scaleDown 和contain类似,但是只会缩小不会放大 |
color | 前景色,会覆盖图片颜色,多数情况和colorBlendMode配合使用 |
colorBlendMode | 与color参数结合使用,设置color的回合模式 |
alignment | 对齐方式 |
repeat | 是否重复(当图片填充不满设置的width/height) |
centerSlice | 设置图片的拉伸区域,需要注意只有在大于width/height的情况下才可以使用这个属性 |
matchTextDirection | 需要配合Directionality进行使用,一般配合文本对齐 |
gaplessPlayback | 图片更新之后,是否将原图片进行保留之后在显示 |
filterQuality | 图片显示的过滤质量 |
loadingBuilder | 图片加载中的显示 返回Widget |
frameBuilder | 对需要显示的图片进行定制处理 |
Image Demo
class ImageDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Image Demo")),
body: Center(
child: Image(
width: 100,
height: 100,
fit: BoxFit.cover,
image: NetworkImage("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1421194919,3695584663&fm=26&gp=0.jpg"),
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
return Container(
decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.grey, blurRadius: 1, spreadRadius: 1)],
),
child: child,
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes
: null,
),
);
},
),
),
);
}
}
效果图如下:
图片加载流程
图片缓存
Flutter
中对图片是进行了缓存的(但是没有做本地缓存),缓存的方法实现在ImageCache
中的putIfAbsent
方法。
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? onError }) {
TimelineTask? timelineTask;
TimelineTask? listenerTask;
if (!kReleaseMode) {
timelineTask = TimelineTask()..start(
'ImageCache.putIfAbsent',
arguments: <String, dynamic>{
'key': key.toString(),
},
);
}
ImageStreamCompleter? result = _pendingImages[key]?.completer;
// 还在加载,直接返回
if (result != null) {
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{'result': 'pending'});
}
return result;
}
// 已经缓存过了,直接返回
final _CachedImage? image = _cache.remove(key);
if (image != null) {
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
}
_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
_cache[key] = image;
return image.completer;
}
final _CachedImage? liveImage = _liveImages[key];
if (liveImage != null) {
_touch(key, liveImage, timelineTask);
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
}
return liveImage.completer;
}
try {
result = loader();
_trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
} catch (error, stackTrace) {
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{
'result': 'error',
'error': error.toString(),
'stackTrace': stackTrace.toString(),
});
}
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
if (!kReleaseMode) {
listenerTask = TimelineTask(parent: timelineTask)..start('listener');
}
// If we're doing tracing, we need to make sure that we don't try to finish
// the trace entry multiple times if we get re-entrant calls from a multi-
// frame provider here.
bool listenedOnce = false;
// We shouldn't use the _pendingImages map if the cache is disabled, but we
// will have to listen to the image at least once so we don't leak it in
// the live image tracking.
// If the cache is disabled, this variable will be set.
_PendingImage? untrackedPendingImage;
void listener(ImageInfo? info, bool syncCall) {
// Images that fail to load don't contribute to cache size.
final int imageSize = info == null || info.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result!, imageSize);
_trackLiveImage(
key,
_LiveImage(
result,
imageSize,
() => _liveImages.remove(key),
),
);
final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
// Only touch if the cache was enabled when resolve was initially called.
if (untrackedPendingImage == null) {
_touch(key, image, listenerTask);
}
if (!kReleaseMode && !listenedOnce) {
listenerTask!.finish(arguments: <String, dynamic>{
'syncCall': syncCall,
'sizeInBytes': imageSize,
});
timelineTask!.finish(arguments: <String, dynamic>{
'currentSizeBytes': currentSizeBytes,
'currentSize': currentSize,
});
}
listenedOnce = true;
}
final ImageStreamListener streamListener = ImageStreamListener(listener);
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, streamListener);
} else {
untrackedPendingImage = _PendingImage(result, streamListener);
}
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
return result;
}
由于图片的缓存都是异步的方式,他并不知道图片会消耗多少内存(在未解码之前),所以当一个页面加载了大量图片,此时就有可能会造成内存不够的情况,在iOS
上面的表现就是会被系统给杀掉。
所以如果对内存缓存大小和数量有要求的话,可以通过PaintingBinding.instacne.imageCache.maximunSize
进行设置(默认是100m
),同时最好在页面不可见的时候暂停图片的I/O
和下载。
网络请求
Flutter
中的网络请求是不经过原生端的,所以我们使用的是Dart
层的网络请求服务。
HttpClient
/// 创建Client
HttpClient httpClient = HttpClient();
/// 地址
Uri uri = Uri(scheme: "https", host: "example.com");
/// 得到request
HttpClientRequest request = await httpClient.getUrl(uri);
/// 添加请求头
request.header.add("token": "xxxxx");
/// 等待请求结果
HttpClientResponse response = await request.close();
/// 解析数据
String responseBody = await response.transform(utf8.decoder).join();
/// 关闭client
httpClient.close();
一般情况下,我们很少使用到HttpClient
来进行网络请求,在Flutter
大部分还是使用第三方封装好的SDK
,比如Dio
Dio
Flutter
中出名的网络请求框架,除了基本的网络请求之外还设置了拦截器、代理等相当使用的功能。
具体的实现可以去GitHub
或者pub.dev
查看
import 'package:dio/dio.dart';
void getHttp() async {
try {
Response response = await Dio().get("http://www.google.com");
print(response);
} catch (e) {
print(e);
}
}