Flutter仿写一个iOS风格的通讯录

此文章主要介绍怎么使用Flutter的Cupertino风格控件,写一个iOS风格的通讯录,还有在此过程中遇到的问题及解决办法。

大家在用Flutter写App的时候,一般都会使用material风格的控件,因为material风格的控件比较丰富,但是,他在iOS上就会显得Android气息比较重,不太适合,所以本文章将通过用仿写iOS通讯录,系统地介绍Cupertino控件,及系统的一些底层控件和怎么自己定义优美的适合自己的控件。

由于使用的联系人三方包的限制,有些功能未能实现,我会持续关注这个联系人插件的更新,及时加上新功能。

Github地址

首页

首页截图

主要用到的控件及问题

CupertinoPageScaffold

一个iOS风格Scaffold,可以添加NavigationBar。

NestedScrollView

实现浮动的NavigationBar和SearchBar。

NestedScrollView我用的自己重写过的,主要是因为源码中的有两个问题。

1、当列表滑动到底部,然后继续滑动,然后停止,松手,这时候可列表会重新滚动到底部,但是源码没有处理当速度等于0的时候的情况,所以当松手的时候,列表会回弹回去,回弹距离小于maxScrollExtent。

源码如下:

1@protected 2ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) { 3 return position.createBallisticScrollActivity( 4 position.physics.createBallisticSimulation( 5 velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity), 6 velocity, 7 ), 8 mode: _NestedBallisticScrollActivityMode.inner, 9 ); 10}

这里当velocity == 0的时候,直接把innerPosition赋值给了createBallisticSimulation方法的position参数,我们继续往下看。

1ScrollActivity createBallisticScrollActivity( 2 Simulation simulation, { 3 @required _NestedBallisticScrollActivityMode mode, 4 _NestedScrollMetrics metrics, 5}) { 6 if (simulation == null) return IdleScrollActivity(this); 7 assert(mode != null); 8 switch (mode) { 9 case _NestedBallisticScrollActivityMode.outer: 10 assert(metrics != null); 11 if (metrics.minRange == metrics.maxRange) return IdleScrollActivity(this); 12 return _NestedOuterBallisticScrollActivity( 13 coordinator, 14 this, 15 metrics, 16 simulation, 17 context.vsync, 18 ); 19 case _NestedBallisticScrollActivityMode.inner: 20 return _NestedInnerBallisticScrollActivity( 21 coordinator, 22 this, 23 simulation, 24 context.vsync, 25 ); 26 case _NestedBallisticScrollActivityMode.independent: 27 return BallisticScrollActivity(this, simulation, context.vsync); 28 } 29 return null; 30}

这里velocity == 0的时候,执行的是

1case _NestedBallisticScrollActivityMode.inner: 2 return _NestedInnerBallisticScrollActivity( 3 coordinator, 4 this, 5 simulation, 6 context.vsync, 7 );

这时候的simulation就是上面通过innerPosition得到的,然后传给了_NestedInnerBallisticScrollActivity,我们在继续往下看,

1class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity { 2 _NestedInnerBallisticScrollActivity( 3 this.coordinator, 4 _NestedScrollPosition position, 5 Simulation simulation, 6 TickerProvider vsync, 7 ) : super(position, simulation, vsync); 8 9 final _NestedScrollCoordinator coordinator; 10 11 @override 12 _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition; 13 14 @override 15 void resetActivity() { 16 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity( 17 delegate, 18 velocity, 19 )); 20 } 21 22 @override 23 void applyNewDimensions() { 24 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity( 25 delegate, 26 velocity, 27 )); 28 } 29 30 @override 31 bool applyMoveTo(double value) { 32 return super.applyMoveTo(coordinator.nestOffset(value, delegate)); 33 } 34}

我们发现这里执行的操作并不是我们想要的,当velocity == 0,滑动距离大于maxScrollExtent的时候,我们只想滚动到列表的最底部,所以我们改一下这里的实现。此处有两种实现方式:

第一种方式:改_getMetrics方法
1// This handles going forward (fling up) and inner list is 2// underscrolled, OR, going backward (fling down) and inner list is 3// scrolled past zero. We want to skip the pixels we don't need to grow 4// or shrink over. 5if (velocity > 0.0) { 6 // shrinking 7 extra = _outerPosition.minScrollExtent - _outerPosition.pixels; 8} else if (velocity < 0.0) { 9 // growing 10 extra = _outerPosition.pixels - (_outerPosition.maxScrollExtent - _outerPosition.minScrollExtent); 11} else { 12 extra = 0.0; 13} 14assert(extra <= 0.0); 15minRange = _outerPosition.minScrollExtent; 16maxRange = _outerPosition.maxScrollExtent + extra; 17assert(minRange <= maxRange); 18correctionOffset = 0.0;

这里加上velocity == 0的判断。

第二种方式:修改createInnerBallisticScrollActivity方法,加上velocity == 0的判断。
1@protected 2ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) { 3 return position.createBallisticScrollActivity( 4 position.physics.createBallisticSimulation( 5 velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity), 6 velocity, 7 ), 8 mode: velocity == 0 ? _NestedBallisticScrollActivityMode.independent : _NestedBallisticScrollActivityMode.inner, 9 ); 10}

2、当我们手动调用position.moveTo方法滚动到最底部的时候,获取到的maxScrollExtent并不是实际innerPosition的maxScrollExtent,而应该是maxScrollExtent - outerPosition.maxScrollExtent + outerPosition.pixels。

接下来我们分析源码看看哪里出了问题。 首先,我们看看与之有直接关联的maxScrollExtent方法。

1@override 2double get maxScrollExtent => _maxScrollExtent;

我们看到只是单纯的返_maxScrollExtent,那我们看看_maxScrollExtent是在哪里赋值的,经过查看源码得知,_maxScrollExtent赋值的地方主要在下面这个方法里:

1@override 2bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { 3 assert(minScrollExtent != null); 4 assert(maxScrollExtent != null); 5 if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || 6 !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || 7 _didChangeViewportDimensionOrReceiveCorrection) { 8 assert(minScrollExtent != null); 9 assert(maxScrollExtent != null); 10 assert(minScrollExtent <= maxScrollExtent); 11 _minScrollExtent = minScrollExtent; 12 _maxScrollExtent = maxScrollExtent; 13 _haveDimensions = true; 14 applyNewDimensions(); 15 _didChangeViewportDimensionOrReceiveCorrection = false; 16 } 17 return true; 18}

所以我们重写这个方法,修改如下:

1@override 2bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { 3 assert(minScrollExtent != null); 4 assert(maxScrollExtent != null); 5 var outerPosition = coordinator._outerPosition; 6 var outerMaxScrollExtent = outerPosition.maxScrollExtent; 7 var outerPixels = outerPosition.pixels; 8 if (outerMaxScrollExtent != null && outerPixels != null) { 9 maxScrollExtent -= outerMaxScrollExtent - outerPixels; 10 maxScrollExtent = math.max(minScrollExtent, maxScrollExtent); 11 } 12 return super.applyContentDimensions(minScrollExtent, maxScrollExtent); 13}

这样我们成功解决了上面提到的两个问题。

CustomScrollView

实现浮动的Index。

SliverPersistentHeader

实现Index固定在头部。

CupertinoSliverRefreshIndicator

实现下拉刷新。

群组

群组

新建联系人页面

新建联系人

点击取消时

编辑头像

编辑头像

移动和缩放

选择滤镜

选择后,再次编辑

联系人详情

联系人详情

长按复制

选择标签

选择标签

至此,基本完成。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

一篇文章带你了解JavaScript日期

日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用