線程模型
Flutter是運(yùn)行Engine上來(lái)實(shí)現(xiàn)跨平臺(tái)的,Dart支持通過(guò)isolate實(shí)現(xiàn)異步處理的邏輯。但是線程的管理和創(chuàng)建并不是有Engine負(fù)責(zé),而是交由Embedder去實(shí)現(xiàn),除了線程之外Embedder還負(fù)責(zé)了事件循環(huán)、平臺(tái)插件等相關(guān)邏輯,Embedder就是Flutter和運(yùn)行平臺(tái)之間的中間層架構(gòu),他們的關(guān)系如下圖:

在Flutter Engine中需要運(yùn)行四個(gè)任務(wù),分別是:UI Task Runner、Platform Task Runner、GPU Task Runner、I/O Task Runner,這四個(gè)任務(wù)運(yùn)行在不同的線程之上。
UI Task Runner
UI Task Runner其實(shí)就是Flutter的UI運(yùn)行任務(wù),也就是我們所說(shuō)的UI線程,因?yàn)閁I Task Runner會(huì)執(zhí)行的Dart的root isolate去齊總運(yùn)行App main方法,并且為其綁定了UI渲染提交的回調(diào)等。這也是為什么稱(chēng)Flutter是單線程應(yīng)用的原因,這里指的就是UI Task Runner。UI Task Runner除了處理UI之外還處理了microTasks、插件消息的響應(yīng)處理等相關(guān)邏輯。

谷歌官方的UI渲染繪制流程:

GPU Task Runner
當(dāng)UI Task Runner生成Layer Tree之后,接下來(lái)的處理就會(huì)給GPU Task Runner處理,GPU Task Runner會(huì)把Layer Tree轉(zhuǎn)化為Skia所需要的繪制指令,通過(guò)延遲調(diào)度和Buffer來(lái)保證繪制任務(wù)的流程運(yùn)行,下圖就是GPU的整個(gè)流程

