这是我参与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 继承自 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)));
好了,简单的导航栏联动功能就实现完了,代码拙劣,大神勿喷,如果对大家有帮助,更是深感欣慰。