当前位置: 首页 > Web前端 > HTML

Flutter交互实践——即时App探索页面下拉&拖拽效果

时间:2023-04-02 15:52:35 HTML

前言Flutter最近火了一把,但是Flutter作为一个系统的文章不多,前期难免有坑;我的文章主要介绍如何使用Flutter实现一个比较复杂的手势交互,顺便分享一下我在使用Flutter过程中遇到的一些小坑,减少大家的坑;作者:HitenDev链接:https://www.jianshu.com/p/4d1...先睹为快本项目支持ios&android运行,效果如下,顺便分享一下生成gif的小技巧,建议使用手机的录屏功能将mp4文件导出到电脑,然后在电脑上使用ffmpeg命令行进行处理,控制gif的质量和文件大小,我最好的建议就是把分辨率控制在270p,帧率控制在10左右;阅读互动分析文章的朋友最好拿着免安装APP),亲身体验探索页的互动,即用黄色的logo和黄色的主题色;有人说‘黄记’;InstantApp原有的功能包括卡片轮换、卡片退出和卡片自动移除。时间关系还没有实现,但是核心功能都在;从Android开发者的习惯来看,这种交互可以分为内部和外部两部分。在外层,我们需要一个整体的下拉控件,我称之为下拉控件;在内层,我们需要实现一个可以上下左右四个方向拖动移动的控件,我们称之为卡片控件;下拉控件和卡片控件不仅需要处理手势,还需要处理子widgets的布局;下面我来分析一下详细的功能:下拉控件:子控件从上到下垂直放置,顶部菜单默认隐藏在屏幕外。菜单视觉差异效果支持点击自动展开收起效果。交互无非就是控制布局和手势识别。当然Flutter的开发也是这些套路,不过万物皆Widget。Flutter中常用的基本布局有Column、Row、Stack等,手势识别有Listener、GestureDetector、RawGestureDetector等,这是本文重点介绍的控件,不局限于上面这些Widget,因为Flutter提供了太多的Widget,关键的控件需要牢记,其他时候才是真正使用和检查;那么我们就从布局和手势两大技术点来一一分解功能点。;Layout放置这里所谓的布局包括对Widget的大小和位置的控制。通常,父Widget掌管着子Widget的命运。Flutter是一层Widget嵌套。别着急,下面是一个由外而内的具体案例。解释;下拉控件首先我们需要实现最外层的布局,效果是:子Widget垂直放置,topWidget默认需要放置在屏幕外;如上图,红色区域为屏幕范围,header为header隐藏菜单布局,content为卡片布局主体;首先想到的是垂直布局的坑,Column,想要的效果是内容的高度和父Widget的高度一致。我的第一个想法是对内容进行ExpandedWrapping,结果是内容的高度总是等于Column的高度减去header的高度。产生的现象就是内容的高度没有填满,或者被挤压了。如果你继续使用Colunm,你可能不得不放弃Expanded并手动为内容分配高度。也许这是一个解决方案。但是我不想手动分配内容的高度,太不优雅了,最后果断放弃了Column;还有一个问题是如何隐藏header,我想到了两种解决方案:用外层Transform包裹整个布局,用内层Transform包裹header,然后赋值内层dy=-headerHeight,随着手势动态下拉,header的Transform没有变,但是最外层Transform的dy变了;header的高度是动态变化的,初始高度为0,随着手势下拉动态计算;但是上面这两个都有陷阱。第一个方法会影响控件的点击事件,不会回调onTap方法;第二种方式会影响header内部子Widget的布局,因为高度不断变化,难以控制视差;最终方案最终使用Stack来布局,通过Stack和Positioned实现header布局屏幕外,可以做到让内容布局填充父Widget;PullDragWidget首先讲解Positioned的基本用法。top、bottom、height控制高度和位置,它们一起使用。Top和bottom可以理解为marginTop和marginBottom。顾名思义,height就是直接Widget的高度。如果top配置了bottom,表示高度等于parentHeight-top-bottom。如果top/bottom和height一起使用,那么高度一般是固定的。当然,top和bottom接受负数;然后分析代码,首先_offsetY是下拉距离,它是一个变化的初始值为0,内容需要设置为top=_offsetY和bottom=-_offsetY。变化的是上下位置,高度不会变化。同样,header由top和height控制。高度是固定的,只需要动态改变。顶就够了;用Flutter编写布局非常简单。我强烈推荐使用Stack布局,因为它更灵活并且没有太多限制。要用好Stack,就必须用好Positioned。学好它是对的;cardcontrolcardimplementation效果是按顺序堆叠,错落有致,这个很容易想到Stack来实现,当然有了上面的坑,还是很容易用Stack的;overlapping的效果用Stack很简单,patchwork的效果更可能是真实的比如可以用Positioned,也可以包裹Container来改变margin或者padding,但是考虑到角度的旋转,我选择使用Transform,因为Transform不仅可以玩位移,还可以玩角度和缩放等,它实际上操作的是一个Matrix变换;Transform非常好用,但是在Transform多层嵌套的一些特殊情况下,会出现onTap事件没有响应的情况。我想这应该是Transform的一个bug。暂时没发现拖动事件有问题。这不是待确认的bug,暂时不会影响使用;CardStackWidget_CardWidget简单总结了卡片布局代码。CardStackWidget是管理卡片Stack的父控件,负责每张卡片的布局。_CardWidget用于单个卡片的内部布局。总体来说没什么难度,细节控制逻辑就是计算上层_CardWidget和底层_CardWidget的偏移量;这么多关于布局的内容,总的来说,还是比较简单的。所谓陷阱,不一定就是陷阱,只是不适合某些应用场景而已。手势识别Flutter中最常用的手势识别是Listener和GestureDetector。Listener主要是针对原始接触点的。对于处理,GestureDetector已经将原始触摸点处理成不同的手势;这两个类的方法介绍如下;ListenerGestureDetector手势回调:Listener和GestureDetector如何取舍,首先GestureDetector是基于Listener封装的,解决了大部分手势冲突,我们用GestureDetector就够用了,但是GestureDetector也不是万能的,还需要RawGestureDetector必要时定制;另外一个很重要的概念,Flutter的手势事件是一种从内层Widget到外层Widget的冒泡机制,假设内层Widget和外层Widget监听垂直方向的垂直拖动事件onVerticalDragUpdate往往是内层控件获取到的事件,而外层事件被被动取消;这个概念和Android的父布局拦截机制完全不同;Flutter虽然没有外层的拦截机制,但是好像还有一线希望,那就是IgnorePointer和AbsorbPointerWidget,这两个哥们可以忽略或者阻止子Widget树响应Event;介绍了手势分析的基本原理,然后分析了案例交互。如上所述,我将整体布局拆分为下拉控件和卡片。控件,分析InstantApp的拖动行为:当下拉控件不展开下拉菜单时,卡片控件可以响应上下左右三个方向的手势,下拉控件仅响应向下方向的手势;当下拉菜单展开时,卡片无法响应任何手势,下拉控件可以响应垂直方向的所有事件;上图比较形象的解释了两种状态下的手势响应,下拉控件是父widget,卡片控件是子widget,因为子widget可以优先Ring手势,所以在初始阶段,我们不能让子Widget响应向下的手势;由于GestureDetector只封装了水平和垂直手势,两种手势不能同时使用,我们从GestureDetector的源码可以看出是否可以封装一个Monitor的四个不同方向的手势;GestureDetectorGestureDetector最后返回RawGestureDetector,其中gestures是地图,垂直手势在VerticalDragGestureRecognizer类中;VerticalDragGestureRecognizerVerticalDragGestureRecognizer继承了DragGestureRecognizer,大部分逻辑在DragGestureRecognizer中,我们只关注重写的方法:_hasSufficientPendingDragDeltaToAccept方法是关键逻辑,控制是否接受拖动手势。返回拖动进度的dx和dy偏移量_getPrimaryValueFromOffset返回一个方向的手势值,对于不同的方向(水平和垂直),可以通过null_isFlingGesture是否自定义手势的Fling行为DragGestureRecognizer要接受三个方向的手势,自定义DragGestureRecognizer是个好主意;希望接受上下左右四个方向的参数,根据不同的参数监听不同的手势行为,根据葫芦画画定制一个接受方向的GestureRecognizer:DirectionGestureRecognizer可以参考因为很多方法原demo中的DragGestureRecognizer是私有的,我只能重新复制一份代码,然后重写main方法,根据不同的输入参数处理不同的手势逻辑;黑板提示,自定义DragGestureRecognizer时:_getDeltaForDetails的返回值表示dx和dy的偏移量。如果只有水平或只有垂直方向,则需要将另一个方向的dx或dy设置为0;当当前Widget树只有一个手势时,手势判断逻辑_hasSufficientPendingDragDeltaToAccept可能不会被调用。这时候就必须重写_getDeltaForDetails控件,返回dx和dy;如何使用自定义的DirectionGestureRecognizer配置左、右、上、下四个方向的手势,并支持不同方向的组合;比如我们只想监听垂直向下的方向,我们创建一个DirectionGestureRecognizer(DirectionGestureRecognizerrecognizer.down)手势识别;如果要监听上、左、右手势,创建一个DirectionGestureRecognizer(DirectionGestureRecognizer.left|DirectionGestureRecognizer.right|DirectionGestureRecognizer.up)手势识别;DirectionGestureRecognizer就像一块磨刀石,刀已经磨好了,下料很轻松,下面是控件的手势实现;下拉控件手势PullDragWidgetPullDragWidget是一个下拉拖动控件,根Widget是一个用于监听手势的RawGestureDetector,手势支持下拉和点击两种手势;当下拉时,控件处于_opened状态,表示header已经被下拉。这个时候配合IgnorePointer,禁用所有子widget的事件监听。自然地,内部卡不能响应任何事件;卡片控件手势和下拉控件一样,卡片控件只需要监听其他三个方向手势就可以完成任务:CardStackWidget手势问答为什么不使用onPanDownonPanUpdateonPanEnd来拖动?这是掘金评论提出的问题。我来回答一下:GestureDetector中有Pan手势和Drag手势。这两种手势都可以用于拖动场景,不同的是Drag手势仅限于水平和垂直监控,Pan手势不限制方向,可以任意方向监听。此外,触发条件也不一致。Pan手势的触发条件是滑动屏幕的距离大于kTouchSlop*2,Drag手势的触发条件是dx或dy大于kTouchSlop。dx、dy、distance构成勾股定理的三个边长;假设垂直滑动的场景也被监听,VerticalDrag总是在Pan之前触发;如果下拉控件使用VerticalDrag卡片控件使用Pan,下拉控件会优先向上拖动,卡片控件将失去向上拖动的机会,无法实现交互。退一步说,即使Pan的触发条件和VerticalDrag一样,由于Flutter的事件传递是从里到外的,这会导致外层的下拉控件完全失去响应的机会.以上是我个人的理解。如有误导,欢迎评论指正。手势总结分析Flutter手势的冒泡特性。父Widget既没有优先响应事件,也没有监听单一方向(左、右、上、下)的手势。它只能自己想办法自定义GestureRecognizer。将原有的Vertical和Horizo??ntal两个方向的Gesture识别扩展到左、右、上、下四个方向,区分会引起冲突的手势;当然,这个交互的手势识别的实现可能还有其他的方案,条条大路通罗马,我只是如果你有好的方案,可以积极留言,提出你的宝贵意见;由于总结知识点篇幅有限,我们没有介绍所有的交互内容。我深感抱歉。我总结一下代码中用到的知识点:Column、Row、Expanded、Stack、Positioned、Transform等Widget;GestureDetector、RawGestureDetector、IgnorePointer和其他Widget;自定义GestureRecognizer,实现自定义手势识别;AnimationController、Tween等动画;事件总线的使用;最后一章主要介绍了当前场景下Flutter的布局和手势的实用技巧,包括更深层次的手势竞争和分配的源码级分析,有机会可以做深入学习和分享;另外,本文并不是一步步零基础的介绍,对于新人来说可能会有一点感觉。没关系,但没关系。我建议你克隆一份代码并运行它,也许你可以激发你的学习兴趣。最后,本文所有代码均已开源。您的好评是对我最大的鼓励。项目地址:https://github.com/HitenDev/F...阅读更多一波Flutter炫酷特效打金三银四,2019最新实战面试总结从??不纠结算法,如何优化冒泡排序?动画:一种学习TCP的三向握手和四向挥手的方法关于Gradle,这个足以搞定Groovy闭包