半天内如何封装一个 Flutter 导航栏上下联动功能? |8月更文挑战

1,333 阅读4分钟

这是我参与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 继承自 Notification 并声明一个 width 属性
    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;
          return false;
        },
        child: new GestureDetector(
          child: genneralChannelItem,
          onTap: (){
            setState(() {
              this.widget.selectIndex = this.widget.titleList.indexOf(e);
              //发送一个冒泡消息,来实现外层底部的viewPage进行切换。把事件向外传出去,外界收到消息后利用viewPage的Control修改一下当前viewPage的偏移量。 WSLCustomTabbarNotification 继承自 Notification 并声明一个 index 属性
              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(_scrollController.position.maxScrollExtent - _scrollController.offset > 0) {
        //不满足回滚到中间位置,设置为滚到最大位置
        if(_scrollController.position.maxScrollExtent - _scrollController.offset < needScrollView) {
          needScrollView = _scrollController.position.maxScrollExtent - _scrollController.offset;
        }
        _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)));

好了,简单的导航栏联动功能就实现完了,代码拙劣,大神勿喷,如果对大家有帮助,更是深感欣慰。