這是我參與8月更文挑戰的第2天,活動詳情檢視:8月更文挑戰
廢話開篇:導航欄經常是app內常用的UI功能模組,不管是獨立封裝還是引用第三方最好還是要有一些個人的開發心得,這樣會提高個人的思考能力,今天就整理一下怎麼用半天封裝一個導航欄聯動效果,有人會問,都是老掉牙的東西何必再講述,質疑得沒錯,其實就算是老掉牙的東西,實現原理是一樣的,但在不同的語言特性下可能會發現更多的、更開闊的程式設計思想。一個初學者的學習與總結
步驟一、知識點總攬概述
1、利用 Notification 類實現子元件冒泡式給父元件傳遞訊息。
2、利用 EventBus 單例實現父元件給子元件傳送通知訊息。
3、熟悉 ScrollController 實現記錄scrollview 滾動狀態。
4、熟悉 viewPage 元件。
步驟二、UI效果展示及結構分析
先上效果圖:
可以看到頂部自定義導航欄與下面viewPage進行聯動並以滑動的方式保持最終為寬度範圍內可見。
結構分析:
1、上面為 SingleChildScrollView 元件,設定滾動方向為水平並設controller來進行滾動狀態監聽與設定偏移量
new SingleChildScrollView(
scrollDirection: Axis.horizontal,//設定水平滾動
controller: _scrollController,//設定滾動管理物件
child: new Row(
mainAxisAlignment: MainAxisAlignment.start,
children: _getBarItems(context),
),
)
複製程式碼
裡面的item需要單獨封裝,這樣可以儲存更多的資訊,如:自身的寬度,目的是在切換導航欄的時候進行可視範圍內滾動調整。
//導航欄item元件封裝
class GenneralChannelItem extends StatelessWidget {
String name;
int index;
int currentSelectIndex;
double left;
double right;
double width = 0;//記錄寬度
GenneralChannelItem({required this.name,required this.index,required this.currentSelectIndex,this.left = 16,this.right = 16});
@override
Widget build(BuildContext context) {
TextStyle style = SystemFont.blodFontSizeAndColor(this.index == this.currentSelectIndex ? 17.0 : 17.0, this.index == this.currentSelectIndex ? Colors.red : Colors.black);
//計算寬度
this.width = SystemFont.getTextSize(this.name, style).width + this.left + this.right;
WSLGetWidgetWithNotification(width: this.width).dispatch(context);
return new Container(
padding: EdgeInsets.only(left: this.left,right: this.right),
child: new Text(this.name,style: style,)
);
}
}
複製程式碼
SystemFont 類計算寬度實現程式碼:
static Size getTextSize(String text, TextStyle style) {
TextPainter painter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
maxLines: 1,
ellipsis: '...',
);
painter.layout();
return painter.size;
}
複製程式碼
建立記錄導航欄item位置資訊類
class ChannelFrame{
double left;//距離左側距離
double width;//item寬度
ChannelFrame({this.left = 0,this.width = 0}){}
}
複製程式碼
建立導航欄item集合並儲存相關資訊
List<Widget> _getBarItems(BuildContext context){
this.channelFrameList = [];
this._maxScrollViewWidth = 0;
//titleList 為導航欄item標題集合
return this.widget.titleList.map((e){
//初始化導航欄item
GenneralChannelItem genneralChannelItem = new GenneralChannelItem(
name: e,
index: this.widget.titleList.indexOf(e),
currentSelectIndex: this.widget.selectIndex,
left: this._left,
right: this._right,
);
return new NotificationListener<WSLGetWidgetWithNotification>(
onNotification: (notification){
//這裡接受item建立時發起的冒泡訊息,目的是:此時,導航item的寬度已計算完畢,建立導航欄佈局記錄類,記錄當前item距離左側距離及寬度。
ChannelFrame channelFrame = ChannelFrame(left:this._maxScrollViewWidth,width: genneralChannelItem.width);
//儲存所有ChannelFrame值,以便當外部修改viewPage的index值時或者點選item時進行修改scrollview偏移量
this.channelFrameList.add(channelFrame);
this._maxScrollViewWidth += genneralChannelItem.width;
//返回 false 訊息到此結束,不再繼續往外傳遞
return false;
},
child: new GestureDetector(
child: genneralChannelItem,
onTap: (){
setState(() {
this.widget.selectIndex = this.widget.titleList.indexOf(e);
//傳送一個冒泡訊息,來實現外層底部的viewPage進行切換。把事件向外傳出去,外界收到訊息後利用viewPage的Control修改一下當前viewPage的偏移量。
WSLCustomTabbarNotification(index: this.widget.selectIndex).dispatch(context);
});
},
));
}
).toList();
}
複製程式碼
2、主頁面底部為viewPage,由於裡面為個人內部業務邏輯,這裡不進行過多詳細的敘述。
需要在viewPage滑動完成後修改上部的導航欄item的狀態,在建立的時候需要設定一下事件。
new PageView(
controller: this.pageController!,
//監聽viewPage改變
onPageChanged: (index){
setState(() {
//修改導航欄標記
selectIndex = index;
//傳送EventBus事件,通知導航欄進行已選中item視覺化範圍內滾動
EventBusUtils.getInstance().fire(WSLChannelScrollViewNeedScrollEvent((index)));
});
},
)
複製程式碼
步驟三、如何進行導航欄超出部分滾動計算?
直接上程式碼
//宣告一個eventBus 訊息監聽物件
var _needChangeScrollviewEvent;
//初始化中進行evenBus訊息監聽
@override
void initState(){
_needChangeScrollviewEvent = EventBusUtils.getInstance().on<WSLChannelScrollViewNeedScrollEvent>().listen((event) {
ChannelFrame channelFrame = this.channelFrameList[event.index];
//計算選中的導航item的中心點
double centerX = channelFrame.left + channelFrame.width / 2.0;
//設定需要滾動的偏移量
double needScrollView = 0;
//當選中的導航item在中心偏左時
if(centerX - _scrollController.offset < this.widget.width / 2.0) {
needScrollView = (this.widget.width / 2.0 - centerX + _scrollController.offset);
//存在滾動條件
if(_scrollController.offset > 0) {
//當無法滿足滾動到正中心的位置,就直接回到偏移量原點
if(_scrollController.offset < needScrollView) {
needScrollView = _scrollController.offset;
}
//進行偏移量動畫滾動
_scrollController.animateTo(_scrollController.offset - needScrollView, duration: Duration(milliseconds: 100), curve: Curves.linear);
}
} else {
//當選中的導航item在中心偏右時
needScrollView = (centerX - _scrollController.offset - this.widget.width / 2.0);
if(_maxScrollViewWidth - _scrollController.offset - this.widget.width > 0) {
//不滿足回滾到中間位置,設定為滾到最大位置
if(_maxScrollViewWidth - _scrollController.offset - this.widget.width < needScrollView) {
needScrollView = _maxScrollViewWidth - _scrollController.offset - this.widget.width;
}
_scrollController.animateTo(_scrollController.offset + needScrollView, duration: Duration(milliseconds: 100), curve: Curves.linear);
}
}
});
}
@override
void dispose() {
//銷燬時取消evenBus監聽
_needChangeScrollviewEvent.cancel();
super.dispose();
}
複製程式碼
步驟四、在點選導航item的時候或者viewPage滑動之後傳送EventBus事件,進行導航item進行視覺化範圍內移動展示。
EvenBus 單例類
class EventBusUtils {
static EventBus _instance = new EventBus();
static EventBus getInstance() {
return _instance;
}
}
複製程式碼
註冊 EventBus 的訊息物件類
class WSLChannelScrollViewNeedScrollEvent {
int index = 0;
WSLChannelScrollViewNeedScrollEvent(this.index);
}
複製程式碼
傳送evenBus事件
index 值為當前所選的導航item索引值,觸發可視範圍內滾動事件。
EventBusUtils.getInstance().fire(WSLChannelScrollViewNeedScrollEvent((index)));
複製程式碼
好了,簡單的導航欄聯動功能就實現完了,程式碼拙劣,大神勿噴,如果對大家有幫助,更是深感欣慰。