I/O Task Runner
I/O Task Runner就是顧名思義具備讀寫(xiě)能力線程,比如圖片數(shù)據(jù)的獲取、解析成渲染數(shù)據(jù)等耗時(shí)操作,這些都屬于耗時(shí)操作,交給其他三個(gè)Task Runner處理明顯不合適,所以對(duì)于需要耗時(shí)的操作,一般都是通過(guò)對(duì)I/O Task Runner處理。
Platform Task Runner
Platform Task Runner是Flutter Engine的主線程,在Flutter 中所有和Engine調(diào)用都會(huì)通過(guò)Platform Task Runner,之所以這樣子做是為了保證線程安全。但是也帶來(lái)了一個(gè)弊端,某個(gè)任務(wù)的處理造成了嚴(yán)重堵塞的時(shí)候,可能會(huì)引發(fā)應(yīng)用的ANR奔潰。
那么對(duì)與一些耗時(shí)的操作,我們應(yīng)該放到哪里去處理呢?答案就是isolate
isolate
isolate運(yùn)行我們開(kāi)辟一個(gè)線程,由于isolate的特性(數(shù)據(jù)不能互通),所以也不要鎖。在實(shí)際開(kāi)發(fā)中比如json等一些耗時(shí)操作我們就可以使用isolate去處理。
isolate對(duì)比傳統(tǒng)的線程最大區(qū)別是:
- 數(shù)據(jù)不共享
- 只能通過(guò)
port進(jìn)行通信 - 每個(gè)
isolate都有自己的內(nèi)存和任務(wù)管理
單線程運(yùn)行
之說(shuō)以說(shuō)Flutter是單線程應(yīng)用,是因?yàn)?code>Dart數(shù)據(jù)單線程運(yùn)行機(jī)制,而這個(gè)機(jī)制主要是通過(guò)消息循環(huán)機(jī)制和任務(wù)調(diào)度處理,其中有兩個(gè)任務(wù)隊(duì)列:microTask queue和Event queue。
其中microTask優(yōu)先級(jí)高于Event queue,從下面的運(yùn)行流程圖就可以很明顯的看出。
實(shí)際開(kāi)發(fā)中也要注意由于microTask的特性(優(yōu)先級(jí)高),如果頻繁把任務(wù)插入到microTask中去執(zhí)行,就可能會(huì)造成UI卡頓和掉幀的現(xiàn)象。
[圖片上傳失敗...(image-ff934f-1616915674307)]
async/await、Future
通常我們使用async/await和Future來(lái)實(shí)現(xiàn)異步的操作,但實(shí)際上這并不是真正的異步,而是在單線程上的任務(wù)調(diào)度,也稱(chēng)之為協(xié)程。協(xié)程是不具備線程一樣并發(fā)執(zhí)行的能力。簡(jiǎn)單的說(shuō)就是當(dāng)程序執(zhí)行被標(biāo)注了async方法,運(yùn)行到await的時(shí)候,表示這個(gè)方法需要等待結(jié)果,此時(shí)就會(huì)跳過(guò)這個(gè)片段代碼繼續(xù)執(zhí)行其他邏輯,然后在程序在下一次輪詢的時(shí)候,在判斷是否有了返回結(jié)果,如果就執(zhí)行,沒(méi)有就繼續(xù)等待下一個(gè)輪詢。
比如我們要在StatefulWidget中的initState方法中通過(guò)context獲取一些數(shù)據(jù),如果我們直接使用,就會(huì)拋出異常,但是我們使用Future.delay(duration: Duration(seconds: 0)).then(() {...})方法包裝起來(lái)之后,則不會(huì)出現(xiàn)該問(wèn)題。這是因?yàn)?strong>獲取context的邏輯會(huì)被放到下一個(gè)輪詢中 被執(zhí)行。
動(dòng)畫(huà)
我們知道,動(dòng)畫(huà)其實(shí)是由幀構(gòu)成的,每一幀都是一張圖片,多張圖片連續(xù)執(zhí)行,就形成了動(dòng)畫(huà)。
普通動(dòng)畫(huà)
之前我們說(shuō)過(guò)Widget是不可變的,那么Widget是如何產(chǎn)生動(dòng)畫(huà)的呢?
我們用一個(gè)示例來(lái)看看,一個(gè)簡(jiǎn)單的放大動(dòng)畫(huà)的實(shí)現(xiàn)。
示例代碼
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 ? "點(diǎn)我縮小" : "點(diǎn)我放大"),
),
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
實(shí)現(xiàn)分析
當(dāng)我們要實(shí)現(xiàn)一個(gè)從100→200的矩形變大動(dòng)畫(huà)的時(shí)候,最重要的就是每一幀都讓這個(gè)矩形變大一點(diǎn),每一幀變大多少,就通過(guò)動(dòng)畫(huà)總時(shí)長(zhǎng)和矩形大小的比(不考慮Curve),然后得到每一幀的寬高值,在把值設(shè)置給矩形。
所以我們需要三個(gè)條件:
- 可以更新
- 并且是每一幀都會(huì)有回調(diào)
- 能幫我們計(jì)算沒(méi)幀的寬高值的對(duì)象
分析上面的代碼
-
首先這個(gè)是
StatefulWidget這里使用它的關(guān)鍵地方就是他可以使用
setState進(jìn)行跟新UI操作 -
其次我們混入了
SingleTickerProviderStateMixin提供了每一幀的繪制回調(diào)
Tick -
然后我們使用了
AnimationController通過(guò)
TickerProviderStateMixin每一幀的回調(diào),在使用AnimationController內(nèi)部的simulation計(jì)算單位時(shí)間內(nèi)value的值,最后通過(guò)setState進(jìn)行更新
內(nèi)部_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();
}
最后我們發(fā)現(xiàn)上面使用的是SingleTickerProviderStateMixin,如果我們要在一個(gè)頁(yè)面使用多個(gè)動(dòng)畫(huà)的時(shí)候,只需要把SingleTickerProviderStateMixin換成TickerProviderStateMixin。
至于具體的動(dòng)畫(huà)案例,后續(xù)出對(duì)應(yīng)的文章。
路由動(dòng)畫(huà)
路由動(dòng)畫(huà)指的就是我們?cè)陧?yè)面進(jìn)行切換的時(shí)候兩個(gè)頁(yè)面展示的動(dòng)畫(huà)。系統(tǒng)默認(rèn)給我們實(shí)現(xiàn)了一套,代碼在theme的pageTransitionTheme設(shè)置。我們可以在這里進(jìn)行自定義,同時(shí)也可以在push的時(shí)候自定義PageRouterBuilder,在設(shè)置transitionsBuilder設(shè)置動(dòng)畫(huà)效果。
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動(dòng)畫(huà)
使用Hero動(dòng)畫(huà)的時(shí)候我們需要注意兩個(gè)Hero Widget分別位于兩個(gè)頁(yè)面當(dāng)中,但是兩個(gè)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("內(nèi)頁(yè)")),
body: Center(
child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
),
);
}
}
可以看到使用Hero動(dòng)畫(huà)的過(guò)程很簡(jiǎn)單,那么系統(tǒng)層如何幫助我們做到的?
這是因?yàn)槲覀冊(cè)谑褂?code>MaterialApp的時(shí)候,其內(nèi)部的_MaterialAppState已經(jīng)幫我們初始化了HeroController的示例對(duì)象,并且HeroController是NavigatorObserver的子類(lèi),可以訂閱路由的跳轉(zhuǎn)事件,HeroController監(jiān)聽(tīng)到跳轉(zhuǎn)之后就負(fù)責(zé)生成Hero的動(dòng)畫(huà)和執(zhí)行
static HeroController createMaterialHeroController() {
return HeroController(
createRectTween: (Rect begin, Rect end) {
/// 可以看到這里傳入了開(kāi)始和結(jié)束的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包裝起來(lái)了,其內(nèi)部是繼承InheritedWidget
/// 當(dāng)下個(gè)頁(yè)面也有Hero Widget的時(shí)候,且tag一樣,那么就可以開(kāi)啟Hero動(dòng)畫(huà)
child: HeroControllerScope(
controller: _heroController,
child: result,
)
);
}
}
/// 以下的代碼片段截取自_HeroFlight,可以點(diǎn)擊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,
),
),
),
);
通過(guò)對(duì)源碼的查看,我們知道Hero動(dòng)畫(huà)就是頁(yè)面開(kāi)始跳轉(zhuǎn)的時(shí)候,通過(guò)計(jì)算原Rect和結(jié)束時(shí)目標(biāo)的Rect之后,通過(guò)Positioned改變自身的位置,最后實(shí)現(xiàn)動(dòng)畫(huà)的展示
Rive 動(dòng)畫(huà)
這個(gè)更多的是設(shè)計(jì)通過(guò)工具導(dǎo)出各種絢麗的動(dòng)畫(huà)給到我們,然后我們?cè)诮o展示到屏幕中。
比如下面的這個(gè)動(dòng)畫(huà),如果自己去使用繪制效果去處理,那么絕對(duì)是一件頭疼的事情,但是引入Rive動(dòng)畫(huà)就可以很好的解決這個(gè)問(wèn)題,并且如果你仔細(xì)的觀察下面的動(dòng)畫(huà),你會(huì)發(fā)現(xiàn)動(dòng)畫(huà)是可以分別控制的(雨刮器和車(chē)子)。
當(dāng)然除了這個(gè)Lottie也有對(duì)應(yīng)的Flutter package支持。

