8天让iOS开发者上手Flutter之五

1,729 阅读8分钟

上篇文章,我们已经完成了通讯录的列表。这篇文章介绍完成通讯录右侧的索引条的功能。

显示索引条

之前我们已经做过了我的页面的布局,我的页面上有一个列表和一个拍照按钮,和我们今天要实现的索引条布局十分类似。我的页面的布局如下: image.png 通讯录界面的布局,和我的页面的布局都是使用一个Stack包含列表和其他子视图来实现。索引条是紧贴屏幕右侧,然后里面的子视图是由上至下的。所以自然的会想到使用一个Positioned包含Column来实现。Positioned和Stack的组合我们之前讲过,这两个组合起来使用,就和我们iOS的约束布局类似,可以设置上左宽高等等。Column就更不用多说,我们已经使用过很多次了。所以代码如下图所示: image.png

然后优化一下索引条的位置,高度我们设置为屏幕高度的一半,那么上下的间距就不能设置为0了,设置距离上间距为屏幕的1/8看起来比较合适。

image.png

抽取IndexBar

写到这里我们会发现,这个索引条还有很多的功能需要我们来实现,还是有点复杂的,如果代码都写在friends_page.dart里会有点冗余,我们完全可以将这个索引条作为一个独立的Widget来实现。新建index_bar.dart文件,代码如下: image.png

实现IndexBar的点击切换状态

当没有触摸到IndexBar的时候,默认是不展示背景色的,文字也是黑色的。当我们开始点击IndexBar的时候,显示出背景色,然后文字也变成了白色。 实现这个功能,主要是要对GestureDetector的两个方法有所了解。onVerticalDragDown方法会在手指触摸IndexBar的时候就会被调用,onVerticalDragEnd会在手指松开屏幕的时候调用。利用这两个方法就可以实现需求。代码如下: image.png 因为要对文字的颜色进行修改,所以初始化Text的时候,就需要使用变量_textColor; image.png

获取当前选中的下标

同样是对GestureDetector的一个手势方法的使用,onVerticalDragUpdate这个方法的调用时机,在手指移动的时候会不停的调用这个方法。这个方法有一个DragUpdateDetails参数,它包含了手指所在的坐标信息。不过是相对于整个屏幕的坐标,可以将它转化为相对于IndexBar的坐标,然后通过计算可以得到我们当前选中的是哪个下标。代码如下: image.png 方法中的~/是flutter特有的运算符,意思是除后取整。而clamp()是对边界情况的处理,意思调用该函数的结果在它的两个参数之间。

回调选中的下标

这里的回调,和OC里面的block,Swift里面的闭包都是一个意思。flutter里面带有下划线的变量是私有的,外部无法访问的。所以对外暴露的参数,不能写在_IndexBarState类里面,需要写在IndexBar类里面。声明一个闭包(或者叫block)属性,作为必传参数在初始化的时候传入。 image.png 这样,在friends_page.dart文件中初始化IndexBar的时候,就需要传入一个闭包。然后IndexBar内部在onVerticalDragUpdate的时候,调用这个闭包,就可以将当前选中的下标回调给外部了。 image.png image.png 这个时候,会发现一个小问题,就是点击IndexBar的时候,回调没有执行,只有在点击并手指挪动的时候才会执行。所以需要在onVerticalDragDown方法里面也调用一次闭包。这时候如果直接将onVerticalDragUpdate方法里面的代码复制到onVerticalDragDown方法里面确实没有问题,但是会明显的看到重复的代码太多了。 image.png 所以可以抽取一个方法,将重复的代码放到一块。 image.png 然后调用的时候就简单多了。 image.png

优化回调执行的频率

已经成功的实现了回调,但是从打印的结果来看,会发现同样一个下标会被回调许多次。这样我们滚动好友列表的时候会造成不必要的性能消耗。明明只需要滚动一次,结果却滚动了无数次到同一个位置。所以这里我们需要优化一下,一个很自然的想法就是记录一个_currentIndexLetter,每次执行回调的时候,判断回调的首字母是否和_currentIndexLetter是否不同,如果是一样的就没有必要回调了,只有不同的时候,才执行回调。 image.png 代码如下: image.png image.png 这样回调的频率就正常了。

滚动好友列表ListView

