Flutter仿写一个iOS风格的通讯录

Stella981
• 阅读 925

此文章主要介绍怎么使用Flutter的Cupertino风格控件,写一个iOS风格的通讯录,还有在此过程中遇到的问题及解决办法。

大家在用Flutter写App的时候,一般都会使用material风格的控件,因为material风格的控件比较丰富,但是,他在iOS上就会显得Android气息比较重,不太适合,所以本文章将通过用仿写iOS通讯录,系统地介绍Cupertino控件,及系统的一些底层控件和怎么自己定义优美的适合自己的控件。

由于使用的联系人三方包的限制,有些功能未能实现,我会持续关注这个联系人插件的更新,及时加上新功能。

Github地址

首页

Flutter仿写一个iOS风格的通讯录

主要用到的控件及问题

CupertinoPageScaffold

一个iOS风格Scaffold,可以添加NavigationBar。

NestedScrollView

实现浮动的NavigationBar和SearchBar。

NestedScrollView我用的自己重写过的,主要是因为源码中的有两个问题。

1、当列表滑动到底部,然后继续滑动,然后停止,松手,这时候可列表会重新滚动到底部,但是源码没有处理当速度等于0的时候的情况,所以当松手的时候,列表会回弹回去,回弹距离小于maxScrollExtent

源码如下:

@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参数,我们继续往下看。

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,
  );
}

2、当我们手动调用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

实现下拉刷新。

群组

Flutter仿写一个iOS风格的通讯录

新建联系人页面

Flutter仿写一个iOS风格的通讯录

Flutter仿写一个iOS风格的通讯录

编辑头像

Flutter仿写一个iOS风格的通讯录

Flutter仿写一个iOS风格的通讯录

Flutter仿写一个iOS风格的通讯录

Flutter仿写一个iOS风格的通讯录

联系人详情

Flutter仿写一个iOS风格的通讯录

Flutter仿写一个iOS风格的通讯录

选择标签

Flutter仿写一个iOS风格的通讯录

至此,基本完成。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
2年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
JS 苹果手机日期显示NaN问题
问题描述newDate("2019122910:30:00")在IOS下显示为NaN原因分析带的日期IOS下存在兼容问题解决方法字符串替换letdateStr"2019122910:30:00";datedateStr.repl
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
6个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这