這里把示例代碼展示一下
需要注意的是資源文件你需要自行去這里下載,然后倒入rive的package,之后就可以實(shí)現(xiàn)上圖的動(dòng)畫(huà)效果了。
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 控制車(chē)輛
..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("自定義動(dòng)畫(huà)"),),
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;
}
手勢(shì)與觸摸
當(dāng)我們對(duì)屏幕區(qū)域中的任意一點(diǎn)進(jìn)行點(diǎn)擊的時(shí)候,這個(gè)事件是如何準(zhǔn)確找到是誰(shuí)處理或者拋棄該事件的呢?由于Flutter的跨平臺(tái)特性,所以一個(gè)事件的傳遞需要經(jīng)過(guò)原生層、中間層、Dart層。對(duì)于原生層和中間層會(huì)有各自的處理邏輯,從Dart層開(kāi)始就進(jìn)入了Flutter的領(lǐng)域了。

事件流程
通過(guò)上圖可以看到,在Dart層中事件都是從_dispatchPointerDataPacket開(kāi)始,之后會(huì)通過(guò)Zone判斷環(huán)境進(jìn)行回調(diào),然后返回到GestureBinding類(lèi)中的_handlePointEvent方法,下面就是截取的_handlePointerEvent方法的內(nèi)部代碼
void _handlePointerEvent(PointerEvent event) {
assert(!locked);
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
/// 檢測(cè)并添加合法控件成員列表
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);
/// 滿足上面條件之后,開(kāi)始分發(fā)事件
dispatchEvent(event, hitTestResult);
}
}
hitTest
hitTest方法主要是為了獲取到一個(gè)HitTestResult,這個(gè)hitTestResult內(nèi)用一個(gè)List<HisTestEntry>用于保存競(jìng)技對(duì)象,而每個(gè)HitTestEntry.target都會(huì)存儲(chǔ)每個(gè)空間對(duì)應(yīng)的RenderObject對(duì)象(因?yàn)?code>RenderObject實(shí)現(xiàn)了HitTestTarget接口)
dispatchEvent
dispatchEvent主要是對(duì)事件進(jìn)行分發(fā)。通過(guò)上面的hitTest方法之后,我們得到了所有的競(jìng)技對(duì)象列表,然后就挨個(gè)的進(jìn)行遍歷HitTestEntry對(duì)象,調(diào)用他們的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對(duì)象列表
for (final HitTestEntry entry in hitTestResult.path) {
try {
/// 分別調(diào)用renderObject的handleEvent方法
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
}
}
}
這里可能會(huì)出現(xiàn)一種情況:如果一個(gè)區(qū)域的內(nèi)的多個(gè)控件都實(shí)現(xiàn)了HandleEvent方法,此時(shí)最后的處理權(quán)交給那個(gè)控件呢?這就設(shè)計(jì)了事件競(jìng)爭(zhēng)。
事件競(jìng)爭(zhēng)
Flutter設(shè)計(jì)事件競(jìng)爭(zhēng)的時(shí)候,定義了一個(gè)概念:通過(guò)一個(gè)競(jìng)技場(chǎng),各個(gè)控件參與競(jìng)爭(zhēng),但只有符合下面兩個(gè)條件中的任意一個(gè)的時(shí)候,才可以獲得事件的處理權(quán)。
- 最后得到直接勝利的控件(理解為這個(gè)區(qū)域就只有他)
- 活到最后的空間中排在第一為的控件(這個(gè)區(qū)域有多個(gè),但是列表中排第一個(gè)的才享有處理權(quán))
也就是說(shuō)當(dāng)Down事件時(shí)通過(guò)addPointer加入GestureRecognizer競(jìng)技場(chǎng),如果區(qū)域內(nèi)只有他一個(gè)響應(yīng)者,那么會(huì)在Up的時(shí)候進(jìn)行_checkUp,直接就完成競(jìng)爭(zhēng)流程,如果這個(gè)區(qū)域內(nèi)有多個(gè)響應(yīng),那么Down的時(shí)候就決定不了誰(shuí)是最終響應(yīng)者,則會(huì)在Up的時(shí)候直接把列表中的第一個(gè)作為響應(yīng)者。
滑動(dòng)事件
滑動(dòng)事件的處理,也要求控件在Down流程中通過(guò)addPointer加入競(jìng)技場(chǎng),然后在Move流程中,通過(guò)PointerRouter.route執(zhí)行DragGestureRecoginzer.handleEvent處理事件。

