Flutter]NotificationListener[ˌnoʊtɪfɪˈkeɪʃn ˈlɪsənər]监听滑动事件
- 什么是NotificationListener
- 支持监听哪些通知
- 主要属性onNotification(Notification)
- 应用--仿猫眼电影主页图片滑动广告
什么是NotificationListener
字面意思是通知监听器,通知基本等价于Android中的事件,所以通知听就可以类比成Android中的事件分发机制。但与Android中任何View都可以处理事件分发不同,Flutter中只有Notification可以监听到通知。所以,当需要对某组件进行通知 监听时,需要用NotificationListener将组件包起来。
怎么又是包起来,在Android中处理事件分发不都是要自定义View吗?实际上Flutter中处处都是自定义Widget,只不过大多数不是通过继承的方式,而是组合。可能仅仅是简简单单的Container包含一个Text,这也算的上是自定义Widget。而当我们给自己的Widget树包裹一层NotificationListener,它就是一个能够处理通知监听的新的Widget了。Compose
支持监听哪些通知
滑动通知
通知在Flutter中的抽象为Notification类,它包含12个子类,对应12种通知,下面将介绍跟主题相关的滑动通知ScrollNotification。滑动通知ScrollNotification有如图五个子类,它们分别代表五种滑动通知,滑动组件触发相应滑动事件时,向Ancestor的NotificationListener发送通知。
滑一滑,看一看,找一找。看看能不能在滑动过程中找到这几个通知。
NotificationListener(
onNotification: (ScrollNotification notification) {//收到Notification
setState(() {
_currentNotification = notification.runtimeType.toString();//将notification实例的类名展示
});
print(notification.runtimeType.toString());//同时输出log
return true;
},
child: Stack(
children: <Widget>[
ListView.builder(
//...省略
),
Center(child: Text('$_currentNotification'))
],
),
);
找到了,全找到了,ScrollStartNotification–ScrollUpdateNotification–OverScrollNotification–UserScrollNotification–EndScrollNotification,但是有一个疑问,按常理来说,滑动结束时就应该是EndScroll,而实际结果却是UserScroll,当在原地点击的时候,松手时才是EndScroll。于是,看看Log的输出结果吧:
似乎疑惑可以解开了,在滑动结束时,实际上收到了EndScrollNotification,但是被紧跟其后的UserScrollNotification覆盖掉了。通过观察,发现规律,每次滑动的方向发生改变时,就会产生UserScroll事件。而点击过程中的Log也印证了这一点,End前为无方向,End后为无方向,故不会产生UserScroll。
几种滑动通知对应的事件
ScrollStartNotification
当滑动组件开始滑动时,实际上手指接触屏幕那一刻就会触发。
ScrollEndNotification
滑动组件结束滑动时
ScrollUpdateNotification
组件产生位移时
OverscrollNotification
滑动越界时
UserScrollNotification
滑动方向发生改变时
主要属性onNotification(Notification)
来看一下NotificationListener的事件分发源码
可见,分发一个通知有两个条件,一是通知非空,二是通知的类型为T,原来NotificationListener可以指定泛型,这样onNotification方法只会接收到对应事件的回调,不如下面一段代码只会收到滑动结束的通知。
NotificationListener<ScrollEndNotification>(
onNotification: (ScrollNotification notification) {
return true;
},
onNotification有一个bool返回值,这个设计与Android中完全一样,即是否对事件进行消耗,如果消耗掉则事件不再向父组件及祖先组件进行分发,对应源码如下。
当内部的NotificationListener的onNotification方法返回值为true,则事件被消耗掉,外部(祖先组件)则不会收到该通知。
NotificationListener(
onNotification: (no) {
print('ancestorNotification');
return true;
},
child: NotificationListener(
onNotification: (ScrollNotification notification) {
return true;
},
),
);
应用–仿猫眼电影主页图片滑动广告
效果图
实现思路
首先,整个变化过程都是在滑动中进行的,所以需要用到ScrollUpdateNotification。
1)初始:
因为图片是从屏幕底部滑入的,所以默认显示图片底部。
2)为了保证整张图片都能显示,要在图片所在的Item完全进入时开始滑动,图片Item将要滑出屏幕时停止滑动。在这个过程中,根据当前滑动距离与总位移的比值确定图片应该显示的对应位置。
3)因此,需要在图片所在Item进入屏幕时记录当前滑动偏移量_start,设总偏移量为_total,则滑动过程中偏移比率为_start/_total,以此修改图片的Alignment即可。
4)而这个总偏移量,就是ListView在屏幕中的显示长度-图片Item的长度。
代码实现
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class SlidePicDemo extends StatefulWidget {
@override
_SlidePicDemoState createState() => _SlidePicDemoState();
}
class _SlidePicDemoState extends State<SlidePicDemo> {
var _itemCount = 50; //不包含广告位
var _itemExtent = 150.0; //item宽度/高度,取决于scrollDirection
var _adPicAlignment = 1.0;//默认图片显示位置
var _adPosition = 25;//显示图片广告的位置
var _adStartOffset;//记录图片进入屏幕时的偏移量
var _scrollDirection = Axis.vertical;//滑动方向,实际上也支持横向滑动
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
_handleNotification(notification);
return true;
},
child: Stack(
children: <Widget>[
ListView.builder(
scrollDirection: _scrollDirection,
itemExtent: _itemExtent,
itemBuilder: (context, index) {
if (index == _adPosition) {//在预设的位置上显示图片广告
return Image.asset(
'assets/images/ct.jpeg',
fit: _scrollDirection == Axis.horizontal ? BoxFit.fitHeight : BoxFit.fitWidth,//滑动相对方向上要占满
alignment: _scrollDirection == Axis.horizontal ? Alignment(_adPicAlignment,0) : Alignment(0,_adPicAlignment),//通过Alignment改变图片显示位置
);
} else {
return _buildItem(index < _adPosition ? index : index - 1);
}
},
itemCount: _itemCount + 1,//增加了图片
padding: const EdgeInsets.all(0),
),
],
),
);
}
///处理滑动
_handleNotification(ScrollNotification notification) {
var totalOffset = notification.metrics.viewportDimension - _itemExtent;//总偏移量=listView高度-Item高度
var firstVisible = notification.metrics.extentBefore ~/ _itemExtent;//通过整除,计算出当前第一个可见的Item
var lastVisible =
_itemCount - notification.metrics.extentAfter ~/ _itemExtent;//通过整除,计算出当前最后一个可见的Item
if (firstVisible <= _adPosition && _adPosition <= lastVisible - 1) {//图片完全处于屏幕中时
if (null == _adStartOffset)//第一次,记录初始偏移量
_adStartOffset = notification.metrics.extentBefore;
var percent = (notification.metrics.extentBefore - _adStartOffset) /
totalOffset;//之后的滑动中,计算相对偏移比例
setState(() {
_adPicAlignment = _calculateAlignment(1 - percent);//改变图片显示位置
});
}
return true;
}
_buildItem(index) {
return Container(
color: _getSkipColor(index),
child: Center(child: Text(index.toString())),
height: _itemExtent,
width: MediaQuery.of(context).size.width,
);
}
/// 0 到 1 转化为 -1 到 1。
/// 滑动偏移比例为0-1之间的小数,而Alignment的值为-1,1。
/// 通过单位加减乘除,得到对应alignment。
_calculateAlignment(percent) {
var sourceUnit = (1.0 - 0.0) / 10000;
var targetUnit = (1.0 - (-1.0)) / 10000;
var offset = (percent - 0) / sourceUnit;
var alignment = -1 + offset * targetUnit;
if (alignment > 1.0) alignment = 1.0; //边界
if (alignment < -1.0) alignment = -1.0;
return alignment;
}
_getSkipColor(index) {
switch (index % 5) {
case 0:
return Color.fromARGB(255, 241, 241, 184);
case 1:
return Color.fromARGB(255, 241, 201, 184);
case 2:
return Color.fromARGB(255, 241, 184, 228);
case 3:
return Color.fromARGB(255, 184, 241, 237);
case 4:
return Color.fromARGB(255, 184, 241, 204);
}
}
}