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

实现下拉刷新。

群组

群组

新建联系人页面

新建联系人

点击取消时

编辑头像

编辑头像

移动和缩放

选择滤镜

选择后,再次编辑

联系人详情

联系人详情

长按复制

选择标签

选择标签

至此,基本完成。

相关文章
相关标签/搜索