[Flutter]NotificationListener处理滑动事件分发

Flutter]NotificationListener[ˌnoʊtɪfɪˈkeɪʃn ˈlɪsənər]监听滑动事件

什么是NotificationListener

字面意思是通知监听器,通知基本等价于Android中的事件,所以通知听就可以类比成Android中的事件分发机制。但与Android中任何View都可以处理事件分发不同,Flutter中只有Notification可以监听到通知。所以,当需要对某组件进行通知 监听时,需要用NotificationListener将组件包起来。
怎么又是包起来,在Android中处理事件分发不都是要自定义View吗?实际上Flutter中处处都是自定义Widget,只不过大多数不是通过继承的方式,而是组合。可能仅仅是简简单单的Container包含一个Text,这也算的上是自定义Widget。而当我们给自己的Widget树包裹一层NotificationListener,它就是一个能够处理通知监听的新的Widget了。Compose

支持监听哪些通知

Notification的子类

滑动通知

通知在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的输出结果吧:

滑动过程中的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中完全一样,即是否对事件进行消耗,如果消耗掉则事件不再向父组件及祖先组件进行分发,对应源码如下。

visitAnccestor源码


当内部的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);
    }
  }
}

源码:JoyStick On GitHub

末将于禁 发布了2 篇原创文章 · 获赞 1 · 访问量 29 私信 关注

原文地址:https://blog.csdn.net/weixin_43879272/article/details/103943009

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


简介 java中使用jar包来封装有用的功能,然后将其分发到maven仓库中,供其他人使用。同样的在dart中也有类似的概念叫做packages。packages就是可以用来共享的软件包,可以包含libraries和tools。 你可以在pub.dev网站中查到dart中所有的共享packages的
简介 flutter是google在2015年dart开发者峰会上推出的一种开源的移动UI构建框架,使用flutter可以非常方便的编译成运行在原始android,ios,web等移动平台上的移动应用。 flutter是使用dart来编写的,最新的flutter版本是2.5.3,而最新的Dart语言
简介 dart作为一种面向对象的语言,class是必不可少的。dart中所有的class,除了Null都继承自Object class。 要想使用dart中的类就要构造类的实例,在dart中,一个类的构造函数有两种方式,一起来看看吧。 传统的构造函数 和JAVA一样,dart中可以使用和class名
简介 Exception是程序中的异常情况,在JAVA中exception有checked Exception和unchecked Exception。那么在dart中的情况是不是一样的呢?一起来看看吧。 Exception和Error Dart中表示异常的类有两个,分别是Exception和Err
简介 虽然dart中的类只能有一个父类,也就是单继承的,但是dart提供了mixin语法来绕过这样限制。 今天,和大家一起来探讨一下dart类中的继承。 使用extends 和JAVA一样,dart中可以定义一个父类,然后使用extends来继承他,得到一个子类,如下所示: class Studen
简介 pubspec.yaml是所有dart项目的灵魂,它包含了所有dart项目的依赖信息和其他元信息,所以pubspec.yaml就是dart项目的meta! pubspec.yaml支持的字段 根据dart的定义,pubspec.yaml中可以包含下面的字段: 字段名 是否必须字段 描述 nam
dart系列之:dart语言中的特殊操作符 简介 有运算就有操作符,dart中除了普通的算术运算的操作符之外,还有自定义的非常特殊的操作符,今天带大家一起来探索一下dart中的特殊操作符。 普通操作符 普通操作符就很好解释了,就是加减乘除,逻辑运算符,比较运算符和位运算符等。 这些操作符和其他语言的
简介 在dart系统中,有pubspec.yaml文件的应用就可以被成为一个package。而Libray package是一类特殊的package,这种包可以被其他的项目所依赖. 也就是通常所说的库。 如果你也想你写的dart程序可以上传到pub.dev上,或者提供给别人使用,则来看看这篇文章吧。
简介 和所有的编程语言一样,dart有他内置的语言类型,这些内置类型都继承自Object,当然这些内置类型是dart语言的基础,只有掌握了这些内置类型才能够在使用dart语言的时候得心应手。 今天就给大家讲解一下dart语言的内置类型。 Null 在dart中用null来表示空。那么null和Nul
简介 函数是所有编程语言都有的内容,不管是面向对象还是面向过程,函数都是非常重要的一部分。dart中的函数和java中的函数有什么区别呢? dart作为一种面向对象的编程语言,它的函数也是一个对象,用Function来表示。先看下函数的定义: abstract class Function { ex
简介 熟悉JAVA的朋友可能知道,JAVA在8中引入了泛型的概念。什么是泛型呢?泛型就是一种通用的类型格式,一般用在集合中,用来指定该集合中应该存储的对象格式。 有了泛型可以简化我们的编程,并且可以减少错误的产生,非常的方便。 dart语言中也有泛型。一起来看看吧。 为什么要用泛型 使用泛型的主要目
简介 熟悉javascript的朋友应该知道,在ES6中引入了await和async的语法,可以方便的进行异步编程,从而摆脱了回调地狱。dart作为一种新生的语言,没有理由不继承这种优秀的品质。很自然的,dart中也有await和async语言,一起来看看吧。 为什么要用异步编程 那么为什么要用异步
简介 要想熟悉一种语言,最简单的做法就是熟悉dart提供的各种核心库。dart为我们提供了包括dart:core,dart:async,dart:math,dart:convert,dart:html和dart:io这几种常用的库。 今天给大家介绍一下dart:core中的数字和字符串的使用。 数字
简介 ES6中在引入异步编程的同时,也引入了Generators,通过yield关键词来生成对应的数据。同样的dart也有yield关键词和生成器的概念。 什么时候生成器呢?所谓生成器就是一个能够持续产生某些数据的装置,也叫做generator。 两种返回类型的generator 根据是同步生成还是
简介 Flutter的基础是widget,根据是否需要跟用户进行交互,widget则可以分为StatelessWidget和StatefulWidget。StatelessWidget只能根据传入的状态进行简单的初始化widget,如果要实现跟用户交互这种复杂的功能,则需要用到StatefulWid
简介 时间和日期是我们经常会在程序中使用到的对象。但是对时间和日期的处理因为有不同时区的原因,所以一直以来都不是很好用。就像在java中,为时间和日期修改和新增了多次API,那么作为新生的语言dart而言,会有什么不一样的地方吗? dart中关于日期和时间的两个非常重要的类是DateTime和Dur
简介 Library是dart用来组织代码的一种非常有用的方式,通过定义不同的Library,可以将非常有用的dart代码进行封装,从而提供给其他的项目使用。虽然我们可以自由使用import或者export来对library进行导入和导入。但是什么样的用法才是最合适的用法呢? 一起来看看吧。 使用p
简介 dart中的集合有三个,分别是list,set和map。dart在dart:core包中提供了对于这三种集合非常有用的方法,一起来看看吧。 List的使用 首先是list的创建,可以创建空的list或者带值的list: var emptyList =[]; var nameList = [&#
简介 dart:html包为dart提供了构建浏览器客户端的一些必须的组件,之前我们提到了HTML和DOM的操作,除了这些之外,我们在浏览器端另一个常用的操作就是使用XMLHttpRequest去做异步HTTP资源的请求,也就是AJAX请求。 dart同样提供了类似JS中XMLHttpRequest
简介 Flutter是google开发的一个跨平台的UI构建工具,flutter目前最新的版本是3.0.5。使用flutter你可以使用一套代码搭建android,IOS,web和desktop等不同平台的应用。做到一次编写到处运行的目的。 说到一次编写处处运行,大家可能会想到java。那么flut