在Material的設(shè)計(jì)準(zhǔn)則里面,tabs是一個(gè)常用的模塊。Flutter里面包含了 material library 創(chuàng)建tab布局的簡(jiǎn)便方法
目前創(chuàng)新互聯(lián)公司已為近千家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬空間、網(wǎng)站運(yùn)營(yíng)、企業(yè)網(wǎng)站設(shè)計(jì)、甘南網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。
為了使tab起作用,我們需要保持選中的tab和相關(guān)內(nèi)容同步。這就是 TabController 的職責(zé)。
我們可以手動(dòng)創(chuàng)建 TabController ,也可以使用 DefaultTabController 小部件。使用 DefaultTabController 是最簡(jiǎn)單的選項(xiàng),因?yàn)樗鼘槲覀儎?chuàng)建一個(gè) TabController ,并使它可用于所有子類Widget。
現(xiàn)在我們已經(jīng)有個(gè)一個(gè) TabController ,我們可以 TabBar Widget去使用創(chuàng)建我們的tab。在這個(gè)示例中,我們將會(huì)在一個(gè) AppBar 下.創(chuàng)建一個(gè)包含3個(gè) Tab Widgets 的 TabBar 。
默認(rèn)情況下, TabBar 在Widget樹中查找最近的 DefaultTabController 。如果是手動(dòng)創(chuàng)建的 TabController ,則需要將其傳遞到“TabBar”。
既然我們有了選項(xiàng)卡,那么我們就需要在選擇選項(xiàng)卡時(shí)顯示相關(guān)的內(nèi)容。因此,我們將使用 TabBarView Widget.
備注: 順序很重要,必須與 TabBar 中的選項(xiàng)卡的順序相對(duì)應(yīng)!
1. Flutter初步探索(二)使用Tabs
1. Working with Tabs
最近在開發(fā)中想實(shí)現(xiàn)一個(gè)AppBar下面有選項(xiàng)卡,來回切換的頁面功能,百度了很多沒有和自己需求符合的,網(wǎng)上大都是返回的Scaffold,使用系統(tǒng)的Appbar,添加至.bottom中,但是現(xiàn)在項(xiàng)目中用的是自定義的Appbar,不想破壞系統(tǒng)的統(tǒng)一封裝。
所以在body 中實(shí)現(xiàn)TabBar 和 TabBarView,開始使用Column一直不行,只能顯示一個(gè),但是在body里面同時(shí)放置 TabBar 和 TabBarView需要注意
TabBarView 的父 Widget 必須知道寬高才能布局,否則,會(huì)報(bào)錯(cuò):BoxConstraints forces an infinite height.
使用 Column + Expanded 即可:
注:還有設(shè)置tabbar的tab背景顏色,tabbar中的tab的背景顏色取的實(shí)際是AppBar的主題色,所以我們將tabbar放在Material中來重置了主題色,變成我們想要的背景色.
buildTabBar為創(chuàng)建TabBar的方法:
buildBodyView創(chuàng)建視圖方法:
有時(shí)候我們不希望某個(gè)頁面每次打開時(shí)都重新加載,比如就我們之前的Tabbar結(jié)構(gòu)的頁面,每當(dāng)我們?cè)谇袚QTab的時(shí)候都會(huì)執(zhí)行 void initState() ,這就意味著頁面每次都會(huì)重新渲染,之所以這樣就是因?yàn)槲覀兊?State 狀態(tài)沒有保存,如下圖所示:
[沒有狀態(tài)保存效果圖]
給當(dāng)前 State 類添加一個(gè)擴(kuò)展(這里就用擴(kuò)展這個(gè)詞吧,其實(shí)類似于iOS下的 Category ),一個(gè)系統(tǒng)的擴(kuò)展類 AutomaticKeepAliveClientMixin ,并重寫 wantKeepAlive 方法,讓一個(gè)普通的 State 類,具有保存狀態(tài)的能力。
在Dart語法中通過使用 with 關(guān)鍵字來添加擴(kuò)展:
bool get wantKeepAlive = true; 之后,當(dāng)前 State 就具備保存能力了,也就意味著重復(fù)切換Tab后, void initState() 就不會(huì)重復(fù)執(zhí)行了(由原來的 viewWillAppear() 變成了 viewDidLoad() )。
按照上面方式修改后,發(fā)現(xiàn)切換Tab后 void initState() 依然重復(fù)執(zhí)行了,這是為什么吶?這里我們看下我們之前 root_page.dart 里面是如何配置我們的tabbar結(jié)構(gòu)的:
這里我們是通過一個(gè) _viewControllers 的List,把4個(gè)子頁面放在了里面,全局有一個(gè) _currentIndex ,當(dāng) onTap 回調(diào)后后,更新 _currentIndex 的值,執(zhí)行 setState () 后, body 對(duì)應(yīng)的 widget 頁面發(fā)生改變。而問題也就出在這里,當(dāng) body 部分發(fā)生改變時(shí),根據(jù)Flutter的底層渲染邏輯,這里會(huì)移除掉之前的 Widget ,并重新創(chuàng)建新的 Widget ,我們之前在 _viewControllers 放的子頁面,并不像iOS下是一個(gè)實(shí)例對(duì)象,存在就直接拿來使用。在Flutter 中 setState () 后界面會(huì)被重新繪制,而 body 部分只知道我要渲染一個(gè)什么樣的 widget ,而該類型的 widget 每次都是會(huì)重新創(chuàng)建,這也就意味著我們?cè)赥ab切換時(shí),每次都是重新創(chuàng)建,所以每次都執(zhí)行了 initState() 。
顯然我們現(xiàn)在的方式是不合理的,那在Flutter中如何管理這樣的子頁面,而避免重復(fù)渲染吶?
這就要用到一個(gè)新的部件了: PageView() ,內(nèi)部的2個(gè)關(guān)鍵屬性:
子頁面切換通過 _controller.jumpToPage(index); 來實(shí)現(xiàn)。
這樣子頁面也就不會(huì)重新創(chuàng)建渲染了,我們的狀態(tài)保存也就能正常實(shí)現(xiàn)了。
學(xué)習(xí)是一個(gè)循序漸進(jìn)的過程,我們總是在踩坑中不斷的前行,把坑填平了也就意味著我們?cè)谶@個(gè)新的東西面前立了足,就可能進(jìn)行更多為什么的探索了。
bottom_tab_bar,
用法和bottom_navigation_bar一樣,但是新增了一些屬性的用法
bottom navigation bar 里面的 icon and title.
回調(diào),帶的是tab的index
The callback that is called when a item is tapped.
The widget creating the bottom navigation bar needs to keep track of the current index and call setState to rebuild it with the newly provided index.
The index into [items] of the current active item.
當(dāng)前激活的是哪一個(gè)tab
Defines the layout and behavior of a [BottomTabBar].
See documentation for [BottomTabBarType] for information on the meaning of different types.
The color of the selected item when bottom navigation bar is [BottomTabBarType.fixed].
If [fixedColor] is null then the theme's primary color, [ThemeData.primaryColor], is used. However if [BottomTabBar.type] is [BottomTabBarType.shifting] then [fixedColor] is ignored.
The size of all of the [BottomTabBarItem] icons.
See [BottomTabBarItem.icon] for more information.
動(dòng)畫是否開啟,默認(rèn)是開起的
未讀消息的顏色,默認(rèn)是fixedColor
按壓水墨屏效果是否開啟,默認(rèn)是開啟的,
還是帶動(dòng)畫的,不太適合我們的正常項(xiàng)目
未讀消息,是一個(gè)widget,可以自定義樣式
未讀消息
first import dependeny in pubspec.yaml
example:
講道理我起的好長(zhǎng)的名字啊,不過文如上題,搜索到這里的兄弟應(yīng)該都知道我說的是啥情況,正好
~~
我這個(gè)方案可能有點(diǎn)笨拙TT,不過自測(cè)有效,有其它想法的老哥希望可以幫忙指點(diǎn)一下~
下面進(jìn)入正題
點(diǎn)進(jìn)源碼里面看,可以發(fā)現(xiàn)他直接繼承了StatelessWidget,那我們就直接看看build方法
可以看到,這里直接返回一個(gè)scrollable或者一個(gè)子節(jié)點(diǎn)是scrollable的InheritedWidget
scrollable是一個(gè)StatefulWidget,那我們就看看它的state
首先scrollable持有一個(gè)scrollposition對(duì)象,是通過其scrollcontroller構(gòu)建的
在其state的setCanDrag方法中,對(duì)其拖動(dòng)設(shè)置了一系列的監(jiān)聽
這里就可以看出來,當(dāng)拖動(dòng)觸發(fā)時(shí),就會(huì)通過當(dāng)前scrollable的position生成一個(gè)Drag/Hold對(duì)象,并調(diào)用相應(yīng)的方法 這個(gè)position有幾個(gè)子類,我們先隨便看一個(gè)實(shí)現(xiàn)
可以看到生成了一個(gè)ScrollDragController對(duì)象,當(dāng)手勢(shì)拖動(dòng)而調(diào)用這個(gè)對(duì)象的update方法時(shí)
可以看到直接調(diào)用其委托對(duì)象的applyUserOffset方法進(jìn)行偏移,而這個(gè)委托對(duì)象根據(jù)剛才的drag方法可以得知正是我們scrollable中的position
最后,由position通知其scrollcontext,也就是之前的scrollable進(jìn)行滑動(dòng)
具體的滑動(dòng)流程這里就不細(xì)說了,我們只是要知道這個(gè)事件是怎么傳遞的就好了,有興趣的老哥可以自行分析
NestedScrollView是一個(gè)statefulwidget,那我們就先看看它的build方法
先忽略其他奇奇怪怪的方法,我們發(fā)現(xiàn)在我們body的外面,包裹了一層PrimaryScrollController,同時(shí)它還持有innerController,這個(gè)innerController暫時(shí)先不管它是啥
還記不記得在最開始ScrollView的build方法中,生成Scrollable的時(shí)候,我們已經(jīng)見過這個(gè)PrimaryScrollController了,再回顧一下
再看看PrimaryScrollController.of(context)
可以看到,在生成scrollable的時(shí)候,在primary = true的情況下是會(huì)向上查找的,看看有沒有PrimaryScrollController,如果有的話,scrollable使用的controller實(shí)際就是nestedscrollview中的innerController了
而之前看過了,scrollable中的position就是scrollcontroller來生成的,那么在這種情況下:
實(shí)際上是生成了_NestedScrollPosition并返回給了body中的scrollable
構(gòu)造方法中有一個(gè)參數(shù)coordinator 暫時(shí)先不管
好了,下面我們?cè)诨仡^看剛才NestedScrollView的build方法,實(shí)際上是生成了一個(gè)_NestedScrollViewCustomScrollView,繼承自大名鼎鼎的CustomScrollView,它當(dāng)然也是scrollview啦,而我們傳給它的controller也是一個(gè)_NestedScrollController,不過叫做_outerController,和body中的不是同一個(gè)罷了,那么自然這個(gè)父scrollview的position也是_NestedScrollPosition。
下面我們按照之前的邏輯,當(dāng)拖動(dòng)開始時(shí),就會(huì)調(diào)用position.drag方法
可以看到,實(shí)際上吧方法交給了我們之前多次見到的coordinator來完成,那我們就簡(jiǎn)單看一下吧
這里可以看到,他把返回的ScrollDragController的委托者設(shè)成了自己
那么自然在拖動(dòng)的時(shí)候,調(diào)用的就是coordinator的applyUseroffset方法了 我們分析一下
可以看到,在需要子列表滾動(dòng)時(shí),是對(duì)innerPositions中的所有position調(diào)用滑動(dòng)方法的
而這innerPositions中的position是怎么來的呢?跟蹤一下可以發(fā)現(xiàn)是在調(diào)用NestedScrollController的attach時(shí)添加進(jìn)來的,如下
因?yàn)橹拔覀兛吹竭^,子scrollable中的controller就是這個(gè)NestedScrollController,所以在updateopsition時(shí)會(huì)把舊的detach調(diào),把新生成的position attach進(jìn)來
另外,在dispose中也會(huì)detach
由此我們就知道啦,因?yàn)殚_啟了緩存后就不會(huì)調(diào)用劃出屏幕的頁面的dispose,自然所有子scrollable的position都存在nestedScrollController里面了,當(dāng)發(fā)生滑動(dòng)時(shí),遍歷調(diào)用positions數(shù)組,就導(dǎo)致屏幕外的列表也跟著滑動(dòng)了~
既然開啟了緩存,手動(dòng)dispose肯定是沒啥意義的,實(shí)際上我們只要在頁面切換過后把未顯示的position 給detach掉就好了。
然鵝,因?yàn)閒lutter不支持反射,子布局傳遞的position我們拿不到,nestedScrollController我們也不能直接拿到=。=
不過有一個(gè)對(duì)象我們之前見到過,scrollable就是通過他獲取controller的,而position則是傳給了獲取到的controller 就是PrimaryScrollController了,所以我打算在中間第三者插足,對(duì)傳遞Position的PrimaryScrollController進(jìn)行Hook
在使用的時(shí)候把子列表添加進(jìn)去,并設(shè)置對(duì)應(yīng)的GlobalKey。
然后監(jiān)聽Tab切換
以上是我的方案,有問題的話還希望老哥幫忙指正,也希望有其他思路的老哥指點(diǎn)一下~~
上一下Github項(xiàng)目地址 用Flutter寫的WanAndroid 其中用到了這個(gè)方案
= =
3
如果和我一樣,用慣了VS Code來開發(fā)項(xiàng)目的話,那就跟我一起來配置一下如何在VS Code里運(yùn)行flutter項(xiàng)目。
1.在VS Code里安裝擴(kuò)展:
2.在VScode上打開打開終端,快捷鍵:Ctrl+~(Tab上一個(gè)鍵),在終端上輸入:flutter create flutter_app02,即可創(chuàng)建完成!
也可以把之前的項(xiàng)目放到工作區(qū)
3.在終端中運(yùn)行命令:flutter run
運(yùn)行的時(shí)候你會(huì)發(fā)現(xiàn)手機(jī)提示你安裝個(gè)app,點(diǎn)擊安裝完成,項(xiàng)目就在手機(jī)上顯示了,下圖是運(yùn)行成功的提示。
下圖是手機(jī)效果:
如果報(bào)錯(cuò)的話,運(yùn)行下清緩存的命令:flutter clean,把文件夾.gradle刪掉,然后flutter run重新跑下項(xiàng)目。