時(shí)間:2022-08-06 22:21:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2022-08-06 22:21:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
Flutter
搭建微信首頁(yè)的功能,詳細(xì)講述該功能實(shí)現(xiàn)過(guò)程中所運(yùn)用到的技術(shù),以及遇到問(wèn)題后如何解決的心得體會(huì)。該功能雖然粗看時(shí)看似簡(jiǎn)單,但是細(xì)作時(shí)發(fā)現(xiàn)其功能邏輯復(fù)雜,內(nèi)部細(xì)節(jié)處理較高,當(dāng)然其中涵蓋了Flutter
中大部分知識(shí)點(diǎn),筆者相信初學(xué)者通過(guò)實(shí)現(xiàn)該功能后,定會(huì)對(duì)所學(xué)的Flutter
知識(shí)的掌握上更上一層樓
。Flutter
的過(guò)程中有所幫助,當(dāng)然筆者必將知無(wú)不言、言無(wú)不盡
,梳理實(shí)戰(zhàn)過(guò)程之問(wèn)題,總結(jié)解決問(wèn)題之方案,讓爾等知其然,知其所以然
。望能拋玉引磚,擺渡眾生,如有紕漏,還望斧正。Stack + Positioned 布局
Transform.translate(平移)
、Transform.scale(放大)
、Opacity(設(shè)置子部件透明度)
滾動(dòng)監(jiān)聽(tīng)及控制
動(dòng)畫組件使用(AnimatedPositioned、AnimatedOpacity、ScaleTransition)
狀態(tài)管理Provider
監(jiān)聽(tīng)鍵盤彈起
通過(guò)GlobalKey 獲取某個(gè) Widget 的尺寸
側(cè)滑刪除
的功能,主要利用 flutter_slidable 插件來(lái)實(shí)現(xiàn)的,其具體實(shí)現(xiàn)過(guò)程以及細(xì)節(jié)處理的心得體會(huì),與筆者前面寫過(guò)的 Flutter 玩轉(zhuǎn)微信——通訊錄 文章中詳細(xì)說(shuō)明如何實(shí)現(xiàn)聯(lián)系人側(cè)滑刪除
的功能類似,這里筆者就不再一一贅述。有興趣的同學(xué),還請(qǐng)自行移步。Flutter
必備的知識(shí)點(diǎn),所以筆者會(huì)詳述其實(shí)現(xiàn)過(guò)程中遇到的坑以及填坑的方法。內(nèi)容頁(yè)
、導(dǎo)航欄
、三個(gè)點(diǎn)
、小程序
都會(huì)層疊展示,所以整個(gè)微信頁(yè)面這里采取的是 Stack + Positioned
布局方案,關(guān)于UI構(gòu)建的細(xì)節(jié),大家參看源碼即可,這里就不再贅述,具體偽代碼如下:/// 構(gòu)建子部件Widget _buildChildWidget() { return Container( constraints: BoxConstraints.expand(), color: Style.pBackgroundColor, child: Stack( overflow: Overflow.visible, // 注意層疊順序,她不像 Web 中有 z-index 的概念 children: <Widget>[ // 導(dǎo)航欄 // 內(nèi)容頁(yè) // 三個(gè)點(diǎn)部件 // 小程序 // 菜單 ], ), );}
特別注意:Stack
中子部件(Positioned
)添加順序,最后面添加的在最上面,她不像 Web
中的樣式有z-index
的概念。 下拉顯示小程序
和上拉隱藏小程序
兩個(gè)過(guò)程的邏輯處理,當(dāng)然這才是一個(gè)真正的閉環(huán),有顯示就會(huì)有隱藏。這里筆者就只拿以 下拉邏輯
為例,詳細(xì)講解其中的邏輯分析和細(xì)節(jié)處理。上拉邏輯
大家可以反推即可。導(dǎo)航欄
的頂部會(huì)隨著手指下拉而向下偏移(offset)
,偏移距離等于下拉距離。 臨界點(diǎn)① = 60
時(shí),出現(xiàn)一個(gè)小球
逐漸放大,放大系數(shù)(scale) = 0
,當(dāng) 偏移量 > 臨界點(diǎn)①
時(shí),scale 會(huì)逐漸變大
;反之,scale = 0
。 臨界點(diǎn)② = 90
時(shí),此過(guò)程中,小球
會(huì)放大到最大值(scale = 2
)。即offset:臨界點(diǎn)① --> 臨界點(diǎn)②
,scale: 0 --> 2
。繼 臨界點(diǎn)③ = 130
時(shí),此過(guò)程中,小球
會(huì)生成兩個(gè)小球,一個(gè)小球逐漸左平移到最大值,一個(gè)小球逐漸右平移到最大值,其本身也縮放到原始值(scale = 1
)。 臨界點(diǎn)④ = 180
時(shí),此過(guò)程中,三個(gè)球
的透明度(opacity
)從 1.0 --> 0.2
變化,以及小程序模塊透明度(opacity
)從0 --> 0.5
變化且自身縮放比例(scale
)為(scale = 0.4
)。 6. 繼續(xù)下拉 offset > 臨界點(diǎn)④
時(shí),三個(gè)小球
的透明度恒等于0.2
,以及小程序模塊透明度恒等于0.5
且自身縮放比例(scale
)恒為(scale = 0.4
)。拖拽
狀態(tài),也就是手指沒(méi)有離開(kāi)屏幕。那么手指離開(kāi)屏幕后,有會(huì)發(fā)生什么狀況呢,請(qǐng)聽(tīng)筆者一一道來(lái)。offset
是否大于 臨界點(diǎn)② = 90
, 若大于,則顯示小程序模塊,反之,則隱藏小程序模塊。0.5 --> 1
且縮放比例由0.4 --> 1
、底部導(dǎo)航欄隱藏。滾動(dòng)偏移量(offset)
,那么我們必須得監(jiān)聽(tīng)列表的滾動(dòng)
,從而根據(jù)偏移量來(lái)完成整個(gè)UI邏輯。關(guān)于滾動(dòng)監(jiān)聽(tīng),大家可以參看 滾動(dòng)監(jiān)聽(tīng)及控制 這篇文章。 // 方案一_controller.addListener(() { // 獲取偏移量 final offset = _controller.offset; // 處理 _handlerOffset(offset);});// 方案二NotificationListener( onNotification: (ScrollNotification notification) { // 正在刷新 do nothing... if (_isRefreshing || _isAnimating) { return false; } // offset final offset = notification.metrics.pixels; if (notification is ScrollStartNotification) { if (notification.dragDetails != null) { _focus = true; } } else if (notification is ScrollUpdateNotification) { // 能否進(jìn)入刷新?tīng)顟B(tài) final bool canRefresh = offset <= 0.0 ? (-1 * offset >= _topDistance ? true : false) : false; if (_focusState && notification.dragDetails == null) { _focus = false; // 手指釋放的瞬間 _isRefreshing = canRefresh; } } else if (notification is ScrollEndNotification) { if (_focusState) { _focus = false; } } // 處理 _handlerOffset(offset); return false;},
通過(guò)NotificationListener監(jiān)聽(tīng)滾動(dòng)事件
和通過(guò)ScrollController
有兩個(gè)主要的不同:NotificationListener
可以在從可滾動(dòng)組件到widget
樹根之間任意位置都能監(jiān)聽(tīng)。而ScrollController
只能和具體的可滾動(dòng)組件關(guān)聯(lián)后才可以。NotificationListener
在收到滾動(dòng)事件時(shí),通知中會(huì)攜帶當(dāng)前滾動(dòng)位置和ViewPort
的一些信息,而ScrollController
只能獲取當(dāng)前滾動(dòng)位置。NotificationListener監(jiān)聽(tīng)滾動(dòng)事件
的另一個(gè)重要原因是:監(jiān)聽(tīng)手指是否處于拖拽狀態(tài)
,即notification.dragDetails != null
。從而明確用戶手指離開(kāi)屏幕的瞬間時(shí),得到此時(shí)的偏移量,以此來(lái)決定小程序模塊
的顯示與否。_offset
的變化而變化即可,偏移量處理如下:// 處理偏移邏輯void _handlerOffset(double offset) { // 計(jì)算 if (offset <= 0.0) { _offset = offset * -1; } else if (_offset != 0.0) { _offset = 0.0; } // 這里需要 if (_isRefreshing && !_isAnimating) { // 刷新且非動(dòng)畫狀態(tài) // 正在動(dòng)畫 _isAnimating = true; // 動(dòng)畫時(shí)間 _duration = 300; // 最終停留的位置 _offset = ScreenUtil.screenHeightDp - kToolbarHeight - ScreenUtil.statusBarHeight; // 隱藏掉底部的TabBar Provider.of<TabBarProvider>(context, listen: false).setHidden(true); setState(() {}); return; } _duration = 0; // 非刷新且非動(dòng)畫狀態(tài) if (!_isAnimating) { setState(() {}); }}
因?yàn)榭紤]到UI布局依賴于_offset
的變化而變化,這里必須強(qiáng)調(diào)的是下拉過(guò)程中的兩種狀態(tài): - 拖拽狀態(tài)(手指未離開(kāi)屏幕) - 非拖拽狀態(tài)(手指離開(kāi)屏幕)拖拽狀態(tài)
下時(shí)UI,導(dǎo)航欄的頂部回跟隨_offset
的變化發(fā)生偏移,其無(wú)非是修改Positioned
的top
屬性即可,偽代碼如下:Positioned( top: _offset, //...)
當(dāng)結(jié)束 拖拽狀態(tài)
下時(shí)UI,即:如果手指釋放的瞬間,_offset
大于 臨界點(diǎn),則 導(dǎo)航欄
,內(nèi)容頁(yè)
...等部件會(huì)絲滑的過(guò)渡
到底部,這里想必大家一定清楚了,要想實(shí)現(xiàn)絲滑過(guò)渡
這個(gè)功能,一定離不開(kāi)動(dòng)畫
的加持。那么這種狀態(tài)下,若依然延用修改Positioned
的top
屬性方法就會(huì)在這個(gè)過(guò)程中顯得生硬
,所以這里采用Flutter
自帶的動(dòng)畫組件 AnimatedPositioned
來(lái)代替 Positioned
。 偽代碼如下:AnimatedPositioned( top: _offset, duration: Duration(milliseconds: 300), //...)
AnimatedPositioned
雖然輕而易舉的實(shí)現(xiàn)了非拖拽狀態(tài)
下時(shí) 導(dǎo)航欄
絲滑過(guò)渡到底部的功能,但是若處于拖拽狀態(tài)
下時(shí),用AnimatedPositioned
就會(huì)導(dǎo)致導(dǎo)航欄
很Q彈,比較差強(qiáng)人意。為了兼顧這兩種狀態(tài),筆者采用的是控制AnimatedPositioned
的duration
屬性來(lái)實(shí)現(xiàn)的,即:拖拽時(shí),_duration=0
;釋放且大于臨界點(diǎn)時(shí),_duration=300
。偽代碼如下:AnimatedPositioned( top: _offset, duration: Duration(milliseconds:(_isRefreshing ? 300 : 0)), //...)
當(dāng)然,筆者認(rèn)為下拉過(guò)程中比較有趣的功能點(diǎn)就是:三個(gè)小球邏輯
。當(dāng)然結(jié)合上面的功能分析,其實(shí)實(shí)現(xiàn)也比較簡(jiǎn)單,主要用到Opacity 、Transform.translate、Transform.scale
組件,且其使用比較高頻,大家很有必要掌握,這里筆者給出關(guān)鍵代碼邏輯,大家一看便知:// 階段I臨界點(diǎn)final double stage1Distance = 60;// 階段II臨界點(diǎn)final double stage2Distance = 90;// 階段III臨界點(diǎn)final double stage3Distance = 130;// 階段IV臨界點(diǎn)final double stage4Distance = 180;final top = (offset + 44 + 10 - 6) * 0.5;// 中間點(diǎn)相關(guān)double scale = 0.0;double opacityC = 0;// 右邊點(diǎn)相關(guān)double translateR = 0.0;double opacityR = 0;// 右邊點(diǎn)相關(guān)double translateL = 0.0;double opacityL = 0;final cOffset = (offset <= stage4Distance) ? offset : stage4Distance;if (offset > stage3Distance) { // 第四階段 1 - 0.2 final step = 0.8 / (stage4Distance - stage3Distance); double opacity = 1 - step * (cOffset - stage3Distance); if (opacity < 0.2) { opacity = 0.2; } // 中間點(diǎn)階段III: 保持scale 為1 opacityC = opacity; scale = 1; // 右邊點(diǎn)階段III: 平移到最右側(cè) opacityR = opacity; translateR = 16; // 左邊點(diǎn)階段III: 平移到最左側(cè) opacityL = opacity; translateL = -16;} else if (offset > stage2Distance) { final delta = stage3Distance - stage2Distance; final deltaOffset = offset - stage2Distance; // 中間點(diǎn)階段II: 中間點(diǎn)縮?。? -> 1 final stepC = 1 / delta; opacityC = 1; scale = 2 - stepC * deltaOffset; // 右邊點(diǎn)階段II: 慢慢平移 0 -> 16 final stepR = 16.0 / delta; opacityR = 1; translateR = stepR * deltaOffset; // 左邊點(diǎn)階段II: 慢慢平移 0 -> -16 final stepL = -16.0 / delta; opacityL = 1; translateL = stepL * deltaOffset;} else if (offset > stage1Distance) { final delta = stage2Distance - stage1Distance; final deltaOffset = offset - stage1Distance; // 中間點(diǎn)階段I: 中間點(diǎn)放大:0 -> 2 final step = 2 / delta; opacityC = 1; scale = 0 + step * deltaOffset;}
小程序模塊,在下拉過(guò)程中,只需要控制其透明度opacity
,以及內(nèi)容頁(yè)的縮放scale
系數(shù)即可,以及上拉過(guò)程中,控制好其透明度opacity
即可,總體來(lái)說(shuō),So Easy ~,當(dāng)然整個(gè)過(guò)程也是都需要考慮手指的 拖拽狀態(tài)
,也就是需要加動(dòng)畫,如:透明度動(dòng)畫、縮放動(dòng)畫。對(duì)此這里用到的對(duì)應(yīng)的動(dòng)畫組件如下, - AnimatedOpacity
替代 Opacity
,增加透明度動(dòng)畫 - ScaleTransition
替代 Transform.scale
,增加縮放動(dòng)畫小程序模塊
筆者覺(jué)得比較細(xì)節(jié)的地方,就是UI布局
上了。因?yàn)橐獙?shí)現(xiàn)上拉滑動(dòng),且小程序內(nèi)容頁(yè)也支持上下拉。所以就涉及到嵌套滑動(dòng)
,即ListView
嵌套ListView
。因?yàn)樽钔鈱拥纳侠瑒?dòng),能促使導(dǎo)航欄、內(nèi)容頁(yè)
向上偏移,所以最外層的ListView
的 maxScrollExtent:最大可滾動(dòng)長(zhǎng)度
的處理是比較細(xì)節(jié)的。也就是理想情況下,手指從屏幕最底部向上拖拽到屏幕最頂部,正好能使導(dǎo)航欄
的最頂部到達(dá)屏幕的頂部即可,那么maxScrollExtent = 2 * 屏幕的高度 - 狀態(tài)欄的高度 - 導(dǎo)航欄的高度
,且如果小程序內(nèi)容頁(yè)高度已知(假設(shè):480)。那么最外層的ListView
不僅要嵌套一個(gè)ListView(高度480)
,而且要嵌套一個(gè)空(占位)部件(SizedBox
),且空部件的高度為:占位部件高度 = 2 * 屏幕的高度 - 狀態(tài)欄的高度 - 導(dǎo)航欄的高度 - 480;
當(dāng)然上拉和下拉類似,無(wú)非也是監(jiān)聽(tīng)滾動(dòng),處理滾動(dòng)的偏移量,上拉的偏移量的處理代碼如下:/// 處理小程序滾動(dòng)事件void _handleAppletOnScroll(double offset, bool dragging) { if (dragging) { _isAnimating = false; // 去掉動(dòng)畫 _duration = 0; // 計(jì)算高度 _offset = ScreenUtil.screenHeightDp - kToolbarHeight - ScreenUtil.statusBarHeight - offset; // Fixed Bug: 如果是dragging 狀態(tài)下 已經(jīng)為0.0 ;然后 非dragging 也為 0.0 ,這樣會(huì)導(dǎo)致 即使 setState(() {}); 也沒(méi)有卵用 // 最小值為 0.001 _offset = max(0.0001, _offset); setState(() {}); return; } if (!_isAppletRefreshing && !_isAnimating) { // 開(kāi)始動(dòng)畫 _duration = 300; // 計(jì)算高度 _offset = 0.0; _isAppletRefreshing = true; _isAnimating = true; setState(() {}); }}
小模塊內(nèi)容頁(yè),也有個(gè)比較新穎的小功能:就是默認(rèn)每次進(jìn)來(lái)小程序模塊是隱藏搜索框
的,只有當(dāng)用戶下拉一丟丟,手指釋放時(shí),會(huì)自動(dòng)看到搜索框
,且用戶上拉一丟丟,手指釋放時(shí),也會(huì)自動(dòng)隱藏搜索框
的。實(shí)現(xiàn)這一功能主要涉及到兩個(gè)知識(shí)點(diǎn):監(jiān)聽(tīng)滾動(dòng)
和 控制滾動(dòng)
。其中監(jiān)聽(tīng)滾動(dòng)
肯定已經(jīng)耳熟能詳了,控制滾動(dòng)
有兩個(gè)常用API如下: jumpTo(double offset)
animateTo(double offset,...)
return NotificationListener( onNotification: (ScrollNotification notification) { if (notification is ScrollStartNotification) { if (notification.dragDetails != null) { // 記錄起始拖拽 _startOffsetY = notification.metrics.pixels; } } else if (notification is ScrollEndNotification) { final offset = notification.metrics.pixels; if (_startOffsetY != null && offset != 0.0 && offset < ScreenUtil().setHeight(60.0 * 3)) { // 如果小于 60 再去判斷是 下拉 還是 上拉 if ((offset - _startOffsetY) < 0) { // 下拉 Future.delayed( Duration(milliseconds: 10), () async { _controllerContent.animateTo(.0, duration: Duration(milliseconds: 200), curve: Curves.ease); }, ); } else { // 上拉 // Fixed Bug : 記得延遲一丟丟,不然會(huì)報(bào)錯(cuò) Why? Future.delayed( Duration(milliseconds: 10), () async { _controllerContent.animateTo(ScreenUtil().setHeight(60.0 * 3), duration: Duration(milliseconds: 200), curve: Curves.ease); }, ); } } // 這里設(shè)置為null _startOffsetY = null; } return true; // 阻止冒泡 }, child: ListView()}
但是如果我們?cè)诮Y(jié)束滾動(dòng)的一瞬間,調(diào)用 jumpTo(double offset) 或 animateTo(double offset,...)
其實(shí)是不起作用的,只有延遲一丟丟時(shí)間,再去控制其滾動(dòng)才行,這里筆者也是懵逼好久,還望有緣人解答一下哈(評(píng)論即可)~。tabBar
;上拉釋放時(shí),需要顯示底部tabBar
。這里就要用到狀態(tài)管理
的功能。 這里主要筆者借助 provider 來(lái)實(shí)現(xiàn)的。關(guān)鍵代碼如下:/// 用于控制TabBar 的顯示和隱藏class TabBarProvider with ChangeNotifier { // 顯示or隱藏 bool _hidden = false; bool get hidden => _hidden; void setHidden(bool hidden) { _hidden = hidden; notifyListeners(); }}// UI層return Consumer<TabBarProvider>( builder: (context, tabBarProvider, _) { return Scaffold( appBar: null, body: list[_currentIndex], // iOS bottomNavigationBar: tabBarProvider.hidden ? null : CupertinoTabBar( items: myTabs, onTap: _itemTapped, currentIndex: _currentIndex, activeColor: Style.pTintColor, inactiveColor: Color(0xFF191919), ), ); },);// 下拉釋放時(shí),隱藏Provider.of<TabBarProvider>(context, listen: false).setHidden(true);// 上拉釋放時(shí),顯示Provider.of<TabBarProvider>(context, listen: false).setHidden(false);
至此!下拉顯示小程序的功能點(diǎn)也就是以上這些了,當(dāng)然一些UI搭建和邏輯處理還是比較復(fù)雜的,只要你思維縝密,邏輯清晰,也就沒(méi)什么難得了。透明度動(dòng)畫AnimatedOpacity
和縮放動(dòng)畫ScaleTransition
組件。@overridevoid initState() { super.initState(); // 配置動(dòng)畫 _controller = new AnimationController( vsync: this, duration: Duration(milliseconds: 200)); _animation = new CurvedAnimation(parent: _controller, curve: Curves.easeInOut); // 監(jiān)聽(tīng)動(dòng)畫 _controller.addStatusListener((AnimationStatus status) { // 到達(dá)結(jié)束狀態(tài)時(shí) 要回滾到開(kāi)始狀態(tài) if (status == AnimationStatus.completed) { // 正向結(jié)束, 重置到當(dāng)前 _controller.reset(); setState(() {}); } });}@overrideWidget build(BuildContext context) { if (widget.show) { // 只有顯示后 才需要縮放動(dòng)畫 _shouldAnimate = true; _scaleBegin = _scaleEnd = 1.0; } else { _scaleBegin = 1.0; _scaleEnd = 0.5; // 處于開(kāi)始階段 且 需要?jiǎng)赢?/span> if (_controller.isDismissed && _shouldAnimate) { _shouldAnimate = false; _controller.forward(); } else { _scaleEnd = 1.0; } } // Fixed Bug: offstage 必須要等縮放動(dòng)畫結(jié)束后才去設(shè)置為 true, 否則 休想看到縮放動(dòng)畫 return Offstage( offstage: !widget.show && _controller.isDismissed, child: InkWell()}
結(jié)合 代碼,特別要注意的是,隱藏菜單時(shí),要加個(gè)判斷邏輯,只有當(dāng)顯示過(guò)菜單以及動(dòng)畫狀態(tài)正處于開(kāi)始狀態(tài)時(shí),才去進(jìn)行縮放動(dòng)畫,且動(dòng)畫完成后需要重置到初始狀態(tài),以便下次繼續(xù)縮放。當(dāng)然,一定要等縮放動(dòng)畫結(jié)束后,方可隱藏整個(gè)菜單(蒙版+內(nèi)容
),否則是看不到縮放動(dòng)畫的,因?yàn)槊砂鏁?huì)比內(nèi)容先隱藏。Flutter
的路上,很多知識(shí)都不夠全面,導(dǎo)致其實(shí)現(xiàn)過(guò)程中還是遇到了些許坑,這里筆者一一詳盡,所需知識(shí)點(diǎn)如下:通過(guò)GlobalKey 獲取某個(gè) Widget 的尺寸
AnimatedPositioned 實(shí)現(xiàn)平移動(dòng)畫
監(jiān)聽(tīng)鍵盤的高度變化
微信首頁(yè)
搜索框, 搜索
和 取消
按鈕同時(shí)向左平移,并且AppBar
和 Search
頁(yè)同時(shí)向上移動(dòng),鍵盤彈出;微信內(nèi)容頁(yè)
和 底部TabBar
隱藏,搜索頁(yè)面
展示,按住說(shuō)話
按鈕跟隨鍵盤彈出而彈出。 搜索頁(yè)的取消按鈕
, 搜索
和 取消
按鈕同時(shí)向右平移,并且AppBar
和 Search
頁(yè)同時(shí)向下移動(dòng),鍵盤收起;微信內(nèi)容頁(yè)
和 底部TabBar
顯示,搜索頁(yè)面
隱藏,清掉搜索內(nèi)容,按住說(shuō)話
按鈕跟隨鍵盤收起而收起。搜索框(SearchBar)
和搜索頁(yè)(SearchContent)
的搭建,雖整體不難,但細(xì)節(jié)滿滿。因?yàn)榭紤]平移(左移、右移)動(dòng)畫
和監(jiān)聽(tīng)鍵盤高度變化而變化的UI
,所以整體內(nèi)部widget
布局都是采用Stack + Positioned/AnimatedPositioned
來(lái)構(gòu)建的,當(dāng)然道路千萬(wàn)條,實(shí)現(xiàn)第一條。這里以SearchBar
為例,其內(nèi)部的子部件(widget
)布局,偽代碼如下:Stack( children: <Widget>[ // 白色背景框 AnimatedPositioned(), // 輸入框 Positioned(), // 搜索 按鈕 AnimatedPositioned(), // 取消按鈕 AnimatedPositioned() ])
搜索
居中實(shí)現(xiàn)。雖然UI實(shí)現(xiàn)居中可能比較簡(jiǎn)單,比如: Stack
的alignment: AlignmentDirectional.center,
和 Row
的mainAxisAlignment: MainAxisAlignment.center,
,以及 Container
的alignment: AlignmentDirectional.center,
等.... 但是需要考慮到動(dòng)畫的加入以及動(dòng)畫絲滑的效果,就不得不采用Stack
布局的形式了,以及采用Stack
的alignment: AlignmentDirectional.center,
來(lái)達(dá)到居中,且AnimatedPositioned
的left
和right
必須設(shè)置null
,不然是無(wú)法居中的,偽代碼如下:Stack( alignment: AlignmentDirectional.center, children: <Widget>[ // 搜索 按鈕 AnimatedPositioned( child: ` 搜索`, left : null, top: 0, bottom: 0 ), ])
雖然上述確實(shí)實(shí)現(xiàn)了 搜索
居中,且不費(fèi)吹灰之力。如果點(diǎn)擊 搜索
按鈕,假設(shè)此時(shí)是編輯模式,即 _isEdit = true;
,此時(shí) 搜索
按鈕需要加入左移動(dòng)畫,即AnimatedPositioned
的left : 0
,同學(xué)們可能會(huì)非常輕松的寫出如下代碼:Stack( alignment: AlignmentDirectional.center, children: <Widget>[ // 搜索 按鈕 AnimatedPositioned( child: ` 搜索`, left : _isEdit? 0 : null, top: 0, bottom: 0 ), ])
當(dāng)然,上述代碼邏輯確實(shí)是穩(wěn)如藏獒,但是一旦運(yùn)行后,你就會(huì)一臉懵逼,因?yàn)辄c(diǎn)擊 搜索
按鈕, 搜索
按鈕會(huì)嗖
的一下到達(dá)左側(cè),絲毫沒(méi)有理想情況下的左移的絲滑度。側(cè)面驗(yàn)證 理想34D(很豐滿),現(xiàn)實(shí)對(duì)A(很骨感)
的道理。 其實(shí)原因就是: AnimatedPositioned 的 left 是從 null --> 0 過(guò)渡的,若 left 有值過(guò)渡到 0 是有動(dòng)畫的。
AnimatedPositioned 的 left
在非編輯(_isEdit = false
)的場(chǎng)景下必須的有值 ,且為了保證 搜索
居中,則left必須滿足:left = (屏幕的寬度 - 搜索的寬度) * 0.5
,所以首要任務(wù)是獲取 搜索
按鈕的尺寸,這里采用GlobalKey
來(lái)獲取,關(guān)于GlobalKey
的使用,大家請(qǐng)自行百度。 偽代碼如下/// 用于獲取文字高度GlobalKey _textKey = new GlobalKey();/// 搜索圖標(biāo)距離左側(cè)的距離double _searchIconLeft = 0;@overrideWidget build(BuildContext context) { // 方案一: 先算出 SearchCube 的寬度,再去計(jì)算其位置 left ,雖然能實(shí)現(xiàn),但是初次顯示時(shí)會(huì)跳動(dòng) widgetUtil.asyncPrepare(context, true, (Rect rect) { final RenderBox box = _textKey.currentContext.findRenderObject(); final Size size = box.size; setState(() { _searchIconLeft = (rect.width - 16.0 - size.width) * .5; }); print('渲染完成 ${rect.size} $size ${size.width} $_searchIconLeft'); }); return Stack( alignment: AlignmentDirectional.center, children: <Widget>[ / / 搜索 按鈕 AnimatedPositioned( child: ` 搜索`, left : _isEdit? 0 : _searchIconLeft, top: 0, bottom: 0 ), ] ) ;}
上面完美實(shí)現(xiàn)了 搜索
按鈕居中,且左移動(dòng)畫縱享絲滑,但是在首次初始化的時(shí)候,會(huì)有跳動(dòng)的Bug,原因就是_searchIconLeft
初始化為0,導(dǎo)致在widgetUtil.asyncPrepare()
計(jì)算出來(lái)_searchIconLeft
,會(huì)有個(gè)_searchIconLeft 由 0 過(guò)渡到 大于0
的動(dòng)畫,導(dǎo)致了跳動(dòng)的Bug,解決方法:初始狀態(tài)下,left 為 null ,等渲染完成后,再去設(shè)置 left 為 _searchIconLeft ,且渲染完成后再去顯示 搜索 按鈕
,終極偽代碼如下/// 用于獲取文字高度GlobalKey _textKey = new GlobalKey();/// 搜索圖標(biāo)距離左側(cè)的距離double _searchIconLeft = 0;/// 是否已經(jīng)渲染好bool _isPrepared = false;@overrideWidget build(BuildContext context) { // 方案一: 先算出 SearchCube 的寬度,再去計(jì)算其位置 left ,雖然能實(shí)現(xiàn),但是初次顯示時(shí)會(huì)跳動(dòng) widgetUtil.asyncPrepare(context, true, (Rect rect) { final RenderBox box = _textKey.currentContext.findRenderObject(); final Size size = box.size; setState(() { _isPrepared = true; _searchIconLeft = (rect.width - 16.0 - size.width) * .5; }); print('渲染完成 ${rect.size} $size ${size.width} $_searchIconLeft'); }); return Stack( alignment: AlignmentDirectional.center, children: <Widget>[ / / 搜索 按鈕 AnimatedPositioned( child: Offstage( offstage: !_isPrepared, child: ` 搜索`, ), left : _isEdit? 0 : (_isPrepared ? _searchIconLeft : null), top: 0, bottom: 0 ), ] ) ;}
鍵盤高度監(jiān)聽(tīng)。這個(gè)雖然看似簡(jiǎn)單,但確實(shí)是筆者在實(shí)現(xiàn)過(guò)程中耗時(shí)最久的模塊,首先縱觀全網(wǎng),鍵盤監(jiān)聽(tīng)高度的方法都是如下實(shí)現(xiàn),偽代碼如下: class _HomePageState extends State<HomePage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); // Fixed Bug : bottomNavigationBar 的子頁(yè)面無(wú)法監(jiān)聽(tīng)到鍵盤高度變化, so 沒(méi)辦法只能再此監(jiān)聽(tīng)了 WidgetsBinding.instance.addPostFrameCallback((_) { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; }); }}
秉承著前人栽樹,后人乘涼
的原則,以為代碼一復(fù)制,則功能已實(shí)現(xiàn),考慮到只有搜索頁(yè)(SearchContent)
需要監(jiān)聽(tīng),所以興致勃勃的把上述代碼復(fù)制進(jìn)去了,結(jié)果 didChangeMetrics
中獲取的MediaQuery.of(context).viewInsets.bottom;
的值一直是 0
,代碼完全沒(méi)問(wèn)題,當(dāng)結(jié)果卻是有問(wèn)題,真是百撕不得騎姐,結(jié)果發(fā)現(xiàn),微信頁(yè)、聯(lián)系人頁(yè) 都監(jiān)聽(tīng)不到,后來(lái)筆者大膽猜想,是否bottomNavigationBar 的子頁(yè)面無(wú)法監(jiān)聽(tīng)到鍵盤高度變化
,后面筆者把代碼拷貝到 Homepage
頁(yè)就行了,期間過(guò)程真是欲哭無(wú)淚... 只好利用Provider
來(lái)記錄HomePage
的鍵盤高度變化,從而修改 搜索頁(yè)(SearchContent)
的UI變化,偽代碼如下:@overridevoid didChangeMetrics() { super.didChangeMetrics(); // Fixed Bug : bottomNavigationBar 的子頁(yè)面無(wú)法監(jiān)聽(tīng)到鍵盤高度變化, so 沒(méi)辦法只能再此監(jiān)聽(tīng)了 WidgetsBinding.instance.addPostFrameCallback((_) { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; Provider.of<KeyboardProvider>(context, listen: false) .setKeyboardHeight(keyboardHeight); });}
消息的側(cè)滑刪除
、下拉顯示小程序
、點(diǎn)擊導(dǎo)航欄 + 按鈕,彈出菜單欄
等功能。其中通過(guò)對(duì)功能點(diǎn)的逐步剖析和邏輯處理,筆者相信大家在各個(gè)功能點(diǎn)的代碼實(shí)現(xiàn)上應(yīng)該能得心應(yīng)手了。flutter
組件庫(kù); 同時(shí)學(xué)會(huì)了列表的監(jiān)聽(tīng)滾動(dòng)
和控制滾動(dòng)
等知識(shí)點(diǎn),掌握了不同的監(jiān)聽(tīng)或控制滾動(dòng)
的方案,以及對(duì)Flutter
中的狀態(tài)管理的實(shí)現(xiàn)有了一定的了解等...Flutter
產(chǎn)生學(xué)習(xí)的動(dòng)力和樂(lè)趣。關(guān)鍵詞:
客戶&案例
營(yíng)銷資訊
關(guān)于我們
客戶&案例
營(yíng)銷資訊
關(guān)于我們
微信公眾號(hào)
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。