第 5 章:无限滚动视图:实现

原文链接
作者:C4 开源项目
译者:Crystal Sun
全部章节请关注此文集C4教程翻译
校对后的内容请看这里

需要你做下面几件事情:

  1. UIScrollview 的子类
  2. 重写 layoutSubviews() 方法
  3. 抓住 currentOffset
  4. 如果需要,跳过结尾或者开始

UIScrollview 的子类是可选的,这里已经存在一个 InfiniteScrollview.swift 文件了,你可以在这里编写代码。如果你选择跳过这部分,那你可以直接跳到 增加一些内容 这段。然而,对于之前没有做过的同学,我已经把创建子类的步骤下在下面了。

UIScrollview 的子类

在 Xcode 里创建一个新的文件,可以用这种方法:

File > New > File...

或者敲击快捷键:

CMD + N

选择:

iOS > Source > Swift File

命名文件:

InfiniteScrollView

文件创建后,将下面这行代码删除:

import Foundation

换成这行代码:

import UIKit

接着,输入 class 单词,在你输入整个单词之前,你会看到类似下图的弹出框:

显示自动补全的内容,有 Subclass 选项

选择 Swift Subclass 敲击回车。Xcode 会给你刚刚创建的类自动提供空白来让你输入内容。

选择 name 然后输入 InfiniteScrollView

点击 Tab 键,光标出现在 super class variable 这里,输入 UIScrollView

再次点击 Tab 键,光标出现在 properties and methods variable 这里。

输入 layoutSubviews,输入的时候会出现自动补全,点击 回车 即可使用自动补全功能。

接下来,复制下面的代码,粘贴到你刚刚创建的新方法里:

super.layoutSubviews()

完成了(查了半天,finito是西班牙语 我完成了 的意思)。你创建了一个子类叫做 'InfiniteScrollView',这个类和 UIScrollView 的功能一模一样,不过,还没开发什么特别的功能,所以,让我们来开发无限效果。

另外,你的方法应该看起来和下面这个方法一样:

override func layoutSubviews() {
   super.layoutSubviews() //调用父类的方法
}

放到屏幕上

是时候来测试你新创建的类了,所以,回到 WorkSpace 文件,然后输入下列代码:

class WorkSpace: CanvasController {
    let infiniteScrollView = InfiniteScrollView() //创建一个新的 InfiniteScrollview 对象
    override func setup() {
        //根据 canvas 的 frame 创建 CGRect
        //然后赋值给 infiniteScrollView,让 infiniteScrollView 的大小和 canvas 一样大
        infiniteScrollView.frame = CGRect(canvas.frame)
        
        //把 infiniteScrollView 添加到 canvas 里
        canvas.add(infiniteScrollView)
    }
}

创建了一个名为 infiniteScrollViewInfiniteScrollView 变量,将其 frame 的等同于当前 canvas 的大小,然后添加到 canvas 里。

你需要把 canvas 的 frame 值( Rect)转换成 CGRect,因为 InfiniteScrollViewUIKit 的子类,只能处理 CGRectCGPoint 类型。

继续...

点击:

Build > Run

或者

CMD + R

或者

点击 Xcode 上方的 play 按钮

模拟器出现了,里面没什么东西...实际上这里是有一个 view 的,只是你看不到,背景是透明的,你也不能向下滑动,毕竟里面没有内容,自然无法滑动。

增加一些内容

为了测试你刚刚创建的 InfiniteScrollView,你需要添加一些视觉指示器。

将下面的代码复制粘贴到 WorkSpace 里:

func addVisualIndicators() {
    //指示器的最大数量
    let count = 20
    
    //每个指示器之间的间距
    let gap = 150.0
    
    //初始化指示器的偏移量,因为我们要把指示器放在居中的位置
    let dx = 40.0
    
    //计算 view contentSize 的总宽度
    let width = Double(count) * gap + dx
    
    //创建主要的指示器
    for x in 0...count {
        //给新的指示器创建中心点
        let point = Point(Double(x) * gap + dx, canvas.center.y)
        //创建 textshap
        let ts = TextShape(text: "\(x)")
        ts.center = point
        //添加到 canvas 上
        infiniteScrollView.add(ts)
    }
}

