flutter开发实战-手势Gesture与ListView滚动竞技场的可滑动关闭组件

flutter开发实战-手势Gesture与ListView滚动竞技场的可滑动关闭组件

最近看到了一个插件,实现一个可滑动关闭组件。滑动关闭组件即手指向下滑动,组件随手指移动,当移动一定位置时候,手指抬起后组件滑出屏幕。

一、GestureDetector嵌套Container非ListView

如果要可滑动关闭,则需要手势GestureDetector,GestureDetector这里实现了onVerticalDragDown、onVerticalDragUpdate、onVerticalDragEnd,通过手势,更新AnimatedContainer的高度。

@overrideWidget build(BuildContext context) {Size screenSize = MediaQuery.of(context).size;return Column(mainAxisSize: MainAxisSize.min,children: <Widget>[GestureDetector(onVerticalDragDown: _onVerticalDragDown,onVerticalDragUpdate: _onVerticalDragUpdate,onVerticalDragEnd: _onVerticalDragEnd,child: AnimatedContainer(curve: Curves.easeOut,duration: Duration(milliseconds: 250),onEnd: () {_onAniPositionedEnd(context);},height: yBottomOffset + widget.displayHeight,width: screenSize.width,clipBehavior: Clip.hardEdge,decoration: const BoxDecoration(color: Colors.transparent,),child: widget.child,),),],);}

我们通过onVerticalDragUpdate来更新AnimatedContainer的高度height,