在PointerMoveEvent事件里,DragGestureRecognizer.handleEvent會(huì)通過(guò)_hasSufficientPendingDragDeltaToAccept方法判斷是否符合條件,如果符合條件直接執(zhí)行resolve(GestureDisposition.accepted),之后流程回到競(jìng)技場(chǎng)執(zhí)行acceptGesture,觸發(fā)onStart和onUpdate去通知上層處理事件。
onUpdate事件回去更新Offset,然后通過(guò)markNeedsLayout是的ViewPort重新布局,讓界面看起來(lái)像滾動(dòng)起來(lái)。
滑動(dòng)Physic
我們都知道滑動(dòng)結(jié)束后不會(huì)馬上停止,而是有一個(gè)物理的減弱動(dòng)畫(huà)的實(shí)現(xiàn),那么他是怎么實(shí)現(xiàn)的呢?
官方給我們定義了四種ScrollPhysics
-
BouncingScrollPhysics允許滾動(dòng)超出邊界,松手之后內(nèi)容會(huì)進(jìn)行回彈效果 -
ClampingScrollPhysics防止超出邊界,夾住收尾區(qū)域 -
AlwaysScrollableScrollPhysics始終響應(yīng)用戶的滾動(dòng),松手之后內(nèi)容會(huì)進(jìn)行回彈效果 -
NeverScrollableScrollPhysics不響應(yīng)用戶的滾動(dòng)
部分情況下我們不會(huì)特意去設(shè)置physics,細(xì)心的你可能會(huì)發(fā)現(xiàn)在iOS和Android上面ListView、CustomScrollView等控件,拖拽效果會(huì)有不同,為啥子呢?這是因?yàn)樵?code>ScrollConfiguration內(nèi)部進(jìn)行特別的處理。
ScrollConfiguration
實(shí)現(xiàn)滑動(dòng)溢出的效果關(guān)鍵的兩個(gè)雷就是ScrollConfiguration和ScrollBehavior。在Scrollable的_updatePosition源碼中,里面有一個(gè)關(guān)鍵的判斷,如果widget.physics==null的時(shí)候,就會(huì)使用ScrollConfiguration.of(context)的getScrollPhysics(context)方法獲取_physics,ScrollConfiguration.of(context)返回是一個(gè)InheritedWidget對(duì)象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);
}
所以在我們不填寫(xiě)physics屬性的情況下,我們獲取的physics就要看getScrollPhysics(context)這個(gè)方法的內(nèi)部實(shí)現(xiàn)
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;
}
上面的源碼就可以很好的展示了默認(rèn)情況下的為什么列表在iOS和Android平臺(tái)中會(huì)有不同滑動(dòng)效果的原因。
然后我們?nèi)ゲ榭?code>ScrollConfiguration.of方法
static ScrollBehavior of(BuildContext context) {
final ScrollConfiguration configuration = context.dependOnInheritedWidgetOfExactType<ScrollConfiguration>();
return configuration?.behavior ?? const ScrollBehavior();
}
發(fā)現(xiàn)這個(gè)類(lèi)是InheritedWidget,我們知道InheritedWidget的特性(數(shù)據(jù)向下傳遞),那么他是在哪里被初始化的呢?最終在MaterialApp的實(shí)現(xiàn)中找到了,代碼截圖如下:
/// 此為代碼片段,刪除了部分無(wú)關(guān)代碼
@override
Widget build(BuildContext context) {
Widget result = _buildWidgetApp(context);
/// 發(fā)現(xiàn)這里是ScrollConfiguration包裝起來(lái)的對(duì)象
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: HeroControllerScope(
controller: _heroController,
child: result,
)
);
}
ScrollPhysics
通過(guò)查看ScrollConfiguration的相關(guān)實(shí)現(xiàn),我們知道了為什么不同的平臺(tái)會(huì)有不同的拖曳處理,但是怎么去實(shí)現(xiàn)這個(gè)效果則主要是通過(guò)ScrollPhysics的子類(lèi)去實(shí)現(xiàn),ScrollPhysics本身只是定義了一些屬性和方法,其中幾個(gè)比較重要的方法如下:
class ScrollPhysics {
// 將用戶拖曳距離Offset轉(zhuǎn)化為需要移動(dòng)的pixels
// 如果沒(méi)有父類(lèi)就直接返回offset,否則就調(diào)用父類(lèi)的applyPhysicsToUserOffset方法
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
// 返回邊界條件,如果是0,overscroll則一直就是0
// 如果沒(méi)有父類(lèi),直接返回0,否則就調(diào)用父類(lèi)的同名方法
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
/// 創(chuàng)建一個(gè)滾動(dòng)的模擬器,這個(gè)處理器就是處理阻尼、滑動(dòng)、回彈效果的具體實(shí)現(xiàn)
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
/// 最小的滑動(dòng)速率
/// const double kMinFlingVelocity = 50.0 默認(rèn)是50
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
/// 傳輸動(dòng)量,返回重復(fù)滾動(dòng)的速度
/// 如果父類(lèi)為空就直接返回0 否則調(diào)用父類(lèi)同名方法
double carriedMomentum(double existingVelocity) {
if (parent == null)
return 0.0;
return parent.carriedMomentum(existingVelocity);
}
/// 最小的開(kāi)始拖曳距離
/// 如果返回空,則不執(zhí)行最低閾值。
double get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
/// 滾動(dòng)模擬的公差
/// 指定距離、持續(xù)時(shí)間和速度差應(yīng)視為平等的差異的結(jié)構(gòu)
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
/// 這個(gè)就是默認(rèn)實(shí)現(xiàn)
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的具體實(shí)現(xiàn)就依賴Scrollable的觸摸響應(yīng)流程中,主要的邏輯關(guān)鍵地方就是下面的是三個(gè)方法:
-
applyPhysicsToUserOffset: 通過(guò)physics將用戶拖曳距離offset轉(zhuǎn)化為setPixels(滾動(dòng))的增量 -
applyBoundaryConditions:通過(guò)physics計(jì)算當(dāng)前滾動(dòng)的邊界條件 -
createBallisticSimulation:創(chuàng)建自動(dòng)滑動(dòng)的模擬器
這三個(gè)觸發(fā)的時(shí)機(jī)分別在于:_handleDragUpdate、_handleDragCancel、_handleDragEnd也就是拖曳的過(guò)程和結(jié)束的時(shí)機(jī)
-
applyPhysicsToUserOffset和applyBoundaryConditions是在_handleDragUpdate時(shí)觸發(fā)計(jì)算 -
createBallisticSimulation是在_handleDragCancel和_handleDragEnd時(shí)觸發(fā)

圖片加載
在Flutter加載圖片一般有兩種方式:Image和DecorationImage,其中DecorationImage用于DecoratedBox中。
兩者不同之處是Image是一個(gè)StatefulWidget,其內(nèi)部通過(guò)RawImage實(shí)現(xiàn)圖片的繪制;而DecorationImage并不是控件,只是單純圖片繪制,內(nèi)部通過(guò)DecorationImagePainter直接繪制圖片。正因如此,DecorationImage對(duì)比Image少了一些可定制化的配置。
兩者相同的地方則是都需要ImageProvider實(shí)現(xiàn)圖片的加載和數(shù)據(jù)轉(zhuǎn)換。只是Image控件提供了Image.asset方法對(duì)AssetImage的封裝,其實(shí)他們最終都會(huì)通過(guò)decoration_image.dart下的paintImage方法實(shí)現(xiàn)圖片的繪制邏輯。
下表是Image的參數(shù)列表
| 參數(shù) | 描述 |
|---|---|
| width/height | 圖片區(qū)域的寬高,需要注意不是圖片的寬高 |
| fit | 填充模式 BoxFit.fill 鋪滿設(shè)置的區(qū)域,可能會(huì)拉伸 BoxFit.fitHeight 填充高度,可能會(huì)裁剪、拉伸 BoxFit.fitWidth 填充寬度、可能會(huì)裁剪、拉伸 BoxFit.contain 居中顯示,顯示原比例,可能不會(huì)填滿 BoxFit.cover 原比例進(jìn)行裁剪并填滿容器 BoxFit.scaleDown 和contain類(lèi)似,但是只會(huì)縮小不會(huì)放大 |
| color | 前景色,會(huì)覆蓋圖片顏色,多數(shù)情況和colorBlendMode配合使用 |
| colorBlendMode | 與color參數(shù)結(jié)合使用,設(shè)置color的回合模式 |
| alignment | 對(duì)齊方式 |
| repeat | 是否重復(fù)(當(dāng)圖片填充不滿設(shè)置的width/height) |
| centerSlice | 設(shè)置圖片的拉伸區(qū)域,需要注意只有在大于width/height的情況下才可以使用這個(gè)屬性 |
| matchTextDirection | 需要配合Directionality進(jìn)行使用,一般配合文本對(duì)齊 |
| gaplessPlayback | 圖片更新之后,是否將原圖片進(jìn)行保留之后在顯示 |
| filterQuality | 圖片顯示的過(guò)濾質(zhì)量 |
| loadingBuilder | 圖片加載中的顯示 返回Widget |
| frameBuilder | 對(duì)需要顯示的圖片進(jìn)行定制處理 |
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中對(duì)圖片是進(jìn)行了緩存的(但是沒(méi)有做本地緩存),緩存的方法實(shí)現(xiàn)在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;
}
// 已經(jīng)緩存過(guò)了,直接返回
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;
}
由于圖片的緩存都是異步的方式,他并不知道圖片會(huì)消耗多少內(nèi)存(在未解碼之前),所以當(dāng)一個(gè)頁(yè)面加載了大量圖片,此時(shí)就有可能會(huì)造成內(nèi)存不夠的情況,在iOS上面的表現(xiàn)就是會(huì)被系統(tǒng)給殺掉。
所以如果對(duì)內(nèi)存緩存大小和數(shù)量有要求的話,可以通過(guò)PaintingBinding.instacne.imageCache.maximunSize進(jìn)行設(shè)置(默認(rèn)是100m),同時(shí)最好在頁(yè)面不可見(jiàn)的時(shí)候暫停圖片的I/O和下載。
網(wǎng)絡(luò)請(qǐng)求
Flutter中的網(wǎng)絡(luò)請(qǐng)求是不經(jīng)過(guò)原生端的,所以我們使用的是Dart層的網(wǎng)絡(luò)請(qǐng)求服務(wù)。
HttpClient
/// 創(chuàng)建Client
HttpClient httpClient = HttpClient();
/// 地址
Uri uri = Uri(scheme: "https", host: "example.com");
/// 得到request
HttpClientRequest request = await httpClient.getUrl(uri);
/// 添加請(qǐng)求頭
request.header.add("token": "xxxxx");
/// 等待請(qǐng)求結(jié)果
HttpClientResponse response = await request.close();
/// 解析數(shù)據(jù)
String responseBody = await response.transform(utf8.decoder).join();
/// 關(guān)閉client
httpClient.close();
一般情況下,我們很少使用到HttpClient來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,在Flutter大部分還是使用第三方封裝好的SDK,比如Dio
Dio
Flutter中出名的網(wǎng)絡(luò)請(qǐng)求框架,除了基本的網(wǎng)絡(luò)請(qǐng)求之外還設(shè)置了攔截器、代理等相當(dāng)使用的功能。
具體的實(shí)現(xiàn)可以去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);
}
}