然后,调用 setup() 方法,如下:

override func setup() {
    infiniteScrollView.frame = CGRect(canvas.frame)
    canvas.add(infiniteScrollView)
    addVisualIndicators()
}

运行,现在,模拟器上出现了很多数组。

内容 + 滚动

infiniteScrolView 里还有些东西无法滚动,因为 view 实际上还不知道里面有什么内容,你必须告诉 view 你更新了一些内容,通过更新 contentSize 变量即可实现:

在循环后面加上这行代码:

infiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)

现在 view 知道它内容的最大尺寸,view 可以滚动了。使用 0 作为内容的高度,因为你不需要 view 垂直滚动。

如果你滚动到最低不,你会注意到最后一个数字有点奇怪...这是因为内容的大小只是一个大约估计的数字。

为了修复这个问题,你可以从下方两个解决方法中选一个:

  1. 记录最新的原始位置
  2. 想办法知道最后一个对象的结束位置

之后更新这个代码。

把下列删除:

nfiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)

替换成:

infiniteScrollView.contentSize = CGSizeMake(CGFloat(width+gap), 0)

从循环体中得到最后一个变量时,你计算出中心点,然后 x 方位增加一个间隙,这样你就能看到最后 20 这个数字了。

接下来,你要开始跟踪 contentOffset

不要担心,view 还是可以回弹的,你过会就会看到了...

内容的 Offset

UIScrollView 的工作方式基本上是这样的:scrollview 要显示的内容远远大于自身 frame 的大小,在审核时间,用户看到的 content view 实际上滚动视图里的范围。

当用户滑动界面时,你需要找到内容的位置。

回到 InfiniteScrollView.swift 文件,更新 layoutSubviews() ,如下:

override func layoutSubviews() {
   //调用 UIScrollView 的 layout 方法
   super.layoutSubviews()
   print(contentOffset.x)
}

UIScrollView 有一个 contentOffset 属性,所以你刚刚做的就是告诉 app ,只要调用 layoutSubviews() 方法,就打印出变量的 x 坐标。

运行应用,当你在模拟器上滑动时,你会看到 Xcode 调试视图中出现了一大堆的数字。

如果你看不到 Xcode 的调试台,敲击快捷键 CMD+SHIFT+C

或者,点击 Xcode 右上角的图片,就是下图这个图标:

现在能跟踪到 scrollview contentOffset 的 x 坐标了。

你可能会注意到,当你朝向开头滑动时,contentOffset.x 的值变成了负数,之后你会用到这个特点的,给你带来不少好处。

无限循环的规则

现在需要实现无限循环的规则了。

更新 layoutSubviews() 方法,如下:

public override func layoutSubviews() {
    super.layoutSubviews()

    //获取当前内容的偏移量(左上角)
    var curr = contentOffset
    //如果 x 的值小于零
    if curr.x < 0 {
        //更新 x 的值,复制 ScrollView 的结束位置
        curr.x = contentSize.width - frame.width
        //设置 view 的内容偏移量
        contentOffset = curr
    }
        //如果 x 值大于 width - frame width 
        //(比如,当右上角的点超出了当前的 contentSize.width)
    else if curr.x >= contentSize.width - frame.width {
        //更新 x 的值,赋值 ScrollView 的开始位置
        curr.x = 0
        //设置 view 的内容偏移量
        contentOffset = curr
    }
}

不管什么时候到了内容的边界位置,从另外一个方向,view 应该迅速跳过,这样才能有持续滚动的效果。

运行,自己看一下效果吧。

迅速重叠

还有一件事情需要注意,当 view 重叠时(例如从开头走到了结束,或者反之),里面的内容会尴尬地跳动...

