使用CollectionView制作自定义标签栏

时间:2020-11-04 03:31:10

标签: ios swift

我目前正在尝试使用自定义的选项卡菜单,使用CollectionView作为标题菜单,并使用ScrollView作为屏幕。 我的问题是,我希望指示器在滚动ScrollView时移动,停在正确的标签位置并根据标签的大小进行调整,例如下图所示的youtube应用中的一个。我该如何实现?

https://i.stack.imgur.com/l8HoA.jpg

1 个答案:

答案 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的前导。指示器具有固定的宽度。然后,我将两个约束与indicatorCenterXConstraintindicatorWidthConstraint连接起来。 (我还设置了垂直约束,固定高度和一些背景色。)

现在,当我滚动时,将计算新进度并将其传递到选项卡栏视图。调用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.0upperBound之间的进度。这些基本上是最小和最大索引。

let actualProgress = max(0.0, min(progress, upperBound))

开始索引和进度视图将实例值3.25分解为30.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的屏幕的视图位置是什么。这些位置存储在leftright下。

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))