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.pswp.cn/news/697259.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/697259.shtml
英文地址,請注明出處:http://en.pswp.cn/news/697259.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

大保司保費貴,是否物有所值?

《大保司保費貴&#xff0c;是否物有所值》 這是羅師兄的原創文章 預計8-9分鐘讀完 作者&#xff1a;羅師兄 微信號&#xff1a;luoyun515 當我們想要買一份重疾險、儲蓄險等長期險時&#xff0c; 我們會發現&#xff0c;同樣的保障責任和保額&#xff0c; 不同保險公司的…

基于AdaBoost算法的情感分析研究-微博情感分析-文本分類

基于AdaBoost算法的情感分析研究 摘 要 隨著互聯網的快速發展&#xff0c;各類社交媒體平臺如微信、QQ等也與日俱增&#xff0c;而微博更是集成了傳統網站、論壇、博客等的優點&#xff0c;并加上了人與人之間的互動性、關系親密程度等多種智能算法&#xff0c;并以簡練的形式…

python安裝與配置2024最新版

對python不熟悉的可以去看看這篇文章python介紹 pytho安裝 來到Python官網&#xff1a;https://www.python.org/ 然后 選著download列表下的windows , 然后加進入python各個版本的下載界面 可以看到截止2024年2月22日,最新版是3.12.2 但是我們一般下載穩定版 如下,點擊連接進…

設計模式學習筆記 - 面向對象 - 5.接口和抽象類的區別

簡述 在面向對象編程中&#xff0c;抽象類和接口是常被用到的語法概念&#xff0c;是面向對象四大特性&#xff0c;以及很多設計模式、設計思想、設計原則實現的基礎。它們之間的區別是什么&#xff1f;什么時候用接口&#xff1f;什么時候用抽象類&#xff1f;抽象類和接口存…

解決兩個MySQL5.7報錯

目錄 1.啟動不了MySQL&#xff0c;報錯缺少MSVCR120.dll去官網下載vcredist_x64.exe運行安裝進入管理員CMD 2.本地計算機 上的 mysql 服務啟動后停止。某些服務在未由其他服務或程序使用時將自動停止&#xff0c;Fatal error: Can‘t open and lock privilege tables: Table ‘…

wpf menu 菜單 快捷鍵

界面快捷鍵資源 CtrlF F3可加入其它&#xff0c;自行定義 Page可改為Windows xaml文件 <Style TargetType"{x:Type DataGrid}"> <Setter Property"ContextMenu"> <Setter.Value> <ContextMenu St…

實習日志28

1.醫院賬套系統換新&#xff0c;卡片數據轉移 1.1.修改舊導出的Excel 1.2.嘗試導入新系統 1.3.修改導入數據再次導入即可 這個系統做的限制條件比較多&#xff0c;代碼健壯性不錯。 先在Excel表格里改好批量的&#xff0c;再導入檢查&#xff0c;改一些細節的比較快捷。 2.…

套接字(Sockets)編程——逆向分析向

套接字&#xff08;Sockets&#xff09;編程 套接字&#xff08;Sockets&#xff09;編程是一種網絡編程技術&#xff0c;用于在不同計算機之間或同一臺計算機上的不同進程之間進行通信。在套接字編程中&#xff0c;我們創建套接字&#xff0c;這是一個支持網絡請求和響應的端…

PHP安全

PHP安全 推薦鏈接PHP版本號隱藏 推薦鏈接 鏈接目錄 PHP版本號隱藏 PHP 版本信息泄露 系統數據包 X-Powered-By 字段泄露了 PHP 具體版本信息 //找到php.ini文件 //要修改的位置&#xff0c;把expose_phpOn 改為 expose_phpOff //service php-fpm restart #apache服務器可使用…

ChatGPT回答模式

你發現了嗎&#xff0c;ChatGPT的回答總是遵循這些類型方式。 目錄 1.解釋模式 2.類比模式 3.列舉模式 4.限制模式 5.轉換模式 6.增改模式 7.對比模式 8.翻譯模式 9.模擬模式 10.推理模式 1.解釋模式 ChatGPT 在回答問題或提供信息時&#xff0c;不僅僅給出…

