Flutter仿寫一個iOS風格的通信錄

此文章主要介紹怎麼使用Flutter的Cupertino風格控件,寫一個iOS風格的通信錄,還有在此過程當中遇到的問題及解決辦法。git

你們在用Flutter寫App的時候,通常都會使用material風格的控件,由於material風格的控件比較豐富,可是,他在iOS上就會顯得Android氣息比較重,不太適合,因此本文章將經過用仿寫iOS通信錄,系統地介紹Cupertino控件,及系統的一些底層控件和怎麼本身定義優美的適合本身的控件。github

因爲使用的聯繫人三方包的限制,有些功能未能實現,我會持續關注這個聯繫人插件的更新,及時加上新功能。app

Github地址ide

首頁

首頁截圖

主要用到的控件及問題

CupertinoPageScaffold

一個iOS風格Scaffold,能夠添加NavigationBar。ui

NestedScrollView

實現浮動的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並非實際innerPositionmaxScrollExtent,而應該是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);
}

這樣咱們成功解決了上面提到的兩個問題。

CustomScrollView

實現浮動的Index。

SliverPersistentHeader

實現Index固定在頭部。

CupertinoSliverRefreshIndicator

實現下拉刷新。

羣組

羣組

新建聯繫人頁面

新建聯繫人

點擊取消時

編輯頭像

編輯頭像

移動和縮放

選擇濾鏡

選擇後,再次編輯

聯繫人詳情

聯繫人詳情

長按複製

選擇標籤

選擇標籤

至此,基本完成。

相關文章
相關標籤/搜索