此文章主要介紹怎麼使用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固定在頭部。
實現下拉刷新。
至此,基本完成。