void _onVerticalDragUpdate(DragUpdateDetails details) {print("_onVerticalDragUpdate");if (details.delta.dy <= 0) {// 向上isDragDirectionUp = true;} else {// 向下isDragDirectionUp = false;}yBottomOffset -= details.delta.dy;if (yBottomOffset > 0.0) {yBottomOffset = 0.0;}if (yBottomOffset < -widget.displayHeight) {yBottomOffset = -widget.displayHeight;}setState(() {});}

当拖动手势结束之后,来检测是否是隐藏状态。

void _onVerticalDragEnd(DragEndDetails details) {print("_onVerticalDragEnd");if (yBottomOffset < -widget.displayHeight / 3) {// 隐藏移除yBottomOffset = -widget.displayHeight;isCompleteHide = true;} else {yBottomOffset = 0.0;isCompleteHide = false;}setState(() {});}

AnimatedContainer中有onEnd方法回调,当动画结束之后,在此方法回调中来处理是否pop等操作

void _onAniPositionedEnd(BuildContext context) {print("_onAniPositionedEnd");if (isCompleteHide) {// 隐藏了,则移除Navigator.of(context).pop();}}

DragBottomSheet2完整代码如下

import 'package:flutter/material.dart';class DragBottomSheet2 extends StatefulWidget {const DragBottomSheet2({super.key,required this.child,required this.displayHeight,});// childfinal Widget child;// 展示的child高度final double displayHeight;@overrideState<DragBottomSheet2> createState() => _DragBottomSheet2State();
}class _DragBottomSheet2State extends State<DragBottomSheet2> {bool? isDragDirectionUp;double yBottomOffset = 0.0;bool isCompleteHide = false;void _onVerticalDragDown(DragDownDetails details) {print("_onVerticalDragDown");}void _onVerticalDragUpdate(DragUpdateDetails details) {print("_onVerticalDragUpdate");if (details.delta.dy <= 0) {// 向上isDragDirectionUp = true;} else {// 向下isDragDirectionUp = false;}yBottomOffset -= details.delta.dy;if (yBottomOffset > 0.0) {yBottomOffset = 0.0;}if (yBottomOffset < -widget.displayHeight) {yBottomOffset = -widget.displayHeight;}setState(() {});}void _onVerticalDragEnd(DragEndDetails details) {print("_onVerticalDragEnd");if (yBottomOffset < -widget.displayHeight / 3) {// 隐藏移除yBottomOffset = -widget.displayHeight;isCompleteHide = true;} else {yBottomOffset = 0.0;isCompleteHide = false;}setState(() {});}void _onAniPositionedEnd(BuildContext context) {print("_onAniPositionedEnd");if (isCompleteHide) {// 隐藏了,则移除Navigator.of(context).pop();}}@overrideWidget build(BuildContext context) {Size screenSize = MediaQuery.of(context).size;return Column(mainAxisSize: MainAxisSize.min,children: <Widget>[GestureDetector(onVerticalDragDown: _onVerticalDragDown,onVerticalDragUpdate: _onVerticalDragUpdate,onVerticalDragEnd: _onVerticalDragEnd,child: AnimatedContainer(curve: Curves.easeOut,duration: Duration(milliseconds: 250),onEnd: () {_onAniPositionedEnd(context);},height: yBottomOffset + widget.displayHeight,width: screenSize.width,clipBehavior: Clip.hardEdge,decoration: const BoxDecoration(color: Colors.transparent,),child: widget.child,),),],);}
}

点击按钮弹出bottomSheet2代码如下

void showBottomSheet2(BuildContext context) {Size size = MediaQuery.of(context).size;double displayHeight = size.height - 88;showModalBottomSheet(context: context,isScrollControlled: true,builder: (ctx) {return DragBottomSheet2(displayHeight: displayHeight,child: Container(width: size.width,height: displayHeight,color: Colors.orangeAccent,child: Text('内容',style: TextStyle(color: Colors.black,),),),);},);}

效果图如下

在这里插入图片描述

二、GestureDetector嵌套ListView

GestureDetector嵌套ListView后,Flutter会根据竞技场Arena机制,通过一定逻辑选择一个组件胜出。
Flutter为了解决手势冲突问题,Flutter给开发者提供了一套解决方案。在该方案中,Flutter引入了Arena(竞技场)概念,然后把冲突的手势加入到Arena中并竞争,谁胜利,谁就获得手势的后续处理权。

Arena竞技场的原理请看https://juejin.cn/post/6874570159768633357

所以在GestureDetector嵌套ListView后,Flutter框架会将这些Gesture与ListView组件都加入竞技场,然后通过一定的逻辑选择一个组件胜出,通常同类组件嵌套时最内层的组件胜出,胜出的组件会处理接下来的move和up事件,其它组件则不会继续处理这些事件了。所以在GestureDetector嵌套ListView的场景中,由于是ListView最终胜出,所以后续的事件都交由ListView处理,而GestureDetector收不到后续的事件,也就不会响应用户的手势了。因此,我们解决这个问题的第一步就是要让GestureDetector在这种场景下也能收到后续的事件

参考请看https://zhuanlan.zhihu.com/p/680586251

我们需要根据GestureDetector真正处理用户手势事件的是内部的Recognizer,比如处理上下滑动的是VerticalDragGestureRecognizer而Recognizer在竞技场失败后也可以单方面宣布自己胜出这样即使在竞技场失败了,GestureDetector也能收到后续的手势事件
因此我们现定义一个单方面宣布胜出的Recognizer

class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {@overridevoid rejectGesture(int pointer) {// 单方面宣布自己胜出acceptGesture(pointer);}
}

我们需要将Recognizer加入到GestureDetector中,会用到RawGestureDetector

RawGestureDetector(gestures: {_MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<_MyVerticalDragGestureRecognizer>(() => _MyVerticalDragGestureRecognizer(),(_MyVerticalDragGestureRecognizer recognizer) {recognizer..onStart = (DragStartDetails details) {}..onUpdate = (DragUpdateDetails details) {}..onEnd = (DragEndDetails details) {};}),},child: ...);

这时候当滚动ListView时候,也能收到手势事件了。

监听ListView的滚动,时候我们需要用到NotificationListener

 NotificationListener(  // 监听内部ListView的滑动变化onNotification: (ScrollNotification notification) {if (notification is OverscrollNotification && notification.overscroll < 0) {// 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件} else if (notification is ScrollUpdateNotification) {// 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理} else {}return false;},child:  //ListView),

最后DragGestureBottomSheet完整代码如下

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_demolab/drag_sheet_controller.dart';class DragGestureBottomSheet extends StatefulWidget {const DragGestureBottomSheet({super.key,required this.child,required this.displayHeight,this.duration = const Duration(milliseconds: 200),this.openDraggable = true,this.autoNavigatorPop = true,this.onShow,this.onHide,});// childfinal Widget child;// 展示的child高度final double displayHeight;// 拖动动画时长durationfinal Duration duration;// 是否需要拖动final bool openDraggable;// 是否需要自动popfinal bool autoNavigatorPop;// This method will be executed when the solid bottom sheet is completely// opened.final void Function()? onShow;// This method will be executed when the solid bottom sheet is completely// closed.final void Function()? onHide;@overrideState<DragGestureBottomSheet> createState() => _DragGestureBottomSheetState();
}class _DragGestureBottomSheetState extends State<DragGestureBottomSheet> {bool? isDragDirectionUp;double yBottomOffset = 0.0;bool isDraggable = false;bool isCompleteHide = false;DragSheetController? dragSheetController;@overridevoid initState() {// TODO: implement initStatedragSheetController = DragSheetController();dragSheetController?.dispatch(widget.displayHeight);super.initState();}@overridevoid dispose() {// TODO: implement disposedragSheetController?.dispose();super.dispose();}void _onVerticalDragUpdate(data) {if (widget.openDraggable) {print("data.delta.dy:${data.delta.dy}");if (data.delta.dy <= 0) {// 向上isDragDirectionUp = true;} else {// 向下isDragDirectionUp = false;}yBottomOffset -= data.delta.dy;if (yBottomOffset > 0.0) {yBottomOffset = 0.0;}if (yBottomOffset < -widget.displayHeight) {yBottomOffset = -widget.displayHeight;}double height = widget.displayHeight + yBottomOffset;dragSheetController?.dispatch(height);}}void _onVerticalDragEnd(data) {if (widget.openDraggable) {// 根据判断是否隐藏与显示if (false == isDragDirectionUp) {if (yBottomOffset < -widget.displayHeight / 3) {// 隐藏移除yBottomOffset = -widget.displayHeight;isCompleteHide = true;} else {yBottomOffset = 0.0;isCompleteHide = false;}} else {yBottomOffset = 0.0;isCompleteHide = false;}double height = widget.displayHeight + yBottomOffset;dragSheetController?.dispatch(height);}}void _onAniPositionedEnd(BuildContext context) {// 动画结束print("_onAniPositionedEnd");if (isCompleteHide) {// 隐藏,则调用hidenif (widget.onHide != null) {widget.onHide!.call();}} else {// 显示,则调用showif (widget.onShow != null) {widget.onShow!.call();}}if (isCompleteHide && widget.autoNavigatorPop) {// 隐藏了,则移除Navigator.of(context).pop();}}@overrideWidget build(BuildContext context) {Size screenSize = MediaQuery.of(context).size;return Column(mainAxisSize: MainAxisSize.min,children: <Widget>[RawGestureDetector(gestures: {_MyVerticalDragGestureRecognizer:GestureRecognizerFactoryWithHandlers<_MyVerticalDragGestureRecognizer>(() => _MyVerticalDragGestureRecognizer(),(_MyVerticalDragGestureRecognizer recognizer) {recognizer..onStart = (DragStartDetails details) {}..onUpdate = (DragUpdateDetails details) {if (!isDraggable) {return;}_onVerticalDragUpdate(details);}..onEnd = (DragEndDetails details) {_onVerticalDragEnd(details);};}),},child: StreamBuilder(stream: dragSheetController?.streamData,initialData: widget.displayHeight,builder: (_, snapshot) {return AnimatedContainer(curve: Curves.easeOut,duration: widget.duration,onEnd: () {_onAniPositionedEnd(context);},height: snapshot.data,width: screenSize.width,clipBehavior: Clip.hardEdge,decoration: const BoxDecoration(color: Colors.transparent,),child: NotificationListener(// 监听内部ListView的滑动变化onNotification: (ScrollNotification notification) {if (notification is OverscrollNotification &&notification.overscroll < 0) {// 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件isDraggable = true;} else if (notification is ScrollUpdateNotification) {// 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理isDraggable = false;} else {}return false;},child: widget.child,),);},),)],);}
}class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {@overridevoid rejectGesture(int pointer) {// 单方面宣布自己胜出acceptGesture(pointer);}
}

三、DragSheetController处理数据流

这里定义了DragSheetController来处理数据流,DragSheetController中包括streamController、subscription、streamSink、streamData

StreamBuilder是一个Widget,它依赖Stream来做异步数据获取刷新widget。
Stream是一种用于异步处理数据流的机制,它允许我们从一端发射一个事件,从另外一端去监听事件的变化.Stream类似于JavaScript中的Promise、Swift中的Future或Java中的RxJava,它们都是用来处理异步事件和数据的。Stream是一个抽象接口,我们可以通过StreamController接口可以方便使用Stream。

使用详情请查看https://brucegwo.blog.csdn.net/article/details/136232000

最后DragSheetController代码如下

import 'dart:async';/// 处理Stream、StreamController相关逻辑
class DragSheetController  {StreamSubscription<double>? subscription;//创建StreamControllerStreamController<double>? streamController = StreamController<double>.broadcast();// 获取StreamSink用于发射事件StreamSink<double>? get streamSink => streamController?.sink;// 获取Stream用于监听Stream<double>? get streamData => streamController?.stream;// Adds new values to streamsvoid dispatch(double value) {streamSink?.add(value);}// Closes streamsvoid dispose() {streamSink?.close();}
}

通过DragSheetController,当拖动时候高度发生变化时候会调用dispatch方法,dispatch来发射数据流,DragGestureBottomSheet中通过StreamBuilder来调整AnimatedContainer的高度。

最后调用使用DragGestureBottomSheet

我们使用showModalBottomSheet展示DragGestureBottomSheet时候

// 显示底部弹窗void showCustomBottomSheet(BuildContext context) {Size size = MediaQuery.of(context).size;double displayHeight = size.height - 88;showModalBottomSheet(context: context,isScrollControlled: true,builder: (ctx) {return DragGestureBottomSheet(displayHeight: displayHeight,autoNavigatorPop: true,openDraggable: true,onHide: () {print("onHide");},onShow: () {print("onShow");},child: Container(width: size.width,height: displayHeight,color: Colors.white,child: ScrollConfiguration(behavior: NoIndicatorScrollBehavior(),child: ListView.builder(itemCount: 20,physics: ClampingScrollPhysics(),itemBuilder: (context, index) {return GestureDetector(child: Container(width: size.width,height: 100,decoration: BoxDecoration(color: Colors.transparent,border: Border.all(color: Colors.black12,width: 0.25,style: BorderStyle.solid,),),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('index -- $index'),SizedBox(width: 50,child: ClipOval(child:Image.asset("assets/images/hero_test.png")),),],),),onTap: () {Navigator.of(context).push(CupertinoPageRoute(builder: (BuildContext context) {return HeroPage();}));},);},),),),);},);}

效果图如下

在这里插入图片描述

https://brucegwo.blog.csdn.net/article/details/136241765

四、小结

flutter开发实战-手势Gesture与ListView滚动竞技场的可滑动关闭组件

学习记录,每天不停进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/264066.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用一行代码在几秒钟内抓取任何网站

如果你正在寻找最强大的 Python 抓取工具&#xff1f;不要再看了&#xff01;这一行代码将帮助你立即启动并运行。 Scrapeasy Scrapeasy 是一个 Python 库&#xff0c;可以轻松抓取网页并从中提取数据。它可用于从单个页面抓取数据或从多个页面抓取数据。它还可用于从 PDF 和…

通过跳板机拷贝远程服务器文件

## 背景 在日常开发或者运维中&#xff0c;经常会遇到开发环境与线上环境网络隔离&#xff0c;需要通过跳板机连接的场景&#xff0c;如果需要将目标机器上的定位信息搬迁到开发机做进一步排查时&#xff0c;经常取文件比较费劲&#xff0c;一般操作是将目标文件拷贝到跳板机&…

gnss 自然灾害监测预警系统是什么

【TH-WY1】GNSS自然灾害监测预警系统是一种基于全球导航卫星系统&#xff08;GNSS&#xff09;技术的自然灾害监测和预警系统。它利用GNSS的高精度定位技术&#xff0c;通过在地表布置GNSS接收设备&#xff0c;实时监测地表形变、位移、沉降等参数&#xff0c;从而实现对自然灾…

【FX110】因为爱情,台湾汇友陷OrientalFX投资骗局损失近50万

近日&#xff0c;一位台湾汇友就在感情与金钱上受到双重打击&#xff0c;在一场“浪漫骗局”中损失近50万新台币。 因为爱情&#xff0c;陷入无法出金的投资骗局 据汇友描述&#xff0c;她从未有过投资经验&#xff0c;之所以突然入市&#xff0c;是因为“Kevin”。Kevin是她在…

全面InfiniBand解决方案——LLM培训瓶颈问题

ChatGPT对技术的影响引发了对人工智能未来的预测&#xff0c;尤其是多模态技术的关注。OpenAI推出了具有突破性的多模态模型GPT-4&#xff0c;使各个领域取得了显著的发展。 这些AI进步是通过大规模模型训练实现的&#xff0c;这需要大量的计算资源和高速数据传输网络。端到端…

软考系分之多媒体的容量计算、多媒体的标准、媒体数据压缩

文章目录 1、概要2、数据压缩3、多媒体的标准4、多媒体的容量计算5、总结 1、概要 本篇重点介绍多媒体技术&#xff0c;包括多媒体标准、数据压缩和媒体容量的计算。 2、数据压缩 媒体数据能压缩的话&#xff0c;有个前提条件就是数据存在冗余&#xff0c;包括时间冗余、空间冗…

【Java程序设计】【C00267】基于Springboot的在线考试系统(有论文)

基于Springboot的在线考试系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 本系统是基于Springboot的在线考试系统&#xff1b;本系统主要分为管理员、教师和学生三种角色&#xff1b; 管理员登录系统后&#xff0c;可以对首页&#x…

【打工日常】使用docker部署StackEdit编辑器-Markdown之利器

一、StackEdit介绍 StackEdit一款强大的在线Markdown编辑器&#xff0c;不仅具备卓越的写作功能&#xff0c;还支持实时预览、多设备同步等特性。 很多时候基于安全和信息保密的关系&#xff0c;建议放在自己的服务器或者本地linux去运行&#xff0c;这样会比较省心。 二、本次…

day16_ListSet课后练习题 - 参考答案

文章目录 day16_课后练习题第1题第2题第3题第4题第5题第6题第7题第8题 day16_课后练习题 第1题 案例&#xff1a; ​ 1、用一个String[]数组存点数 ​ 2、用一个String[]数组存花色 ​ 3、用一个String[]数组存大王、小王 ​ 4、用上面的数组&#xff0c;生成一副扑克牌 …

第四节:Vben Admin登录对接后端getUserInfo接口

系列文章目录 第一节&#xff1a;Vben Admin介绍和初次运行 第二节&#xff1a;Vben Admin 登录逻辑梳理和对接后端准备 第三节&#xff1a;Vben Admin登录对接后端login接口 第四节&#xff1a;Vben Admin登录对接后端getUserInfo接口 文章目录 系列文章目录前言一、回顾Vben…

多重网格(Multigrid Method)-1

See https://zhuanlan.zhihu.com/p/49489140?utm_id0

【Linux进程】进程状态---进程僵尸与孤儿

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.进程排队2.进程状态…

Stable Diffusion 绘画入门教程(webui)-ControlNet(Inpaint)

上篇文章介绍了语义分割Tile/Blur&#xff0c;这篇文章介绍下Inpaint&#xff08;重绘&#xff09; Inpaint类似于图生图的局部重绘&#xff0c;但是Inpain效果要更好一点&#xff0c;和原图融合会更加融洽&#xff0c;下面是案例&#xff0c;可以看下效果&#xff08;左侧原图…

前端基础面试题(一)

摘要&#xff1a;最近&#xff0c;看了下慕课2周刷完n道面试题&#xff0c;记录下... 1.请说明Ajax、Fetch、Axios三者的区别 三者都用于网络请求&#xff0c;但维度不同&#xff1a; Ajax&#xff08;Asynchronous Javascript ang XML&#xff09;&#xff0c;是一种在不重新…

ARM处理器有哪些工作模式和寄存器?各寄存器作用是什么?ARM异常中断处理流程?

《嵌入式工程师自我修养/C语言》系列——ARM处理器有哪些工作模式和寄存器&#xff1f;各寄存器作用是什么&#xff1f; 一、ARM处理器的工作模式及寄存器1.1 ARM处理器的工作模式1.2 ARM处理器中的寄存器 二、ARM 异常中断处理2.1 什么是异常&#xff1f;异常向量表是什么&…

[RCTF2015]EasySQL1 题目分析与详解

一、题目介绍&#xff1a; 1、题目来源&#xff1a; BUUCTF网址 2、题目介绍&#xff1a; 拿到flag。 二、解题思路&#xff1a; 我们发现题目首页有登录和注册账号两个选项&#xff0c;我们首先尝试注册账号&#xff0c;尝试注册username为admin的账号&#xff0c;输入密码…

FPGA OSERDESE2

Output Parallel-to-Serial Logic Resources (OSERDESE2) OSERDESE2 在Xilinx 7 系列器件是一款专用的并行至串行转换器,具有特定的时钟和逻辑资源,旨在促进高速源同步接口的实现。每个OSERDESE2模块都包括一个专用的数据串行器和 3 状态控制。数据和 3 态串行器都可以在 SD…

TVS管+二极管钳位电路的分析

我们通常的电路中多用TVS管或者单二极管来进行钳位&#xff0c;TVS管二极管同时钳位的情况比较少见&#xff0c;这篇文章就是分析TVS管二极管同时钳位的电路&#xff0c;主要学习其分析思路。 电路如下图&#xff0c;就是个flyback。可以看到&#xff0c;电路中有个钳位电路&a…

petalinux_zynq7 驱动DAC以及ADC模块之五:nodejs+vue3实现web网页波形显示

前文&#xff1a; petalinux_zynq7 C语言驱动DAC以及ADC模块之一&#xff1a;建立IPhttps://blog.csdn.net/qq_27158179/article/details/136234296petalinux_zynq7 C语言驱动DAC以及ADC模块之二&#xff1a;petalinuxhttps://blog.csdn.net/qq_27158179/article/details/1362…

MySQL锁三部曲:临键、间隙与记录的奇妙旅程

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 MySQL锁三部曲&#xff1a;临键、间隙与记录的奇妙旅程 前言临键锁的奥秘间隙锁记录锁 前言 在数据库世界中&#xff0c;锁是维护数据完整性的一种关键机制。而MySQL中的临键锁、间隙锁和记录锁则是锁…