【Linux取經路】文件系統之緩沖區

文章目錄 一、先看現象二、用戶緩沖區的引入三、用戶緩沖區的刷新策略四、為什么要有用戶緩沖區五、現象解釋六、結語 一、先看現象 #include <stdio.h> #include <string.h> #include <unistd.h>int main() {const char* fstr "Hello fwrite\n"…

HW面試常見知識點(新手認識版)

shiro漏洞原理 shiro漏洞原理是攻擊者利用shiro的默認密鑰偽造cookie&#xff0c;觸發JAVA反序列化執行命令或者寫shell。 shiro工具原理 跑默認key shiro550和721的區別 721是需要有效的登錄才可以 550不用登錄就可以直接跑key log4j原理 log4j是一款通用日志記錄工具&#xf…

【思揚贈書 | 第3期】由面試題“Redis是否為單線程”引發的思考

?? 寫在前面參與規則&#xff01;&#xff01;&#xff01; ?參與方式&#xff1a;關注博主、點贊、收藏、評論&#xff0c;任意評論&#xff08;每人最多評論三次&#xff09; ??本次送書1~4本【取決于閱讀量&#xff0c;閱讀量越多&#xff0c;送的越多】 很多人都遇到…

設計模式-抽象工廠模式(C++)

抽象工廠模式是一種設計模式&#xff0c;它提供了一個接口來創建一系列相關或相互依賴的對象&#xff0c;而無需指定它們具體的類。下面是一個使用 C 實現抽象工廠模式的示例&#xff1a; // 抽象產品類 class AbstractProductA { public:virtual void DoSomething() 0; };cl…

gitlab的使用

前一篇文章我們已經知道Git人人都是中心&#xff0c;那他們怎么交互數據呢&#xff1f; ? 使用GitHub或者碼云等公共代碼倉庫 ? 使用GitLab私有倉庫 目錄 一、安裝配置gitlab 安裝 初始化 這里初始化完成以后需要記住一個初始密碼 查看狀態 二、使用瀏覽器訪問&#xf…

Math方法,以及三角函數計算

abs(x) 返回參數的絕對值 var xMath.abs(-5) //5floor(x) 向下舍入為最接近的整數。 var xMath.floor(2.1) //2ceil(x) 向上舍入為最接近的整數。 var xMath.ceil(2.1) //3fround(x) 最接近的&#xff08;32 位單精度&#xff09;浮點表示。 var xMath.fround(2.60) //2.59…

小凡爬樓梯

解法&#xff1a; dp[i]:到第i階梯&#xff0c;總共dp[i]種方案 狀態轉移方程&#xff1a; base condition: #include<iostream> #include<vector> #include<algorithm> using namespace std; #define endl \n int main() {vector<long long> dp(51…

js數據處理util

方法匯總 據時間范圍生成時間刻度數據 /**params startDate 開始時間*params endDate 結束時間*params timeUnit 時間間隔,注意是毫秒數**/function createTimeUnitListByTimeRange(startDate, endDate, timeUnit){let startSeconds new Date(startDate).getTime();let endS…

【前綴和】560. 和為 K 的子數組

560. 和為 K 的子數組 解題思路 創建一個前綴和數組 preSum&#xff0c;其長度比原數組 nums 多 1。preSum[i] 表示 nums 中前 i 個元素的和。通過遍歷 nums 數組&#xff0c;計算前綴和數組 preSum。 在嵌套的兩個循環中&#xff0c;對所有可能的子數組進行窮舉&#xff1a;…

板塊一 Servlet編程:第四節 HttpServletResponse對象全解與重定向 來自【湯米尼克的JAVAEE全套教程專欄】

板塊一 Servlet編程&#xff1a;第四節 HttpServletResponse對象全解與重定向 一、什么是HttpServletResponse二、響應數據的常用方法三、響應亂碼問題字符流亂碼字節流亂碼 四、重定向&#xff1a;sendRedirect請求轉發和重定向的區別 在上一節中&#xff0c;我們系統的學習了…