В настоящее время я пытаюсь создать настраиваемое меню панели вкладок, используя CollectionView для меню заголовка и ScrollView для экранов. Моя проблема здесь в том, что я хочу, чтобы индикатор двигался, когда я прокручиваю ScrollView, останавливаюсь на правой позиции вкладки и меняю размер в соответствии с размером вкладки, как в приложении YouTube на изображении ниже. Как я могу этого добиться?

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

0
minh quy nguyen 4 Ноя 2020 в 06:31

1 ответ

Лучший ответ

Есть несколько способов сделать это. Мне удалось добиться этого с помощью следующего кода:

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 для обновления положения индикатора в зависимости от прокрутки. Прогресс отправляется в представление панели вкладок в виде плавающего указателя на отображаемом экране.

Вид панели вкладок также находится в той же раскадровке. Я добавил такое же количество просмотров на панель вкладок, сколько экранов в режиме прокрутки. (В моем случае я просто использовал текстовые метки, которые имеют разный размер в зависимости от их текста). 3 вида (метки) подключены к myTabPanels в том же порядке, что и экраны в режиме прокрутки.

Рядом с этим я добавил нормальный UIView, представляющий мой индикатор. Я использовал ограничения, как описано в комментариях; Индикатор в центре X соответствует опережению своего обзора. Индикатор имеет фиксированную ширину. И я соединил эти два ограничения с 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. Это означает, что мы на 25% прошли экран с индексом 3.

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))
0
Matic Oblak 4 Ноя 2020 в 14:12