我目前正在尝试使用自定义的选项卡菜单,使用CollectionView作为标题菜单,并使用ScrollView作为屏幕。 我的问题是,我希望指示器在滚动ScrollView时移动,停在正确的标签位置并根据标签的大小进行调整,例如下图所示的youtube应用中的一个。我该如何实现?
答案 0 :(得分:0)
有几种方法可以做到这一点。我设法通过以下代码实现了它:
class MyViewController: UIViewController, UIScrollViewDelegate {
@IBOutlet private var myTabBarView: MyTabBarView?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
myTabBarView?.progress = scrollView.contentOffset.x / scrollView.bounds.width
}
}
class MyTabBarView: UIView {
@IBOutlet private var myTabPanels: [UIView] = [] // A list of your views to which the indicator should snap
@IBOutlet private var indicatorCenterXConstraint: NSLayoutConstraint? // indicatorView.centerX = superview.leading
@IBOutlet private var indicatorWidthConstraint: NSLayoutConstraint?
var progress: CGFloat = 0.0 {
didSet {
refresh()
}
}
private func refresh() {
guard myTabPanels.count > 0 else { return }
let position: (width: CGFloat, xOffset: CGFloat) = {
guard myTabPanels.count > 1 else {
return (myTabPanels[0].frame.width, myTabPanels[0].frame.midX)
}
let upperBound = CGFloat(myTabPanels.count-1)
guard progress < upperBound else {
return (myTabPanels.last!.frame.width, myTabPanels.last!.frame.midX)
}
let actualProgress = max(0.0, min(progress, upperBound))
let startIndex = Int(actualProgress) // Index of view on the left
let progressToNextView = actualProgress - CGFloat(startIndex) // Progress between view on the left and view on the right
let widths: (left: CGFloat, right: CGFloat) = (myTabPanels[startIndex].frame.width, myTabPanels[startIndex+1].frame.width)
let xOffsets: (left: CGFloat, right: CGFloat) = (myTabPanels[startIndex].frame.midX, myTabPanels[startIndex+1].frame.midX)
func interpolate(values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
values.from + (values.to - values.from)*scale
}
return (interpolate(values: (widths.left, widths.right), scale: progressToNextView), interpolate(values: (xOffsets.left, xOffsets.right), scale: progressToNextView))
}()
indicatorCenterXConstraint?.constant = position.xOffset
indicatorWidthConstraint?.constant = position.width
layoutSubviews()
}
}
在情节提要中,我添加了带有水平堆栈视图的滚动视图,以模拟您所描述的多个屏幕。滚动视图的委托设置为视图控制器,该控制器然后实现scrollViewDidScroll
来根据滚动更新指示器位置。进度将作为显示的屏幕的浮动索引发送到标签栏视图。
标签栏视图也在同一情节提要中。我在标签栏中添加了与滚动视图中的屏幕相同数量的视图。 (在我的情况下,我只是使用了文本标签,其大小取决于文本)。这三个视图(标签)以与在滚动视图中表示屏幕相同的顺序连接到myTabPanels
。
接下来,我添加了一个普通的UIView
来代表我的指标。我使用了注释中所述的约束;指标中心X等于其superview的前导。指示器具有固定的宽度。然后,我将两个约束与indicatorCenterXConstraint
和indicatorWidthConstraint
连接起来。 (我还设置了垂直约束,固定高度和一些背景色。)
现在,当我滚动时,将计算新进度并将其传递到选项卡栏视图。调用refresh
方法,该方法计算新位置并正确更新指标。一切似乎都正常。因此,要分解代码:
只有1个视图时,不应进行任何计算。指示器固定在上面
guard myTabPanels.count > 1 else {
return (myTabPanels[0].frame.width, myTabPanels[0].frame.midX)
}
当用户滚动到最后一个视图时,请保留最后一个项目。指示器不能继续前进。
let upperBound = CGFloat(myTabPanels.count-1)
guard progress < upperBound else {
return (myTabPanels.last!.frame.width, myTabPanels.last!.frame.midX)
}
仅允许0.0
和upperBound
之间的进度。这些基本上是最小和最大索引。
let actualProgress = max(0.0, min(progress, upperBound))
开始索引和进度视图将实例值3.25
分解为3
和0.25
。这意味着我们比索引3超出了屏幕25%。
let startIndex = Int(actualProgress) // Index of view on the left
let progressToNextView = actualProgress - CGFloat(startIndex) // Progress between view on the left and view on the right
如果我们位于索引3和4之间,那么我们感兴趣的是代表具有索引3和4的屏幕的视图位置是什么。这些位置存储在left
和right
下。
let widths: (left: CGFloat, right: CGFloat) = (myTabPanels[startIndex].frame.width, myTabPanels[startIndex+1].frame.width)
let xOffsets: (left: CGFloat, right: CGFloat) = (myTabPanels[startIndex].frame.midX, myTabPanels[startIndex+1].frame.midX)
标准线性插值
func interpolate(values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
values.from + (values.to - values.from)*scale
}
仅返回插值
return (interpolate(values: (widths.left, widths.right), scale: progressToNextView), interpolate(values: (xOffsets.left, xOffsets.right), scale: progressToNextView))