可滚动的widget都有一个controller属性,用于控制滚动条的行为。controller属性是一个ScrollController对象。可以使用它来实现指定滚动到某个位置,实现回到顶部等功能。 滚动好友列表需要一个新的对象ScrollController实例,将它设置给ListView的controller属性,然后就可以通过使用ScrollController实例来操作ListView的滚动。 image.png image.png 这里暂时先将滚动的偏移设置为固定值250,试试看效果。可以看到当我们点击IndexBar的时候,ListView就会滚动到偏移为250的地方。接下来就是处理滚动的实际偏移值了。

滚动的实际偏移,是根据我们的数据源来计算的。因为我们的cell的高度是确定的,不显示组头的cell高度是54,有组头的cell高度是54 + 30 = 84。使用首字母作为key,计算出对应的偏移为offset,然后使用Map(类似iOS中字典)记录下来。由于第一个是不是字母,而是搜索符号,而它对应的偏移也是固定的0。所以可以在初始化Map的时候就指定好。而其他的高度我们在initState方法中计算。代码如下: image.png

有了这个Map之后,我们在IndexBar的回调方法中,就可以根据IndexBar回调给我们的首字母得到对应的偏移值了。代码如下: image.png

到这里,我们的IndexBar基本上就实现了滚动ListView的功能。但是滚动几次之后就会发现一个小问题。。。滚动到底部的几个组头的时候,会出现ListView先将组头滚动到指定位置,然后又滚回底部的情况。原因很好理解,后面的组头内容不够显示一整个屏幕了。所以我们这里需要做下处理。这里主要是对ListView的滚动的监听,如果是在iOS中我们会想去获取滚动视图的contentSize然后减去UITableView的高度,就是UITableView的最大的滚动范围。而在Flutter中,这些都不需要我们计算了。

如果需要获取到ListView的一些滚动相关的信息,可以将它包裹在NotificationListener里面,它有一个onNotification属性,是一个闭包,可以回调给我们一些滚动的相关信息。包含在闭包参数ScrollNotification note里面。准确来讲滚动相关的信息包含在ScrollNotification的属性metrics里面。它包含当前滚动偏移值,能滚动的最大范围(这就是我们iOS中contentSize的高减去UITableView的高)等等信息。完整代码如下: image.png_maxScrollExtent定义为一个属性就好了。需要注意的是并不能给初始值为0,否则没有滚动ListView之前,使用IndexBar就无法滚动ListView了。 image.png 至此,IndexBar滚动ListView的功能就实现了。

显示指示器

终于来到了最后一步,显示我们IndexBar的指示器。首先考虑的就是布局。最初的IndexBar只有右侧的下标一列。现在我们左侧需要一块容器用来显示我们的指示器,所以IndexBar的根视图应该考虑改为Row。指示器背景的不规则图形可以使用一张图片展示,图片已经准备好了。中间的文字,使用Text就够了。先看下大概布局代码: image.png

如果觉得位置不是很合适,可以修改一下各自的宽度。然后是对指示器的显示与隐藏做控制,指示器的显示与隐藏的控制,应该说跟背景色的显示隐藏是类似的。都是在手势的那两个方法里面实现控制。使用一个bool变量来控制指示器的显示与隐藏,在手势的触摸方法和离开方法里面操作这个bool变量,然后setState()就可以实现了指示器的显示与隐藏了。然后是关于指示器的显示文本的。这个文本就是我们的_currentIndexLetter,直接使用就好了。最后是关于如何控制整个IndexBar的上下位移的。通过对Alignment的使用,发现可以控制IndexBar的上下位移。通过不断的修改Alignment的y值会找到一个合适的y值指向第一个放大镜,那么-y就指向最后一个字母Z。我这里试了几次发现y=-1.13的时候,指示器刚刚好指向第一个放大镜的位置。那么现在的问题就是将1.13 * 2 = 2.26分成_index_words.length - 1份,然后根据选择的下标,取得对应的Alignment的y值。当我们选择第一个的时候下标为0,y值应该为-1.13,当我们选择最后一个的时候下标为_index_words.length - 1,y值应该为1.13。根据这些信息就可以找到计算y值的公式。最终的代码如下:
新增两个变量_showIndicator_indicatorAlignmentYimage.png 使用这两个变量还有_currentIndexLetter image.png

到这里,我们就终于实现了通讯录的IndexBar的封装。下一节会介绍一些网络请求了...