此文章主要介绍怎么使用Flutter的Cupertino风格控件,写一个iOS风格的通信录,还有在此过程当中遇到的问题及解决办法。git
你们在用Flutter写App的时候,通常都会使用material风格的控件,由于material风格的控件比较丰富,可是,他在iOS上就会显得Android气息比较重,不太适合,因此本文章将经过用仿写iOS通信录,系统地介绍Cupertino控件,及系统的一些底层控件和怎么本身定义优美的适合本身的控件。github
因为使用的联系人三方包的限制,有些功能未能实现,我会持续关注这个联系人插件的更新,及时加上新功能。app
Github地址ide
一个iOS风格Scaffold,能够添加NavigationBar。ui
实现浮动的NavigationBar和SearchBar。this
NestedScrollView我用的本身重写过的,主要是由于源码中的有两个问题。插件
一、当列表滑动到底部,而后继续滑动,而后中止,松手,这时候可列表会从新滚动到底部,可是源码没有处理当速度等于0的时候的状况,因此当松手的时候,列表会回弹回去,回弹距离小于maxScrollExtent。3d
源码以下:code
@protected ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) { return position.createBallisticScrollActivity( position.physics.createBallisticSimulation( velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity), velocity, ), mode: _NestedBallisticScrollActivityMode.inner, ); }
这里当velocity == 0
的时候,直接把innerPosition赋值给了createBallisticSimulation方法的position参数,咱们继续往下看。blog
ScrollActivity createBallisticScrollActivity( Simulation simulation, { @required _NestedBallisticScrollActivityMode mode, _NestedScrollMetrics metrics, }) { if (simulation == null) return IdleScrollActivity(this); assert(mode != null); switch (mode) { case _NestedBallisticScrollActivityMode.outer: assert(metrics != null); if (metrics.minRange == metrics.maxRange) return IdleScrollActivity(this); return _NestedOuterBallisticScrollActivity( coordinator, this, metrics, simulation, context.vsync, ); case _NestedBallisticScrollActivityMode.inner: return _NestedInnerBallisticScrollActivity( coordinator, this, simulation, context.vsync, ); case _NestedBallisticScrollActivityMode.independent: return BallisticScrollActivity(this, simulation, context.vsync); } return null; }
这里velocity == 0
的时候,执行的是
case _NestedBallisticScrollActivityMode.inner: return _NestedInnerBallisticScrollActivity( coordinator, this, simulation, context.vsync, );
这时候的simulation
就是上面经过innerPosition获得的,而后传给了_NestedInnerBallisticScrollActivity
,咱们在继续往下看,
class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity { _NestedInnerBallisticScrollActivity( this.coordinator, _NestedScrollPosition position, Simulation simulation, TickerProvider vsync, ) : super(position, simulation, vsync); final _NestedScrollCoordinator coordinator; @override _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition; @override void resetActivity() { delegate.beginActivity(coordinator.createInnerBallisticScrollActivity( delegate, velocity, )); } @override void applyNewDimensions() { delegate.beginActivity(coordinator.createInnerBallisticScrollActivity( delegate, velocity, )); } @override bool applyMoveTo(double value) { return super.applyMoveTo(coordinator.nestOffset(value, delegate)); } }
咱们发现这里执行的操做并非咱们想要的,当velocity == 0
,滑动距离大于maxScrollExtent
的时候,咱们只想滚动到列表的最底部,因此咱们改一下这里的实现。此处有两种实现方式:
_getMetrics
方法// This handles going forward (fling up) and inner list is // underscrolled, OR, going backward (fling down) and inner list is // scrolled past zero. We want to skip the pixels we don't need to grow // or shrink over. if (velocity > 0.0) { // shrinking extra = _outerPosition.minScrollExtent - _outerPosition.pixels; } else if (velocity < 0.0) { // growing extra = _outerPosition.pixels - (_outerPosition.maxScrollExtent - _outerPosition.minScrollExtent); } else { extra = 0.0; } assert(extra <= 0.0); minRange = _outerPosition.minScrollExtent; maxRange = _outerPosition.maxScrollExtent + extra; assert(minRange <= maxRange); correctionOffset = 0.0;
这里加上velocity == 0
的判断。
createInnerBallisticScrollActivity
方法,加上velocity == 0
的判断。@protected ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) { return position.createBallisticScrollActivity( position.physics.createBallisticSimulation( velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity), velocity, ), mode: velocity == 0 ? _NestedBallisticScrollActivityMode.independent : _NestedBallisticScrollActivityMode.inner, ); }
二、当咱们手动调用position.moveTo
方法滚动到最底部的时候,获取到的maxScrollExtent
并非实际innerPosition
的maxScrollExtent
,而应该是maxScrollExtent - outerPosition.maxScrollExtent + outerPosition.pixels
。
接下来咱们分析源码看看哪里出了问题。 首先,咱们看看与之有直接关联的maxScrollExtent方法。
@override double get maxScrollExtent => _maxScrollExtent;
咱们看到只是单纯的返_maxScrollExtent
,那咱们看看_maxScrollExtent
是在哪里赋值的,通过查看源码得知,_maxScrollExtent
赋值的地方主要在下面这个方法里:
@override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { assert(minScrollExtent != null); assert(maxScrollExtent != null); if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || _didChangeViewportDimensionOrReceiveCorrection) { assert(minScrollExtent != null); assert(maxScrollExtent != null); assert(minScrollExtent <= maxScrollExtent); _minScrollExtent = minScrollExtent; _maxScrollExtent = maxScrollExtent; _haveDimensions = true; applyNewDimensions(); _didChangeViewportDimensionOrReceiveCorrection = false; } return true; }
因此咱们重写这个方法,修改以下:
@override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { assert(minScrollExtent != null); assert(maxScrollExtent != null); var outerPosition = coordinator._outerPosition; var outerMaxScrollExtent = outerPosition.maxScrollExtent; var outerPixels = outerPosition.pixels; if (outerMaxScrollExtent != null && outerPixels != null) { maxScrollExtent -= outerMaxScrollExtent - outerPixels; maxScrollExtent = math.max(minScrollExtent, maxScrollExtent); } return super.applyContentDimensions(minScrollExtent, maxScrollExtent); }
这样咱们成功解决了上面提到的两个问题。
实现浮动的Index。
实现Index固定在头部。
实现下拉刷新。
至此,基本完成。