ScrollViewDidScroll偏移问题:indicator超出indicatorBackground可视区域
解决方案
问题出在scrollViewDidScroll方法中对indicator偏移量的计算逻辑——原来的计算基于view.frame.width的一半,没有考虑indicatorBackground添加左右内边距后宽度缩小的情况,导致indicator的移动范围超出了背景视图的边界。
修改scrollViewDidScroll方法,改为基于滚动进度和视图实际宽度的计算方式:
override func scrollViewDidScroll(_ scrollView: UIScrollView) { // 计算滚动进度:0(第一个页面)到1(第二个页面) let progress = scrollView.contentOffset.x / view.frame.width // 计算indicator可移动的最大距离:背景视图宽度减去indicator自身宽度 let maxOffset = indicatorBackground.bounds.width - indicator.bounds.width // 根据进度计算当前需要移动的偏移量 let currentOffset = progress * maxOffset indicator.transform = CGAffineTransform(translationX: currentOffset, y: 0) }
逻辑说明:
progress:表示当前滚动的相对进度,从第一个页面到第二个页面时,值从0平滑过渡到1。maxOffset:是indicator在背景视图内可以移动的最大水平距离,确保indicator移动到最右端时,不会超出背景视图的边界。currentOffset:将滚动进度映射到indicator的可移动范围内,实现平滑且边界正确的跟随效果。
修改后的完整代码
class CollectionPager: UICollectionViewController, UICollectionViewDelegateFlowLayout, CollectionDelegate { func didTapHeaderCell(index: Int) { collectionView.isPagingEnabled = false collectionView.scrollToItem( at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true ) collectionView.isPagingEnabled = true } let indicator: UIView = { let v = UIView() v.backgroundColor = .white v.layer.cornerRadius = 26 return v }() let indicatorBackground: UIView = { let v = UIView() v.backgroundColor = .secondarySystemGroupedBackground v.layer.cornerRadius = 30 return v }() fileprivate let collectionHeader = CollectionHeader(collectionViewLayout: UICollectionViewFlowLayout()) override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemGroupedBackground collectionHeader.delegate = self collectionHeader.collectionView.selectItem(at: [0,0], animated: true, scrollPosition: .centeredHorizontally) if let layout = collectionViewLayout as? UICollectionViewFlowLayout { layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 } setupLayout() collectionView.delegate = self collectionView.register(PagerCell.self, forCellWithReuseIdentifier: "PagerCell") collectionView.isPagingEnabled = true collectionView.showsHorizontalScrollIndicator = false collectionView.allowsSelection = true } fileprivate func setupLayout(){ view.addSubview(indicatorBackground) view.addSubview(indicator) view.addSubview(collectionHeader.view) indicatorBackground.anchor( top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: collectionView.topAnchor, trailing: view.trailingAnchor, padding: .init(top: 24, left: 16, bottom: 4, right: 16), size: .init(width: 0, height: 60) ) indicator.anchor( top: indicatorBackground.topAnchor, leading: indicatorBackground.leadingAnchor, bottom: indicatorBackground.bottomAnchor, trailing: nil, padding: .init(top: 4, left: 4, bottom: 4, right: 4) ) indicator.widthAnchor.constraint(equalTo: indicatorBackground.widthAnchor, multiplier: 0.5).isActive = true collectionHeader.view.anchor( top: indicatorBackground.topAnchor, leading: indicatorBackground.leadingAnchor, bottom: indicatorBackground.bottomAnchor, trailing: indicatorBackground.trailingAnchor, padding: .init(top: 0, left: 0, bottom: 0, right: 0), size: .init(width: 0, height: 60) ) collectionView.anchor( top: indicatorBackground.bottomAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, padding: .init(top: 0, left: 0, bottom: 0, right: 0) ) } override func scrollViewDidScroll(_ scrollView: UIScrollView) { // 计算滚动进度:0(第一个页面)到1(第二个页面) let progress = scrollView.contentOffset.x / view.frame.width // 计算indicator可移动的最大距离:背景视图宽度减去indicator自身宽度 let maxOffset = indicatorBackground.bounds.width - indicator.bounds.width // 根据进度计算当前需要移动的偏移量 let currentOffset = progress * maxOffset indicator.transform = CGAffineTransform(translationX: currentOffset, y: 0) } override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let x = targetContentOffset.pointee.x print(x) let item = x / view.frame.width let indexPath = IndexPath(item: Int(item), section: 0) collectionHeader.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) collectionHeader.selectedIndex = indexPath.item } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 2 } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PagerCell", for: indexPath) as! PagerCell if indexPath.item == 0 { cell.label.text = "Today" cell.backgroundColor = .red }else if indexPath.item == 1 { cell.label.text = "News" cell.backgroundColor = .yellow } return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let height = view.frame.height let width = view.frame.width return .init(width: width, height: height) } } class PagerCell: UICollectionViewCell { let label: UILabel = { let l = UILabel() l.textAlignment = .center return l }() override init(frame: CGRect) { super.init(frame: frame) addSubview(label) label.fillSuperview() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } protocol CollectionDelegate { func didTapHeaderCell(index: Int) } class CollectionHeader: UICollectionViewController, UICollectionViewDelegateFlowLayout { fileprivate let titles = ["Today", "News"] var delegate: CollectionDelegate? let menuBar: UIView = { let v = UIView() v.backgroundColor = .systemBlue v.layer.cornerRadius = 30 return v }() var selectedIndex: Int = 0 { didSet { collectionView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() collectionView.backgroundColor = .clear collectionView.layer.cornerRadius = 30 collectionView.register(HeaderCell.self, forCellWithReuseIdentifier: "HeaderCell") if let layout = collectionViewLayout as? UICollectionViewFlowLayout { layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 } selectedIndex = 0 } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { delegate?.didTapHeaderCell(index: indexPath.item) selectedIndex = indexPath.item } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return titles.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HeaderCell", for: indexPath) as! HeaderCell cell.label.text = titles[indexPath.item] cell.label.textColor = indexPath.item == selectedIndex ? .black : .lightGray return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = view.frame.width return .init(width: width / 2, height: view.frame.height) } } class HeaderCell: UICollectionViewCell { let label: UILabel = { let l = UILabel() l.textAlignment = .center l.textColor = .lightGray l.font = .systemFont(ofSize: 20, weight: .bold) return l }() override var isSelected: Bool { didSet { label.textColor = isSelected ? .black : .lightGray } } override init(frame: CGRect) { super.init(frame: frame) addSubview(label) label.fillSuperview() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
内容的提问来源于stack exchange,提问作者GILL
相关产品推荐
相关产品推荐

