半天內如何封裝一個 Flutter 導航欄上下聯動功能? |8月更文挑戰

頭疼腦脹的程式碼搬運工發表於2021-08-09

這是我參與8月更文挑戰的第2天,活動詳情檢視:8月更文挑戰

廢話開篇:導航欄經常是app內常用的UI功能模組,不管是獨立封裝還是引用第三方最好還是要有一些個人的開發心得,這樣會提高個人的思考能力,今天就整理一下怎麼用半天封裝一個導航欄聯動效果,有人會問,都是老掉牙的東西何必再講述,質疑得沒錯,其實就算是老掉牙的東西,實現原理是一樣的,但在不同的語言特性下可能會發現更多的、更開闊的程式設計思想。一個初學者的學習與總結

步驟一、知識點總攬概述

1、利用 Notification 類實現子元件冒泡式給父元件傳遞訊息。

2、利用 EventBus 單例實現父元件給子元件傳送通知訊息。

3、熟悉 ScrollController 實現記錄scrollview 滾動狀態。

4、熟悉 viewPage 元件。

步驟二、UI效果展示及結構分析

先上效果圖:

螢幕錄製2021-08-09 上午10.54.38.gif

可以看到頂部自定義導航欄與下面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)));
複製程式碼

好了,簡單的導航欄聯動功能就實現完了,程式碼拙劣,大神勿噴,如果對大家有幫助,更是深感欣慰。

相關文章