每个移动平台开发人员始终面临着无法以一种单一方式解决的任务。总有几种方法,有些是快速的,有些是复杂的,每种都有其优点和缺点。iOS中的无穷/循环滚动无法使用标准工具实现,您需要使用不同的技巧。在本文中,我将告诉您解决问题的方法是表面的,以及最终实现的方法。任务
必须使用准备好的图片,标题和副标题的形式对元素进行无休止的循环滚动。输入:中央图像从屏幕缩进16.0点。在中央图像的侧面,“耳朵”突出。横幅之间的距离为8.0点。我们研究同事所做的事情
渡渡鸟(Dodo)-标语不是周期性的,中央标语始终与左边缘相邻一定距离。Auto.ru-条幅不是周期性的,如果您滑动,条幅会翻转很长时间。臭氧-横幅是周期性的,但不能通过触摸来控制:确定了滑动方向后,就无法再停止图片了。野莓-横幅不是周期性的,会出现居中,滚动动画会长时间完成。最后的祝愿:- 标语应居中。
- 滚动应该结束,而无需长时间等待动画。
- 标语的可管理性:应该可以用手指控制滚动动画。
实施方案
当出现尚未解决的新任务时,值得考虑现有的解决方案和方法。UICollectionView
通过额头行动,您可以选择创建UICollectionView
。我们计算元素的数量,Int.max
并在初始化期间显示中间的元素,并在dataSource
-中调用方法func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell
。假设零元素是this,我们将返回相应的元素Int.max / 2
。这样的具有很多特征的怪物(例如UICollectionView
)不适合用于我们的简单任务。UIScrollView和(n + 2)UIView
还有一个创建UIScrollView的选项,绝对所有横幅都放置在上面,并由横幅添加到开头和结尾。当我们将其完成到最后时,我们会无意识地为用户更改偏移量,然后返回第一个元素。当回滚时,我们做相反的事情。结果,在具有大量元素的情况下,将创建一堆视图而不重用它们。资源自己的方式
我们决定制作UIScrollView +三个UIView。这些UIView将被重用。滚动时,我们将返回contentOffset
中心横幅并替换所有三个UIView的内容。然后,您应该获得一个轻型组件来完成此任务。然而,存在这样的担忧,即在滚动期间的内容欺骗对于用户而言将是明显的。我们在实施过程中了解到这一点。实作
准备UIScrollView和三个UIImageView
创建一个继承人UIView
,在其上放置UIScrollView
三个UIImageView
:final class BannersView: UIView {
private let scrollView = UIScrollView()
private let leftItemView = UIImageView()
private let centerItemView = UIImageView()
private let rightItemView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.scrollView)
self.setupScrollView()
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach(self.scrollView.addSubview)
}
}
通过设置添加方法的实现scrollView
:完成基本设置后,我们可以进行布局和放置ImageView
:override func layoutSubviews() {
super.layoutSubviews()
self.scrollView.frame = self.bounds
let horizontalItemOffsetFromSuperView: CGFloat = 16.0
let spaceBetweenItems: CGFloat = 8.0
let itemWidth = self.frame.width - horizontalItemOffsetFromSuperView * 2
let itemHeight: CGFloat = self.scrollView.frame.height
var startX: CGFloat = 0.0
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach { view in
view.frame.origin = CGPoint(x: startX, y: 0.0)
view.frame.size = CGSize(width: itemWidth, height: itemHeight)
startX += itemWidth + spaceBetweenItems
}
let viewsCount: CGFloat = 3.0
let contentWidth: CGFloat = itemWidth * viewsCount + spaceBetweenItems * (viewsCount - 1.0)
self.scrollView.contentSize = CGSize(width: contentWidth, height: self.frame.height)
}
添加到UIImageView
我们将从图像生成器网站https://placeholder.com提取的图像: let imageURLs = ImageURLFactory.makeImageURLS()
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
第一步准备的结果:滚动时使图像居中
为了控制滚动,我们将使用UIScrollViewDelegate
。我们setup
为方法设置了委托UIScrollView
,还设置contentInset
了第一张和最后一张图像在侧面有凹痕。self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0)
self.scrollView.delegate = self
我们extension
为我们BannersView
的方法之一创建。func scrollViewWillEndDragging
当用户停止滚动时,将调用委托方法。在此方法中,我们感兴趣targetContentOffset
-这是负责最终滚动偏移量(滚动停止的点)的变量。
extension BannersView: UIScrollViewDelegate {
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let gap: CGFloat = self.centerItemView.frame.width / 3
let targetRightOffsetX = targetContentOffset.pointee.x + self.frame.width
if (self.rightItemView.frame.minX + gap) < targetRightOffsetX {
targetContentOffset.pointee.x = self.rightItemView.frame.midX - self.frame.midX
}
else if (self.leftItemView.frame.maxX - gap) > targetContentOffset.pointee.x {
targetContentOffset.pointee.x = self.leftItemView.frame.midX - self.frame.midX
}
else {
targetContentOffset.pointee.x = self.centerItemView.frame.midX - self.frame.midX
}
}
}
gap
-这是我们假定视图处于中心位置的距离。如果在屏幕上显示橙色图像宽度的三分之一,则我们将设置最终偏移量,以使橙色图像位于中间。targetRightOffsetX
-这一点将有助于确定正确的视图是否居中。执行此方法的结果:滚动时管理偏移
现在,在滚动过程中,我们将更改contentOffset
,将中心视图返回到屏幕中心。这将使用户产生一种无人注意的无限滚动错觉。添加一个委托方法func scrollViewDidScroll(_ scrollView: UIScrollView)
,当contentOffset
y 更改时调用该方法UIScrollView
。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard
self.leftItemView.frame.width > 0,
self.centerItemView.frame.width > 0,
self.rightItemView.frame.width > 0
else {
return
}
let gap: CGFloat = self.centerItemView.frame.width / 3
let spacing: CGFloat = 8.0
let currentRightOffset: CGFloat = scrollView.contentOffset.x + self.frame.width + scrollView.contentInset.left
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
}
}
gap
-这是我们确定位移需求的距离contentOffset
。我们计算出:的点rightItemView
,self.rightItemView.frame.maxX — gap
在该点的交点之后我们将进行平移contentOffset
。例如,如果rightItemView
仍有100.0点要滚动到完整显示,则contentOffset
考虑到横幅之间的距离(间距),我们将向后移动一个横幅的宽度,以使其centerItemView
就位rightItemView
。类似地,我们这样做leftItemView
:计算点,在该点的交点之后我们将改变contentOffset
。添加一种方法func set(imageURLs: [URL])
以公开数据以在外部显示。在那里,我们从中转移了部分代码setup
。我们还将添加一行,以便在您设置内容时将其centerItemView
立即居中。horizontalItemOffsetFromSuperView
我们已经在中使用过layoutSubviews
,因此我们将其放入常量中并再次使用。func set(imageURLs: [URL]) {
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
我们将在中调用此方法UIViewController.viewDidAppear
。或者,您可以将第一个路线移至layoutSubviews
,但请确保仅在更改整个视图的框架时才能执行此操作。为了演示这项工作,我们将使用第一种方法:如此……随着剧烈的滚动,居中破裂了。事实是,在强力滚动时会忽略它targetContentOffset
。让我们增加contentInset
,之后一切正常。中心视图将始终居中。self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 300.0, bottom: 0.0, right: 300.0)
我们替换内容
任务是同时contentOffset
替换偏移量的视图内容。向右滚动时,右边的图像将成为中心,中心将变为左边,而左边将为右边。1-2-3 |2-3-1 . 为方便起见,创建一个ViewModel:struct BannersViewModel {
let items: [URL] = ImageURLFactory.makeImageURLS()
}
要检查当前位于中心的元素,请向BannersView
每个视图中添加一个变量和带有内容的变量: private var currentCenterItemIndex: Int = 0
private var viewModel: BannersViewModel?
private var leftItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let leftIndex = items.index(before: self.currentCenterItemIndex)
return leftIndex < 0 ? items.last! : items[leftIndex]
}
private var centerItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
return items[self.currentCenterItemIndex]
}
private var rightItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let rightIndex = items.index(after: self.currentCenterItemIndex)
return rightIndex >= items.count ? items.first! : items[rightIndex]
}
leftItemViewModel
,centerItemViewModel
,rightItemViewModel
-的基础上,currentCenterItemIndex
为每个视图返回相关内容。force unwrap
而fatal
在这里,我们使用它,因为元素的数量≥3(如果需要的话,你可以添加一个检查的方法set
)。添加将在必要时更改视图内容的方法: func nextItem() {
self.currentCenterItemIndex += 1
if self.viewModel?.items.count == self.currentCenterItemIndex {
self.currentCenterItemIndex = 0
}
self.updateViews()
}
func prevItem() {
self.currentCenterItemIndex -= 1
if self.currentCenterItemIndex == -1 {
self.currentCenterItemIndex = self.viewModel?.items.indices.last ?? 0
}
self.updateViews()
}
private func updateViews() {
self.leftItemView.setImage(with: self.leftItemViewModel)
self.centerItemView.setImage(with: self.centerItemViewModel)
self.rightItemView.setImage(with: self.rightItemViewModel)
}
更改外部用于公开内容的方法: func set(viewModel: BannersViewModel) {
self.viewModel = viewModel
self.updateViews()
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
然后我们将调用nextItem
,并prevItem
在方法中更改时委托contentOffset
: func scrollViewDidScroll(_ scrollView: UIScrollView) {
.......
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
self.nextItem()
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
self.prevItem()
}
}
让我们将图像的输入链接数增加到5(为方便起见,有3个):最后步骤
仍然需要定制UIView
而不是简单的图片。这将是标题,副标题和图像。扩展ViewModel
:struct BannersViewModel {
let items: [Item]
struct Item {
let title: String
let subtitle: String
let imageUrl: URL
}
}
并编写一个横幅实现:extension BannersView {
final class ItemView: UIView {
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.imageView)
self.addSubview(self.titleLabel)
self.addSubview(self.subtitleLabel)
self.imageView.contentMode = .scaleAspectFill
self.layer.masksToBounds = true
self.layer.cornerRadius = 8.0
}
func set(viewModel: BannersViewModel.Item) {
self.titleLabel.text = viewModel.title
self.subtitleLabel.text = viewModel.subtitle
self.imageView.setImage(with: viewModel.imageUrl)
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView.frame = self.bounds
self.titleLabel.frame.origin = CGPoint(x: 16.0, y: 16.0)
self.titleLabel.frame.size = CGSize(width: self.bounds.width - 32.0, height: 20.0)
self.subtitleLabel.frame.origin = CGPoint(x: 16.0, y: self.titleLabel.frame.maxY + 4.0)
self.subtitleLabel.frame.size = self.titleLabel.frame.size
}
}
}
替换UIImageView
并ViewModel
在BannersView
::
.......
private let leftItemView = ItemView()
private let centerItemView = ItemView()
private let rightItemView = ItemView()
private var leftItemViewModel: BannersViewModel.Item { ... }
private var centerItemViewModel: BannersViewModel.Item { ... }
private var rightItemViewModel: BannersViewModel.Item { ... }
.......
private func updateViews() {
self.leftItemView.set(viewModel: self.leftItemViewModel)
self.centerItemView.set(viewModel: self.centerItemViewModel)
self.rightItemView.set(viewModel: self.rightItemViewModel)
}
.......
结果:发现
制作带有横幅的无尽循环滚动是一项有趣的任务。我相信每个人都可以得出自己的结论或从我们的决策中得出任何想法,只有三个可重复使用的想法UIView
。我们再次确信,首先想到的解决方案和您在Internet上可以找到的解决方案并非总是最佳的。起初,我们担心在滚动过程中替换内容会导致问题,但是一切都进行得很顺利。如果您认为这是对的,请不要害怕尝试您的方法。想一想:)。