UICollectionView 动态数据源项添加到 collection 视图的开头
UICollectionView dynamic data source items adding to the beginning of the collection view
我正在制作具有动态内容加载功能的 collection 视图。就我而言,我正在制作日历。当前日期将在可见部分(加载后)。如果向上滚动,将显示较早的日期;如果向下滚动,则会显示下一个日期。
我的 collection 视图使用标准流布局。我有 7 个连续单元格和 4 个可见行。
我在添加较旧的日期时遇到问题(例如,向上滚动时出现的单元格)。我在做什么:首先我实现了用于检测滚动事件的滚动视图方法。
startOffsetY
是在 viewDidLoad 中设置的 contentOffset.y 值。它不等于0,因为我设置了contentInset
。 upd
只是一个标志,表示可以开始新的更新。
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y <= startOffsetY - 20 && upd {
calendar.updateDataSource()
}
}
接下来,我从数据源数组中的第一个日期开始计算上一个时间片的日期(大约 4 周,我也尝试了 1 周)。
之后,我计算 indexPath 数组并更新到 collection 视图:
var indexes = [NSIndexPath]()
for n in 1...4 {
for i in reverse(1...7) {
indexes.append(NSIndexPath(forItem: n * 7 - i, inSection: 0))
}
}
self.calendarCollectionView.performBatchUpdates({ () -> Void in
self.calendarCollectionView.insertItemsAtIndexPaths(indexes)
}, completion: { (finish) -> Void in
self.upd = true
})
但是,当添加行和滚动正在进行时,我有明显的延迟。
我尝试了不同的策略:我使用了 reloadData()
并且它非常理想(在模拟器上)并且在我的 iPhone 4S 上非常滞后(这是我的经验中第一次模拟器更快比设备)。从这一点上,我发现,插入项目的动画可能是问题所在。我试图将 performBatchUpdates
包装到 UIView.performWithoutAnimation
块中,但也没有运气。
我真的在寻求一些帮助,我并不是在寻找现成的解决方案,除非它们按照我描述的方式工作(上下滚动并加载内容)并且我可以看看它是如何工作的。再一次,滚动已加载的项目不是问题,问题是在我的数据数组的开头添加内容时出现滞后。
编辑
@teamnorge 在 Swift
中提供的代码
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
var offset : CGFloat!
println(currentOffset)
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffset < contentHeight * 0.125 {
offset = contentHeight * 0.125 - currentOffset
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset + contentHeight * 0.5 + offset - CGFloat(cellHeight))
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffset > contentHeight * 0.75 {
offset = currentOffset - contentHeight * 0.75
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset - contentHeight * 0.5 - offset + CGFloat(cellHeight))
}
效果很好,但需要使用 contentOffset
y
公式,因为现在技巧只有在向上滚动时才有效。明天将解决此问题并添加日历日期计算。
编辑 2
重新加载数据会破坏我所有原型中的所有内容。包括我之前做的那个。发现了一些可以降低延迟但仍然非常明显且完全无法接受的东西。这是这些东西:
- 从情节提要中的单元格原型中删除自动布局
将此代码添加到单元格:
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.mainScreen().scale
我认为布局无效会导致这些滞后。那么,我可能需要制作自定义流程布局?如果可以,您能给些建议吗?
这实际上是一个非常有趣的话题。我将尝试解释我在开发另一个日历应用程序时想到的想法。
在我的例子中,它不是日历视图而是日视图,小时数 00:00 - 24:00 从上到下列出,所以我有 UITableView
而不是 UICollectionView
但在这种情况下它并不那么重要。
使用的语言不是Swift而是Objective-C,所以我只是尝试翻译代码示例。
我使用固定数量的行创建 UITableView
,而不是使用数据源进行操作,在我的例子中,正好存储两天(48 行,两天,每天 24 小时)。您可以选择包含在两个全屏中的行数。
重要的是总行数必须是8的倍数。
然后您需要一个公式来根据第一个可见行中的内容计算每个特定单元格的天数。
想法是 UICollectionView
实际上是 UIScrollView
所以当我们向下或向上滚动时我们可以处理相应的事件并计算可见偏移量。
为了模拟不定式滚动,我们处理 scrollViewDidScroll
并检查您是否刚刚通过 UIScrollView
高度的 1/8
向上滚动,将您的 UIScrollView
移动到1/2
的高度加上精确的偏移量,因此它可以平滑地移动到 "second screen"。然后返回,如果您在向下滚动时超过了 UIScrollView
高度的 6/8
,请将 UIScrollView
向上移动到其高度减去偏移量的 1/2
。
执行此操作时,您会看到滚动指示器上下跳动,这非常令人困惑,因此我们必须将其隐藏,只需将其放在 viewDidLoad
:
中的某处即可
calendarView.showsHorizontalScrollIndicator = false
calendarView.showsVerticalScrollIndicator = false
其中 calendarView
是您的 UICollectionView
实例名称。
下面是代码(翻译自Objective-C就在这里,没有在实际项目中尝试):
func scrollViewDidScroll(scrollView_:UIScrollView) {
if !enableScrollingHandling {return}
var currentOffsetX:CGFloat = scrollView_.contentOffset.x
var currentOffSetY:CGFloat = scrollView_.contentOffset.y
var contentHeight:CGFloat = scrollView_.contentSize.height
var offset:CGFloat
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffSetY < (contentHeight * 0.125) {
enableScrollingHandling = false
offset = (contentHeight * 0.125) - currentOffSetY
// @todo: your code, specify which days are listed in the first row (2nd screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset +
contentHeight * 0.5 + offset - CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffSetY > (contentHeight * 0.75) {
enableScrollingHandling = false
offset = currentOffSetY - (contentHeight * 0.75)
// @todo: your code, specify which days are listed in the first row (1st screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset -
contentHeight * 0.5 - offset + CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
}
然后在您的 collectionView:cellForItemAtIndexPath:
中,根据第一行中的内容(这是第一个屏幕或第二个屏幕的内容)以及当前可见的日期(公式),您只需更新单元格内容。
这个公式也可能是一个棘手的部分,可能需要一些解决方法,特别是如果您决定在月份之间放置月份名称。我也有一些组织的想法,所以如果你遇到任何问题,我们可以在这里讨论。
但是这种用 "two screens loop and jumps in between" 模拟不定式滚动的方法真的很有魅力,滚动非常流畅,就像在旧手机上测试过的行为一样。
PS: kRowHeight 只是精确行(单元格)的高度常数,它是精确和平滑滚动行为所需要的,我认为可以跳过它。
更新:
重要通知,我从原始代码中跳过了这一点,但看到这很重要。当您手动设置 contentOffset
时,您也会触发 scrollViewDidScroll
事件,为防止这种情况,您需要暂时禁用 scrollViewDidScroll
事件处理。例如,您可以通过添加状态变量 enableScrollHandling
并更改其状态 true/false
来实现,请参阅更新的代码。
我正在制作具有动态内容加载功能的 collection 视图。就我而言,我正在制作日历。当前日期将在可见部分(加载后)。如果向上滚动,将显示较早的日期;如果向下滚动,则会显示下一个日期。
我的 collection 视图使用标准流布局。我有 7 个连续单元格和 4 个可见行。
我在添加较旧的日期时遇到问题(例如,向上滚动时出现的单元格)。我在做什么:首先我实现了用于检测滚动事件的滚动视图方法。
startOffsetY
是在 viewDidLoad 中设置的 contentOffset.y 值。它不等于0,因为我设置了contentInset
。 upd
只是一个标志,表示可以开始新的更新。
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y <= startOffsetY - 20 && upd {
calendar.updateDataSource()
}
}
接下来,我从数据源数组中的第一个日期开始计算上一个时间片的日期(大约 4 周,我也尝试了 1 周)。
之后,我计算 indexPath 数组并更新到 collection 视图:
var indexes = [NSIndexPath]()
for n in 1...4 {
for i in reverse(1...7) {
indexes.append(NSIndexPath(forItem: n * 7 - i, inSection: 0))
}
}
self.calendarCollectionView.performBatchUpdates({ () -> Void in
self.calendarCollectionView.insertItemsAtIndexPaths(indexes)
}, completion: { (finish) -> Void in
self.upd = true
})
但是,当添加行和滚动正在进行时,我有明显的延迟。
我尝试了不同的策略:我使用了 reloadData()
并且它非常理想(在模拟器上)并且在我的 iPhone 4S 上非常滞后(这是我的经验中第一次模拟器更快比设备)。从这一点上,我发现,插入项目的动画可能是问题所在。我试图将 performBatchUpdates
包装到 UIView.performWithoutAnimation
块中,但也没有运气。
我真的在寻求一些帮助,我并不是在寻找现成的解决方案,除非它们按照我描述的方式工作(上下滚动并加载内容)并且我可以看看它是如何工作的。再一次,滚动已加载的项目不是问题,问题是在我的数据数组的开头添加内容时出现滞后。
编辑
@teamnorge 在 Swift
中提供的代码func scrollViewDidScroll(scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
var offset : CGFloat!
println(currentOffset)
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffset < contentHeight * 0.125 {
offset = contentHeight * 0.125 - currentOffset
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset + contentHeight * 0.5 + offset - CGFloat(cellHeight))
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffset > contentHeight * 0.75 {
offset = currentOffset - contentHeight * 0.75
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset - contentHeight * 0.5 - offset + CGFloat(cellHeight))
}
效果很好,但需要使用 contentOffset
y
公式,因为现在技巧只有在向上滚动时才有效。明天将解决此问题并添加日历日期计算。
编辑 2
重新加载数据会破坏我所有原型中的所有内容。包括我之前做的那个。发现了一些可以降低延迟但仍然非常明显且完全无法接受的东西。这是这些东西:
- 从情节提要中的单元格原型中删除自动布局
将此代码添加到单元格:
cell.layer.shouldRasterize = true cell.layer.rasterizationScale = UIScreen.mainScreen().scale
我认为布局无效会导致这些滞后。那么,我可能需要制作自定义流程布局?如果可以,您能给些建议吗?
这实际上是一个非常有趣的话题。我将尝试解释我在开发另一个日历应用程序时想到的想法。
在我的例子中,它不是日历视图而是日视图,小时数 00:00 - 24:00 从上到下列出,所以我有 UITableView
而不是 UICollectionView
但在这种情况下它并不那么重要。
使用的语言不是Swift而是Objective-C,所以我只是尝试翻译代码示例。
我使用固定数量的行创建 UITableView
,而不是使用数据源进行操作,在我的例子中,正好存储两天(48 行,两天,每天 24 小时)。您可以选择包含在两个全屏中的行数。
重要的是总行数必须是8的倍数。
然后您需要一个公式来根据第一个可见行中的内容计算每个特定单元格的天数。
想法是 UICollectionView
实际上是 UIScrollView
所以当我们向下或向上滚动时我们可以处理相应的事件并计算可见偏移量。
为了模拟不定式滚动,我们处理 scrollViewDidScroll
并检查您是否刚刚通过 UIScrollView
高度的 1/8
向上滚动,将您的 UIScrollView
移动到1/2
的高度加上精确的偏移量,因此它可以平滑地移动到 "second screen"。然后返回,如果您在向下滚动时超过了 UIScrollView
高度的 6/8
,请将 UIScrollView
向上移动到其高度减去偏移量的 1/2
。
执行此操作时,您会看到滚动指示器上下跳动,这非常令人困惑,因此我们必须将其隐藏,只需将其放在 viewDidLoad
:
calendarView.showsHorizontalScrollIndicator = false
calendarView.showsVerticalScrollIndicator = false
其中 calendarView
是您的 UICollectionView
实例名称。
下面是代码(翻译自Objective-C就在这里,没有在实际项目中尝试):
func scrollViewDidScroll(scrollView_:UIScrollView) {
if !enableScrollingHandling {return}
var currentOffsetX:CGFloat = scrollView_.contentOffset.x
var currentOffSetY:CGFloat = scrollView_.contentOffset.y
var contentHeight:CGFloat = scrollView_.contentSize.height
var offset:CGFloat
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffSetY < (contentHeight * 0.125) {
enableScrollingHandling = false
offset = (contentHeight * 0.125) - currentOffSetY
// @todo: your code, specify which days are listed in the first row (2nd screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset +
contentHeight * 0.5 + offset - CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffSetY > (contentHeight * 0.75) {
enableScrollingHandling = false
offset = currentOffSetY - (contentHeight * 0.75)
// @todo: your code, specify which days are listed in the first row (1st screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset -
contentHeight * 0.5 - offset + CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
}
然后在您的 collectionView:cellForItemAtIndexPath:
中,根据第一行中的内容(这是第一个屏幕或第二个屏幕的内容)以及当前可见的日期(公式),您只需更新单元格内容。
这个公式也可能是一个棘手的部分,可能需要一些解决方法,特别是如果您决定在月份之间放置月份名称。我也有一些组织的想法,所以如果你遇到任何问题,我们可以在这里讨论。
但是这种用 "two screens loop and jumps in between" 模拟不定式滚动的方法真的很有魅力,滚动非常流畅,就像在旧手机上测试过的行为一样。
PS: kRowHeight 只是精确行(单元格)的高度常数,它是精确和平滑滚动行为所需要的,我认为可以跳过它。
更新:
重要通知,我从原始代码中跳过了这一点,但看到这很重要。当您手动设置 contentOffset
时,您也会触发 scrollViewDidScroll
事件,为防止这种情况,您需要暂时禁用 scrollViewDidScroll
事件处理。例如,您可以通过添加状态变量 enableScrollHandling
并更改其状态 true/false
来实现,请参阅更新的代码。