解决方法就是将开头的内容连接到结束的位置,这样滚动起来就会流畅许多。

在内容的结尾增加一个开头内容的副本

然而,这和 scrollview 自己没有任何关系,依赖于你如何在结尾增加内容。

所以,回到 WorkSpace 文件。

正确的设置

增加一个新方法 createIndicator() ,用来给滚动视图创建和增加文本。

添加如下代码:

func createIndicator(text: String, at point: Point) {
    //创建一个 TextShape
    let ts = TextShape(text: text)
    //居中 TextShape
    ts.center = point
    //添加到 canvas 上
    infiniteScrollView.add(ts)
}

在这个方法里,我们得到了一个 String 类型和一个 Point 类型,创建了一个新的文本形状的指示器,定位,然后添加到滚动视图里来。

简单。

现在,把 addVisualIndicator 的内容换成下放代码:

func addVisualIndicators() {
    //设置指示器的最大数
    let count = 20
    
    //设置每个指示器之间的间隔
    let gap = 150.0
    
    //initial offset because we're positioning from the center of each indicator's view
    let dx = 40.0
    
    //初始化指示器的偏移量,因为我们要把指示器放在居中的位置
    let width = Double(count + 1) * gap + dx
    
    //创建主要的指示器
    for x in 0...count {
        //给新的指示器创建中心点
        let point = Point(Double(x) * gap + dx, canvas.center.y)
        //创建新的指示器
        createIndicator("\(x)", at: point)
    }
    
    //创建额外的指示器
    var x : Int = 0
    
    //创建偏移量
    var offset = dx
    
    // 总宽度(包括无限滚动视图里最后一个 view,基于 width + screen width)
    // 所以,总宽度,和一个有多少个额外的指示器,在一定程度上是随机的
    // 这也是为什么我们要使用 while 循环
    
    //当偏移量小于 view 的宽度时
    while offset < Double(infiniteScrollView.frame.size.width) {
        //创建中心点,x 坐标值为 width + the current offset
        let point = Point(width + offset, canvas.center.y)
        //创建宽度
        createIndicator("\(x)", at: point)
        //对下一个点增加偏移量
        offset += gap
        //增加 x 坐标值,作为下一个指示器的数值
        x += 1
    }
    
    //更新 infiniteScrollView 的 contentSize
    infiniteScrollView.contentSize = CGSizeMake(CGFloat(width) + infiniteScrollView.frame.size.width, 0)
}

现在,运行之后一切都会流畅啦。

当数字滑动到结束时,就会无缝连接继续循环。

打包

你已经将无限滚动视图添加到了 UIKit 的 UIScrollView 类里,这部分比较简单。真正有意思的是实现无限滚动的效果,这有赖于你是如何添加内容的。

你已经学会了在结尾如何增加一点额外的内容,这样让滚动的效果看起来更流畅,哄到到结尾时,视图直接能够无缝连接

大部分的工作都是在让额外的内容定位准确,设置无限滚动视图。正如这个例子中一样,如果你想纯用代码达到目的,你可能需要写一大推的代码,才能得到自己想要的效果。

代码

这里有 InfiniteScrollView 类的最终代码:

InfiniteScrollView 代码

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,551评论 4 58
  • 翻译自“Collection View Programming Guide for iOS” 0 关于iOS集合视...
    lakerszhy阅读 3,713评论 1 22
  • 用相机拍摄出来的照片含有EXIF信息,UIImage的imageOrientation属性指的就是EXIF中的or...
    柚子CHA阅读 537评论 1 1
  • 到2020年,电商在国内零售市场份额会不会超过50%?这是一个著名对赌(算起来,8年的赌约刚好过半)。不过呢,我不...
    学习项阅读 201评论 0 0
  • 观察,以跳出自己思维的方式去观察周围的一切,同时也观察自己。观察自己是更好的了解自己,而观察周围的世界,是更好的了...
    郭劼阅读 229评论 0 1