Flutter 太好學了!BUG 真的太少了! issues 只有 5000 多!也就那麼億點!簡單得我都枯了!畢竟每次遇到問題,👴🏻 都是直接去找羣裏的法佬、低調、Alex 等幾位大佬(🐶管理,此處小聲嗶嗶)來解決,只要有大佬在,問題也就不大。雖然法佬常常說要學會看源碼,但道理你們其實都懂,看源碼也就圖一樂,真正有 BUG 仍是得找法佬。css
很少嗶嗶,單寫一篇文章,先記錄它一手。本文記錄 👴🏻 在 Flutter 開發中遇到的一些 BUG(as design),避免遺忘,若是正在看文章的你也遇到了,那我們能夠握個手。html
通常是因爲父級容器的 constraints
屬性引發的,在 Flutter 中,子組件的大小會被父組件的 constraints
屬性限制,例如android
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 100.0, // 最小寬度爲 100 像素
minHeight: 50.0 // 最小高度爲 50 像素
),
child: Container(
height: 5.0,// 高度爲 5 邏輯像素
child: redBox
),
)
複製代碼
上面的代碼中,Container
組件設置高度爲 5 像素,是沒法生效的,由於父級容器已經設置了最小高度爲 50 像素,因此 Container
組件的最終高度將會是 50 像素。git
固然,這確定不是咱們想要的效果,咱們就想讓 Container
組件的最終高度是 5 像素怎麼辦?其實很簡單,可使用 UnconstraindBox
解除父級容器的 constraints
屬性對子組件大小的限制。例如:github
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 100.0, // 最小寬度爲 100 像素
minHeight: 50.0 // 最小高度爲 50 像素
),
child: UnconstraintsBox(
child: Container(
height: 5.0,
child: redBox
),
),
)
複製代碼
UnconstrainedBox
容許其子組件按照其自身的大小繪製,咱們不多直接使用此組件,除非對於 Material 自帶的一些組件,如 Appbar
的 icon 被官方限制了固定的大小,利用該組件能夠解除限制,而通常狀況下,咱們在組件外面套一層佈局類組件就能夠解決需求,例如如下組件:json
Row()
Column()
Align()
Center()
Flex()
Wrap()
Flow()
Stack()
複製代碼
其實和上面這個問題是類似的,可使用佈局類組件解決,或者用以下方式:微信
Container(
alignment: Alignment.topLeft,
child: SingleChildScrollView(),
),
複製代碼
若是你看過 Container
的源碼你會發現其實設置 alignment
屬性,和用 Align
組件是一回事,源碼也是使用 Align
組件,這就是個語法糖,僅此而已。網絡
說到語法糖,其實 Center
組件也是 Align
組件的語法糖,當你不給 Align
傳遞任何參數時,使用 Center()
和使用 Align()
是如出一轍的效果,個人習慣是無論什麼狀況,都是隻用Align
組件。app
設置 borderRadius
有兩種作法,第一種使用 Container
等組件自帶的 borderRadius
屬性,第二種是,直接用 ClipRRect
等 clip 組件對容器進行裁剪,第二種比第一種更加暴力、消耗性能,但更有效。less
例如給 TabView
的容器設置 borderRadius
,你會發現沒法生效,而使用 ClipRRect
則能夠解決,個人理解是 ClipRRect
會直接裁剪成圓角形狀,而 BorderRadius
的圓角外的弧形範圍是透明的,相似 css
中的 display:none
與 opaticy:0
的區別,實際具體是什麼緣由,我也沒有去細究,複製粘貼、能跑就行。
能夠認爲 Flutter 中 widget
佈局的層級關係是遞進的,例如 child
的層級比父 Widget
層級更高, Column
、Row
等組件的 children
中同級 widget
,誰在後面誰的層級就更高,和 Stack
其 children
的層級關係相同。
第一種,利用 IndexedStack
組件控制層級,上面也提到過,子組件誰在後面誰的層級就高,Flutter 中雖然沒有 z-index
這一說法,但其實原理和 css 的 z-index
是相似的,index 越大,層級越高,固然這裏的 IndexedStack
的 index
屬性是用來控制當前顯示的某一個 children,只能顯示一個。該方法經常使用於 APP 首頁切換底部導航。
第二種,利用 IgnorePointer
及 Opacity
組件組合隱藏 widget,可使用 AnimationOpacity
組件達到之前 JQuery
中經常使用的 fadeIn
效果。
第三種,利用 Positioned
或 Transform.translate
移動到屏幕外,須要顯示時再移動回來,這種作法很是適合動畫切換,例如視頻進度條等效果。
第四種,利用 Offstage
組件,前三種都是利用視覺效果將元素隱藏起來,其實在佈局上並未發生改變,而此組件就是相似於 css 中的 display:none
,直接讓元素在佈局中隱藏,不會在佈局上繼續佔用空間。
最後一種,在 build 方法中提早判斷,不符合條件直接不渲染,或者返回空 box,這就相似於 HTML 中刪除 dom 元素,我人沒了,還顯示個🔨,這是最恐怖的。
Listener
默認的 behavior
是 HitTestBehavior.deferToChild
若是 Listener
的子組件是一個 Container
,這個 Container
不設置 decoration
的狀況下,即透明背景色、無邊框,則點擊 Container
時,沒法觸發 down、up
等事件。
同理,GestureDetector
是對 Listener
的封裝,沒法觸發 onTap
等事件也是必然的,那麼解決辦法也很簡單,有如下兩種解決辦法:
1. 給 Container 設置 decoration
2. 將 behavior 屬性設置爲 opaque 或 translucent
複製代碼
setState() or markNeedsBuild() called during build
遇到此提示,通常解決思路都是利用 addPostFrameCallback
來解決,例如:
WidgetsBinding.instance.addPostFrameCallback((_){
_model.setOpacity(opacity);
});
複製代碼
setState() called after dispose()
通常定時器在 app 返回桌面後仍在調用 setState
或 頁面 pop 銷燬後異步任務才完成,此時調用了 setState
必然會出現該提示,那麼解決辦法也很簡單,判斷生命週期再執行重構邏輯。
if (!mounted) return;
setState(() {
// do somthing
});
複製代碼
解決辦法:在 scafold
裏設置 resizeToAvoidBottomInset: false
,鍵盤會遮住佈局,而不是頂起佈局。
溢出確定是由於沒有鍵盤時,總體高度沒有一屏高,鍵盤出現了,卻超出了一屏的高度。解決辦法很簡單,首先將佈局使用 SingleChildScrolleView
之類的滾動組件包裹住,將佈局改變可爲滾動的,這樣鍵盤彈出後佈局就不會溢出了。
接着可使用 WidgetsBindingObserver
類來監聽鍵盤彈起事件,每次彈起鍵盤出觸發 didChangeMetrics
鉤子,在該鉤子裏執行邏輯便可,例如將 SingleChildScrolleView
的當前位置調整至最底部,相關代碼以下:
import 'package:flutter/material.dart';
class Demo extends StatefulWidget {
@override
createState() => _DemoState();
}
class _DemoState extends State<Demo> with WidgetsBindingObserver {
final _scrollController = ScrollController();
final _phoneController = TextEditingController();
FocusNode _phoneFocusNode = FocusNode();
FocusScopeNode _focusScopeNode;
get _phoneTextFiled => TextField(
controller: _phoneController,
focusNode: _phoneFocusNode,
keyboardType: TextInputType.phone,
maxLength: 11,
decoration: InputDecoration(
hintText: '請輸入手機號',
border: InputBorder.none,
counterText: '',
),
);
void handlePostFrame() {
if (!_phoneFocusNode.hasFocus) {
print('requestFocus');
_focusScopeNode.requestFocus(_phoneFocusNode);
}
print('jumpTo');
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangeMetrics() {
WidgetsBinding.instance.addPostFrameCallback(handlePostFrame);
super.didChangeMetrics();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
複製代碼
個人項目中有一個接近 1 萬行代碼的視頻詳情頁,所有使用 Provider
進行狀態管理,若是鍵盤彈起回收觸發 build,就可能出現一些奇怪的 BUG,好比當前的滾動組件在屏幕中的位置發生變化。
個人解決方案是利用 showBottomSheet
方法,頁面中展現的 TextField
上蓋一層透明遮罩,使用戶沒法點擊,而點擊遮罩時,則觸發 showBottomSheet
, push 進一個新的路由,彈起鍵盤,卻不會引發從新 build,收起鍵盤時,則會 pop 回頁面,其實視覺上一直都保持在同一頁面中,和普通的彈起鍵盤沒區別,而且性能也很是棒,相關代碼以下:
get textField => TextField(
autofocus: true,
cursorColor: currentTheme.hoverColor,
cursorWidth: 1.0,
textInputAction: TextInputAction.done,
style: TextStyle(
color: currentTheme.primaryColorLight,
fontSize: setSp(32),
),
decoration: InputDecoration(
hintText: '發一句友善的評論來見證當下吧',
hintStyle: TextStyle(fontSize: setSp(28)),
contentPadding: EdgeInsets.symmetric(horizontal: setWidth(31)),
filled: true,
fillColor: currentTheme.primaryColorDark,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(setWidth(30)),
borderSide: BorderSide.none
),
),
onSubmitted: (value) {},
);
Widget buildTextFieldPage(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomLeft,
children: <Widget>[
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.pop(context),
child: Container(color: Colors.black.withOpacity(.5)),
),
),
buildInput(),
],
),
);
}
buildInput({hasTextField = true}) {
Widget child;
child = hasTextField
? Container(
decoration: BoxDecoration(
color: currentTheme.backgroundColor,
borderRadius: BorderRadius.circular(setWidth(31)),
),
child: textField,
)
: GestureDetector(
onTap: () {
showBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: buildTextFieldPage,
);
},
child: Container(
decoration: BoxDecoration(
color: currentTheme.backgroundColor,
borderRadius: BorderRadius.circular(setWidth(31)),
),
),
);
return Container(
height: setWidth(103),
padding: EdgeInsets.symmetric(
vertical: setWidth(20),
horizontal: setWidth(25),
),
decoration: BoxDecoration(
border: Border(top: commentDivider),
color: currentTheme.primaryColor,
),
child: Row(
children: <Widget>[
Expanded(child: child),
Container(
width: setWidth(66),
padding: EdgeInsets.only(left: setWidth(25)),
alignment: Alignment.center,
child: Icon(
IcoMoon.send,
color: currentTheme.hoverColor.withOpacity(.5),
size: setWidth(42),
),
),
],
),
);
}
複製代碼
相關效果以下:
例如瀏覽記錄中有以下 4 個頁面,當前頁面爲 d
:
a->b->c->d
複製代碼
在當前頁面使用 Navigator.popUtil(context, ModalRoute.withName('a'))
,能夠直接返回至 a
頁面,並銷燬 b
、c
頁面。
在當前頁面使用 Navigator.pushNamedAndRemoveUntil(context, 'e', (route) => false)
,能夠進入 e
頁面以前,銷燬全部歷史記錄,即 e
頁面變成第一頁,e
頁面裏沒法繼續 pop
返回上一頁。
提示以下:
Automatically assigning platform iOS
with version 9.0
on target Runner
because no platform was specified. Please specify a platform for this target in your Podfile.
解決辦法是:刪除 pod
文件中 platform
前的 #
由於沒有作過原生開發,因此對於這種 build 問題真的是一臉茫然,最開始遇到過幾回相似錯誤,我經過網上搜索答案、羣裏問大佬來解決,很是之麻煩。因此後來我在 Mac 環境 build 產生錯誤時,都是直接重建項目,把邏輯代碼複製進新項目裏,再從新 build 就不會發生各類亂七八糟看不懂的錯誤了,效率也快。
解決辦法:給滾動組件加上 key
屬性,用於保存位置信息,例如: key: PageStorageKey(1)
其實通常的 ListView 還沒法知足咱們平常開發中各類花式的需求,推薦使用法佬的 NestedScrollView
法佬已經給咱們解決了不少奇怪的 bug,還要什麼自行車?
我須要當 app 返回桌面時暫停視頻的播放,從桌面返回 app 後再繼續播放,解決方案以下:
class _DemoState extends State<Demo> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('app lifecycle state: $state');
if (state == AppLifecycleState.inactive) {
_playerModel.pausePlayer();
} else if (state == AppLifecycleState.resumed) {
if (_homeModel.isFindPage) _playerModel.startPlayer();
}
super.didChangeAppLifecycleState(state);
}
}
複製代碼
WidgetsBindingObserver 這個類我常用,例如監聽鍵盤彈起事件也會用到這個類。
TextField 的 border
有以下 3 種,須要針對性地設置,只設置一個是沒法生效的:
decoration: InputDecoration(border enabledBorder focusBorder)
複製代碼
ps:設置 maxLength
屬性後,decoration
裏須要設置 counterText: ''
,不然默認會附帶一個統計字數的樣式。
使用 Dio 進行 HTTP 請求時,請求頭 content-type
的默認值是
application/json; charset=utf-8
複製代碼
若是返回頭的 content-type
是
application/json
複製代碼
Dio 將自動解析返回 json 數據爲 Dart 相應的數據類型,而不須要手動地調用 jsonDecode
方法,因此客戶端、服務端的統一使用 application/json
做爲 content-type
,他好我也好。
在我第一次使用 Flutter 打包項目時遇到了這個問題,最後發現是沒有網絡請求的權限,相似的,儲存讀取本地文件時可能也會有相似問題,這種問題設置權限就能夠解決了。
在 android/app/src/profile/AndroidManifest.xml
中
以及 android/app/src/main/AndroidManifest.xml
兩個文件的 manifest
標籤內添加以下子標籤便可:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
複製代碼
不引用其餘屬性的成員,定義爲屬性
引用其餘屬性,且不接收參數的成員,定義爲getter
引用其餘屬性,且接受參數的成員,定義爲function
強制豎屏:
void initState() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown
]);
super.initState();
}
複製代碼
強制橫屏:
initState() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight
]);
super.initState();
}
複製代碼
推薦使用 Transform
組件來完成動畫效果,例如 Transform.translate
和 Transform.scale
能夠完成位置、縮放的變化, Transform.rotate
能夠完成旋轉角度的變化。
Transform.rotate
和 RotateBox
均可以完成旋轉功能,他們之間有什麼區別?
使用 RotateBox
渲染 widget 是在 layout 階段,渲染完畢後就會佔用實際位置,而 Transform
組件則是在 layout 以後的繪製階段, Transform
只是一個視覺效果,實際所佔空間大小是 transform 變化以前所佔用的空間大小,因此從新渲染 Transform.rotate
組件比從新渲染 RotateBox
開銷更小。
Flutter 的 Transform
組件的這個特性和 CSS 的 transform
屬性很是類似,均可以用來提高動畫性能。
不過作視頻全屏功能時,能夠用 IndexedStack
+ RotateBox
替代 push 一個橫屏的路由的作法,RotateBox
它會使容器填充全屏,而 IndexedStack
能夠控制是否顯示全屏,這裏若是使用 Transform
則沒法填充全屏,由於容器的寬高在 layout 時就已經肯定了,因此只能使用 RotateBox
。
我在項目中不只使用 RotatedBox 完成視頻全屏功能,還利用了 Transform
來完成鏡像翻轉功能,寫法以下:
Selector<VideoModel, bool>(
selector: (context, model) => model.isMirror,
builder: (context, isMirror, child) => Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateY(isMirror ? math.pi : 0),
child: child,
),
child: FijkView(
player: model.player,
color: Colors.black,
panelBuilder: (player, context, size, pos) => emptyBox,
),
)
複製代碼
原理很簡單,FijkView 是 fijkplayer 提供的視頻容器,我將視頻容器以中心位置爲圓心,沿 Y 軸作一個 180 度的旋轉,便可知足需求。
setEntry
用於設置透視,不然將沒法看到 Y 軸及 X 軸的立體轉換效果
rotateY
則與 css 中的 rotateY
是相同含義,即沿 Y 軸旋轉。在 css 中能夠設置 transform: rotateY(180deg)
來達到相同的效果。
隱藏狀態欄:
import 'package:flutter/services.dart';
void toggleFullscreen() {
_isFullscreen = !_isFullscreen;
_isFullscreen
? SystemChrome.setEnabledSystemUIOverlays([])
: SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
複製代碼
改變狀態欄顏色,則須要使用插件:flutter_statusbarcolor
,下面是用法示例:
// 改變狀態欄背景顏色,默認改變爲透明
Future<void> changeStatusColor({Color color: Colors.transparent}) async {
try {
await FlutterStatusbarcolor.setStatusBarColor(
color,
animate: true,
);
FlutterStatusbarcolor.setStatusBarWhiteForeground(true);
FlutterStatusbarcolor.setNavigationBarWhiteForeground(true);
} on PlatformException catch (e) {
debugPrint(e.toString());
}
}
複製代碼
下面介紹一個用法,個人 home 頁使用 indexStack
組件包含了 4 個 tab 頁,每次更改 tab 會改變 currentHomeTab
的值,但不會觸發從新 build,而因爲路由 push
或 pop
又會觸發從新 build,因此若是須要當進入 home 頁的 發現 tab 頁
時改變爲黑色狀態欄,則能夠用下面這種作法:
// 在發現頁的 build 方法裏進行判斷
@override
Widget build(BuildContext context) {
if (ModalRoute.of(context).isCurrent && currentHomeTab == '發現') {
changeStatusColor(color: Colors.black);
}
}
複製代碼
fijkplayer 默認狀況下,進度跳轉、播放可能會有性能問題,針對這些問題,能夠進行如下優化:
_player.setDataSource(_video.src);
await _player.applyOptions(
FijkOption()
..setFormatOption('flush_packets', 1)
..setFormatOption('analyzemaxduration', 100)
..setFormatOption('analyzeduration', 1)
..setCodecOption('skip_loop_filter', 48)
..setPlayerOption('start-on-prepared', 1)
..setPlayerOption('packet-buffering', 0)
..setPlayerOption('framedrop', 1)
..setPlayerOption('enable-accurate-seek', 1)
..setPlayerOption('find_stream_info', 0)
..setPlayerOption('render-wait-start', 1)
);
await _player.prepareAsync();
複製代碼
參考連接:
IjkPlayer 播放器秒開優化以及經常使用 Option 設置
我寫了下面這個工具類,簡單、好用得我都枯了,原理是利用先 LayoutBuilder
判斷是否超出指定的行數,若是超出則返回 Column
,若是未超出則返回原 widget
import 'package:flutter/material.dart';
class ExpandableText extends StatefulWidget {
final String text;
final int maxLines;
final TextStyle style;
final bool expand;
final TextStyle markerStyle;
final String atName;
const ExpandableText(this.text, {
Key key,
this.maxLines,
this.style,
this.markerStyle,
this.expand = false,
this.atName = '',
}) : super(key: key);
@override
createState() => _ExpandableTextState();
}
class _ExpandableTextState extends State<ExpandableText> {
bool expand;
TextStyle style;
int maxLines;
@override
void initState() {
expand = widget.expand;
style = widget.style;
maxLines = widget.maxLines;
super.initState();
}
Widget buildOrdinaryText() {
final text = widget.text;
return LayoutBuilder(builder: (_, size) {
final tp = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr,
);
tp.layout(maxWidth: size.maxWidth);
if (!tp.didExceedMaxLines) return Text(text, style: style);
return Builder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(text, maxLines: expand ? null : widget.maxLines, style: style),
GestureDetector(
onTap: () {
expand = !expand;
(context as Element).markNeedsBuild();
},
child: Text(
expand ? '收起' : '展開',
style: widget.markerStyle,
),
),
],
),
);
});
}
Widget buildAtText() {
return LayoutBuilder(builder: (_, size) {
final tp = TextPainter(
text: TextSpan(text: '回覆 @${widget.text}:', style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr,
);
tp.layout(maxWidth: size.maxWidth);
if (!tp.didExceedMaxLines) return Text.rich(
TextSpan(
children: [
TextSpan(text: '回覆 '),
TextSpan(text: '@${widget.atName}', style: widget.markerStyle),
TextSpan(text: ':${widget.text}'),
],
),
style: style,
);
return Builder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text.rich(
TextSpan(
children: [
TextSpan(text: '回覆 '),
TextSpan(text: '@${widget.atName}', style: widget.markerStyle),
TextSpan(text: ':${widget.text}'),
],
),
maxLines: expand ? null : widget.maxLines,
style: style,
),
GestureDetector(
onTap: () {
expand = !expand;
(context as Element).markNeedsBuild();
},
child: Text(
expand ? '收起' : '展開',
style: widget.markerStyle,
),
),
],
),
);
});
}
@override
build(context) => widget.atName == '' ? buildOrdinaryText() : buildAtText();
}
複製代碼
調用方法以下:
Container(
padding: EdgeInsets.only(top: setWidth(6), bottom: setWidth(11)),
alignment: Alignment.centerLeft,
child: ExpandableText(
reply.content,
maxLines: 4,
style: commentTextStyle,
markerStyle: commentMarkerStyle,
atName: reply.isDirect > 0 ? '' : reply.pNickname,
),
),
複製代碼
相關效果以下:
LayoutBuilder 的做用很是大,能夠用它來監聽某個widget的寬高信息,我在項目中遇到了 一個需求,須要根據某個 widget 的高度來彈出 BottomSheet,而這個 widget 的高度是能夠滑動改變的,那麼 LayoutBuilder
就派上用場了,作法以下:
須要監聽的 widget
是 Body()
組件,給 Body() 組件套上一個 Stack
get body => Stack(
children: <Widget>[
Body(),
BodyLayout(model),
],
);
複製代碼
而後用 BodyLayout
組件來監聽:
import 'package:flutter/material.dart';
import 'package:vhiphop/provider/video/video_model.dart';
class BodyLayout extends StatelessWidget {
final VideoModel model;
BodyLayout(this.model);
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (_, BoxConstraints constraints) {
model.bottomSheetDy = constraints.maxHeight;
return emptyBox;
});
}
}
複製代碼
當 Body()
組件高度發生變化時,會觸發 LayoutBuilder
的 builder
回調函數,在此函數中將高度信息傳遞給 model
,那麼每次彈出 BottomSheet
以前,我就能夠從 model 中拿到高度,以設置 BottomSheet 的高度。
這種動畫在 App 中是很常見的效果,例如 App 分享功能,點擊分享按鈕後,會從頁面底部彈出分享組件。
第一種,利用 showModalBottomSheet
,相關實現代碼以下:
void showShareBottomSheet() {
showModalBottomSheet(
elevation: 0,
backgroundColor: currentTheme.highlightColor,
context: context,
builder: (context) => Container(
width: Screens.width,
decoration: BoxDecoration(color: currentTheme.primaryColor),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
alignment: Alignment.bottomLeft,
height: setWidth(59),
padding: EdgeInsets.only(left: setWidth(42)),
child: Text(
'分享',
style: TextStyle(
fontSize: setSp(32),
color: currentTheme.highlightColor,
),
),
),
Container(
height: setWidth(206),
padding: EdgeInsets.only(top: setWidth(33), left: setWidth(33)),
alignment: Alignment.topLeft,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: setWidth(.7),
color: currentTheme.dividerColor,
),
),
),
child: Row(
children: <Widget>[
shareIconOfQQ,
shareIconOfQQZone,
shareIconOfWeChat,
shareIconOfWeChatMoments,
shareIconOfMicroBlog,
],
),
),
Container(
height: setWidth(206),
padding: EdgeInsets.only(top: setWidth(33), left: setWidth(33)),
alignment: Alignment.topLeft,
child: Row(
children: <Widget>[
shareIconOfLink,
],
),
),
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Container(
width: Screens.width,
height: setWidth(125),
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: setWidth(10),
color: currentTheme.backgroundColor,
),
),
),
child: Text(
'取消',
style: TextStyle(
fontSize: setSp(36),
color: currentTheme.highlightColor,
),
),
),
),
],
),
),
);
}
複製代碼
我在項目中使用 showModalBottomSheet
時發現動畫有點卡頓,多是測試手機不行,只花了 1000 大洋,但咱是個倔強窮人,非要找一種性能更好的方式,那就是 translate
了。
這種方法比 showModalBottomSheet
動畫性能更高,在我 1000 大洋的測試機 debug 模式下都很是地絲滑流暢,只是代碼實現更復雜一點,而且須要依賴 Provider 來更新,我比較喜歡這種方式。
整個頁面都使用 Stack
構建,而 bottomSheet 與遮罩 box 則使用 Positioned
定位至頁面底部:
get body => Stack(
children: <Widget>[
page,
Positioned(
left: 0,
bottom: 0,
right: 0,
child: bottomSheetBox,
),
Positioned(
left: 0,
top: 0,
right: 0,
bottom: shareBottomSheetHeight,
child: bottomSheetBoxMask,
),
],
);
複製代碼
接着使用我定義的一個工具類,名字叫 AnimatedTranslateBox
,我發現 Animated 家族有各類動畫組件,好比 AnimatedPadding
、AnimatedPositioned
等等,惟獨沒有 Translate
,不知道官方是什麼意思,可能他們以爲 Positioned
來調整位置就夠用了叭,但是 translate
動畫性能更高,它不香嗎?不要緊,咱本身造了一個,代碼以下:
import 'package:flutter/material.dart';
class AnimatedTranslateBox extends StatefulWidget {
AnimatedTranslateBox({
Key key,
this.dx,
this.dy,
this.child,
this.curve = Curves.linear,
this.duration = const Duration(milliseconds: 200),
this.reverseDuration,
});
final double dx;
final double dy;
final Widget child;
final Duration duration;
final Curve curve;
final Duration reverseDuration;
@override
createState() => _AnimatedTranslateBoxState();
}
class _AnimatedTranslateBoxState extends State<AnimatedTranslateBox> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
Tween<double> tween;
void _updateCurve() {
animation = widget.curve == null
? controller
: CurvedAnimation(parent: controller, curve: widget.curve);
}
@override
void initState() {
super.initState();
controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
tween = Tween<double>(begin: widget.dx ?? widget.dy);
_updateCurve();
}
@override
void didUpdateWidget(AnimatedTranslateBox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve) _updateCurve();
controller
..duration = widget.duration
..reverseDuration = widget.reverseDuration;
if ((widget.dx ?? widget.dy) != (tween.end ?? tween.begin)) {
tween
..begin = tween.evaluate(animation)
..end = widget.dx ?? widget.dy;
controller
..value = 0.0
..forward();
}
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
build(context) => AnimatedBuilder(
animation: animation,
builder: (context, child) => widget.dx == null
? Transform.translate(
offset: Offset(0, tween.animate(animation).value),
child: child,
)
: Transform.translate(
offset: Offset(tween.animate(animation).value, 0),
child: child,
),
child: widget.child,
);
}
複製代碼
調用很簡單,使用 Selector
依賴 model 中的布爾值,用於控制顯示隱藏:
get bottomSheetBox => Selector<VideoModel, bool>(
selector: (context, model) => model.showBottomSheet,
builder: (context, show, child) => AnimatedOpacity(
opacity: show ? 1 : 0,
curve: show ? Curves.easeOut : Curves.easeIn,
duration: bottomSheetDuration,
child: AnimatedTranslateBox(
dy: show ? 0 : bottomSheetHeight,
curve: show ? Curves.easeOut : Curves.easeIn,
duration: bottomSheetDuration,
child: child,
),
),
child: Container(
height: bottomSheetHeight,
child: bottomSheet,
),
);
複製代碼
每當 dx
或 dy
的值發生改變,AnimatedTranslateBox
的 child 就會根據 dx
或 dy
的值進行 y 軸 或 x 軸的移動動畫。
相關的效果以下:
我發現若是在 MaterialApp
下全局掛載了 Provider ,則在 Home 頁初始化完成前,是沒法使 Provider 的,例如:
class MyApp extends StatelessWidget {
final _userModel = UserModel();
final _homeModel = HomeModel();
Widget build(BuildContext context) {
return OKToast(
dismissOtherOnShow: true,
child: MultiProvider(
providers: [
ChangeNotifierProvider.value(value: _userModel),
ChangeNotifierProvider.value(value: _homeModel),
],
child: Selector<ThemeModel, ThemeData>(
selector: (context, model) => model.theme,
builder: (context, theme, child) => MaterialApp(
navigatorKey: Constants.navigatorKey,
debugShowCheckedModeBanner: false,
theme: theme,
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
},
),
),
),
);
}
}
複製代碼
上面的代碼聲明瞭 MultiProvider
,若是在首頁作以下調用:
@override
initState() {
_model = Provider.of<HomeModel>(context);
_userModel = Provider.of<UserModel>(context);
super.initState();
}
複製代碼
則會報錯:
I/flutter ( 8380): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 8380): The following assertion was thrown building Builder:
I/flutter ( 8380): dependOnInheritedWidgetOfExactType<_DefaultInheritedProviderScope<HomeModel>>() or
I/flutter ( 8380): dependOnInheritedElement() was called before _HomePageState.initState() completed.
I/flutter ( 8380): When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
I/flutter ( 8380): widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
I/flutter ( 8380): or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
I/flutter ( 8380): inherited widget.
I/flutter ( 8380): Typically references to inherited widgets should occur in widget build() methods. Alternatively,
I/flutter ( 8380): initialization based on inherited widgets can be placed in the didChangeDependencies method, which
I/flutter ( 8380): is called after initState and whenever the dependencies change thereafter.
複製代碼
提示 initState
必須調用完成,才能使用 Provider.of
來獲取祖先節點的 model,非要使用怎麼辦?辦法也很簡單, of
方法有一個屬性值 listen
,默認值爲 true
,將此值設置爲 false
則不會創建與 Provider 的依賴關係,其實我在 Provider 的手冊中也發現,建議在 initState
方法中調用 of
時,將 listen
設置爲 false
:
@override
initState() {
_userModel = Provider.of<UserModel>(context, listen: false);
_model = Provider.of<HomeModel>(context, listen: false);
super.initState();
}
複製代碼
分析一下,其實這種效果特別簡單,首先放大背景圖片,其次對圖片進行高斯模糊,直接上代碼:
import 'package:flutter/material.dart';
import 'dart:ui';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final image = Image.asset(
'assets/images/test.jpg',
fit: BoxFit.cover,
width: 200,
height: 200,
);
get blurImage => ClipRRect(
child: Stack(
children: <Widget>[
Transform.scale(
scale: 1.5,
child: image,
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
width: 200,
height: 200,
alignment: Alignment.center,
color: Colors.black.withOpacity(.3),
child: Text(
'1 個內容',
style: TextStyle(
fontSize: 24,
color: Colors.white,
),
),
),
),
],
),
);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo app',
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(title: Text('blur image demo')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 30),
child: image,
),
blurImage,
],
),
],
)
),
);
}
}
複製代碼
這個效果其實沒什麼難度,主要的知識點在於 BackdropFilter
組件默認的模糊效果是全屏的,必須使用 ClipRRect
進行裁剪,並且 Transform
的幾個命名構造函數,如 Transform.translate
帶來的效果是在繪製階段發生的,會超出 widget
實際佔用的空間,也須要使用 ClipRRect
進行裁剪,最後的效果圖以下: