Flutter writes an iOS style address book

Posted by Shamrox on Tue, 28 Apr 2020 22:09:31 +0200

This article mainly introduces how to use the Cupertino style control of Flutter to write an iOS style address book, as well as the problems encountered in the process and solutions.

When you write apps with Flutter, you will generally use material style controls. Because material style controls are rich, but they will appear Android flavor on iOS, which is not suitable. So this article will systematically introduce Cupertino controls, some of the underlying controls of the system and how to define a beautiful and suitable self by imitating iOS address book Your control.

Due to the limitation of the three-party contact package, some functions are not implemented. I will continue to pay attention to the update of this contact plug-in and add new functions in time.

Github address

home page

Main controls and problems


An iOS style Scaffold that can add NavigationBar.


Implement floating NavigationBar and SearchBar.

NestedScrollView I have rewritten with myself, mainly because there are two problems in the source code.

1. When the list slides to the bottom, then continues to slide, and then stops, let go. At this time, the list will scroll to the bottom again, but the source code does not deal with the situation when the speed is equal to 0, so when the list is released, it will rebound back, and the rebound distance is less than maxscrollextend.

The source code is as follows:

@protectedScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {  return position.createBallisticScrollActivity(
      velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
    mode: _NestedBallisticScrollActivityMode.inner,

In this case, when velocity == 0, the innerPosition is directly assigned to the position parameter of the createballisticisimulation method. Let's go on.

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,
      case _NestedBallisticScrollActivityMode.inner:        return _NestedInnerBallisticScrollActivity(
          coordinator,          this,
      case _NestedBallisticScrollActivityMode.independent:        return BallisticScrollActivity(this, simulation, context.vsync);
  }  return null;

When velocity == 0, the execution is

case _NestedBallisticScrollActivityMode.inner:  return _NestedInnerBallisticScrollActivity(
    coordinator,    this,

At this time, the simulation is obtained through innerPosition above, and then passed to "NestedInnerBallisticScrollActivity". We will continue to look down,

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() {
  }  @override
  void applyNewDimensions() {
  }  @override
  bool applyMoveTo(double value) {    return super.applyMoveTo(coordinator.nestOffset(value, delegate));

We found that the operation performed here is not what we want. When the velocity == 0 and the sliding distance is greater than maxscrollextend, we just want to scroll to the bottom of the list, so we need to change the implementation here. There are two ways to achieve this:

The first way: change the "getMetrics" method
// 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;

Add the judgment of velocity == 0.

The second method: modify the createInnerBallisticScrollActivity method, and add the velocity == 0 judgment.
@protectedScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {  return position.createBallisticScrollActivity(
      velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
    ),    mode: velocity == 0 ? _NestedBallisticScrollActivityMode.independent : _NestedBallisticScrollActivityMode.inner,

2. When we manually call the position.moveTo method to scroll to the bottom, the maxscrollextend obtained is not the actual innerPosition maxscrollextend, but should be maxscrollextend - outerposition.maxscrollextend + outerposition.pixels.

Next let's analyze the source code to see what's wrong. First, let's look at the maxscrollextend method that is directly related to it.

@overridedouble get maxScrollExtent => _maxScrollExtent;

We can see that it's just a simple way to return to maxscrollextend. Let's see where maxscrollextend is assigned. After checking the source code, we can see that the assignment of maxscrollextend is mainly in the following method:

@overridebool 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;
    _didChangeViewportDimensionOrReceiveCorrection = false;
  }  return true;

So we rewrite this method and modify it as follows:

@overridebool 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);

In this way, we have successfully solved the two problems mentioned above.


Implement floating Index.


The Index is fixed on the head.


Implement pull-down refresh.


New contact page

Edit Avatar

Contact details

Zhengzhou polycystic ovary hospital: http://jbk.39.net/yiyunfengcai/tsyl_zztjyy/1225/

Select label

Zhengzhou infertility hospital: http://www.03913882333.com/

So far, it is basically completed.

Topics: iOS Android github less