Arm1.ru

Анимация касания индикатора в UISlider

Анимация касания индикатора в UISlider

Стояла задача - при касании слайдера анимировать в приложении сделать анимированное увеличение ползунка. Так же, как это сделано у Apple в приложениях Apple Music и Podcasts в плеере, когда начинаешь перематывать позицию воспроизведения. Пока искал способ стандартными средствами, убил немало времени. Очень не хотелось писать прям свой кастомный слайдер, хотелось использовать системный UISlider, что мне, в итоге, и удалось.

UISlider наследуется от UIControl. У него есть возможность поменять картинку позиции на свою для конкретного состояния:

open func setThumbImage(_ image: UIImage?, for state: UIControlState) 

Состояние .normal - обычное состояние слайдера, состояние .highlighted - когда мы касается слайдера.

Просто задав двум разным состояниям разные картинки, мы получим увеличение ползунка. Вот только будет оно совсем не анимированное. Анимировать переход состояния UIControl из одного состояния можно, но происходит замена картинки, а не изменение её размера. Я пробовал различные переходы вроде CATransition, но лучшее, чего я смог добиться для анимации изменения состояния, это чтобы большая картинка для .highlighted состояния плавно появлялась с эффектом Fade In, а мне нужно было плавное увеличение ползунка.

От того, чтобы задать 2 разные картинки для разных состояний слайдера пришлось отказаться. В итоге в голове созрел такой забавный хак.

Для удобства добавим в проект функцию, которая создаёт круг нужного цвета и размера и возвращает его как UIImage:

extension UIImage {
    class func circle(diameter: CGFloat, color: UIColor) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0)
        let ctx = UIGraphicsGetCurrentContext()
        ctx!.saveGState()

        let rect = CGRect(x: 0, y: 0, width: diameter, height: diameter)
        ctx!.setFillColor(color.cgColor)
        ctx!.fillEllipse(in: rect)
        ctx!.restoreGState()

        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return img!
    }
} 

Добавляем стандартный UISlider:

Анимация касания индикатора в UISlider

Следующий шаг - делаем свой класс, который наследуется от UISlider, и устанавливаем в awakeFromNib() картинку:

class Slider: UISlider {
    override func awakeFromNib() {
        super.awakeFromNib()

        let positionImage = UIImage.circle(diameter: 30, color: UIColor.blue)
        self.setThumbImage(positionImage, for: .normal)
    }
} 

Выставляем в Interface Builder у нашего слайдера поле Class - имя нашего сабкласса (Slider). Получаем слайдер с индикатором позиции нужного цвета:

Анимация касания индикатора в UISlider

Теперь нужно добавить в наш слайдер картинку увеличенного слайдера, которая будет при касании. Делаем это через простое добавление UIImageView:

class Slider: UISlider {
    /// Big position image view
    var bigImage = UIImageView()

    override func awakeFromNib() {
        super.awakeFromNib()

        let positionImage = UIImage.circle(diameter: 30, color: UIColor.blue)
        self.setThumbImage(positionImage, for: .normal)

        let positionImageBig = UIImage.circle(diameter: 60, color: UIColor.blue)
        self.bigImage.contentMode = .scaleAspectFit
        self.bigImage.clipsToBounds = false
        self.bigImage.image = positionImageBig

        self.addSubview(bigImage)
        self.bringSubview(toFront: bigImage)
    }
} 

Далее, нужно использовать метод UISlider, который считает размеры для изображения индикатора позиции:

open func thumbRect(forBounds: CGRect, trackRect: CGRect, value: Float)

Суть использования заключается в том, чтобы в этом методе получить и вернуть размеры для ползунка, не вмешиваясь (или, если хочется, вмешиваясь) в то, какого размера он будет в неактивном состоянии. После этого, мы возьмём UIImageView с большим ползунком, и скажем ему, чтобы он был такого же размера и на той же самой позиции. Этот метод вызывается постоянно, когда мы перетаскиваем наш UISlider, поэтому большой индикатор всегда будет на том же самом месте, что и стандартный, и, пока что, того же самого размера:

class Slider: UISlider {
    // ... previous code

    /// Small indicator counted size
    var indicatorSize: CGSize? = nil

    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
        let unadjustedThumbrect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)

        let origin = unadjustedThumbrect.origin
        let size = unadjustedThumbrect.size

        if self.indicatorSize == nil && unadjustedThumbrect.size.width > 0 {
            self.bigImage.frame = unadjustedThumbrect
            self.indicatorSize = size
        }

        let bigImageSize = self.bigImage.frame.size

        self.bigImage.frame.origin = CGPoint(
            x: origin.x - (bigImageSize.width/2 - size.width/2),
            y: origin.y - (bigImageSize.height/2 - size.height/2)
        )

        self.bringSubview(toFront: bigImage)

        return unadjustedThumbrect
    }
} 

Визуально разницы никакой не видно, но у нас вместе со встроенным ползунком всегда есть картинка для увеличенного состояния на том же самом месте, которая перемещается вместе с ним.

Ну и последнее - когда пользователь касается слайдера, просто анимировать увеличение нашего bigImage. И уменьшение, соответственно, тоже, когда касание заканчивается. Для этого используем встроенный isHighlighted у UISlider, и анимируем, когда он меняет значение:

class Slider: UISlider {

    override var isHighlighted: Bool {
        didSet {
            // avoid situation when indicator size didn't count yet
            guard self.indicatorSize != nil else { return }

            UIView.animate(withDuration: 0.3) {
                if self.isHighlighted == true {
                    self.bigImage.transform = CGAffineTransform(scaleX: 2, y: 2)
                } else {
                    self.bigImage.transform = CGAffineTransform(scaleX: 1, y: 1)
                }
            }
        }
}

    // ..other code
} 

Готово. Индикатор позиции в слайдере анимируется при взаимодействии с ним:

Полный код можно посмотреть на GitHub: https://github.com/makoni/CustomUISlider

keyboard_return back