Анимация шаблонного UIView для имитации индикатора выполнения

Я пытаюсь создать анимацию индикатора выполнения в Swift. Я разработал шаблонное изображение для имитации прогресса, но не могу напрямую применить эту анимацию к UIProgressView. Вместо этого я поместил UIView сверху, чтобы имитировать это поведение.

Я сделал симметричную картинку: зеленый фон, темно-зеленые горизонтальные линии.

Как я понял, поскольку я не могу прикрепить его к UIProgressBar, я добавил UIView для имитации ProgressBar.

Идея состоит в том, чтобы иметь что-то вроде этого:

Вот моя текущая настройка:

import UIKit

class SideMenuButtons: UIViewController {
    var progressOverlayView: UIView!
    @IBOutlet weak var energyReloadingIndicator: UIProgressView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        progressOverlayView = UIView()
        if let patternImage = UIImage(named: "striped-pattern-repeated") {
            progressOverlayView.backgroundColor = UIColor(patternImage: patternImage)
        } else {
            progressOverlayView.backgroundColor = .red
        }
        energyReloadingIndicator.addSubview(progressOverlayView)
        updateProgressOverlayViewWidth()
    }
    
    private func updateProgressOverlayViewWidth() {
        let progress = CGFloat(energyReloadingIndicator.progress)
        let maxWidth = energyReloadingIndicator.frame.width
        let overlayWidth = maxWidth * progress
        let overlayHeight = energyReloadingIndicator.frame.height
        progressOverlayView.frame = CGRect(x: 0, y: 0, width: overlayWidth, height: overlayHeight)
    }
}

Как я могу анимировать шаблон в ProgressOverlayView, чтобы добиться эффекта непрерывного движения?

Одним из решений может быть необходимость сделать 3 изображения индикатора выполнения, например, непрерывно. Нужно взять таймер 0,2 секунды и менять изображение после каждого интервала. Если поместить его в блок UIViewAnimation, вы достигнете желаемого.

Shivbaba's son 15.04.2024 12:53

Вы можете скачать gif-изображение здесь icon8.com/preloaders/en/horizontal и загрузить gif-изображение в свой код.

Shivbaba's son 15.04.2024 13:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
84
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

По сути, мы можем добавить изображение gif в представление изображения и добиться анимации.

1. Нравится этот ответ GIF-изображение для добавления

let jeremyGif = UIImage.gifImageWithName("loader")
loaderImageView = UIImageView(image: jeremyGif)
loaderImageView?.contentMode = .left
loaderImageView?.clipsToBounds = true

Как вы сказали, возьмите таймер

2. timerForIncreaseProgress = Timer.scheduledTimer(timeInterval: 1, цель: self, селектор: #selector(increaseProgress), userInfo: ноль, повторы: true)

 @objc func increaseProgress() {
        progress = progress + 0.05
        progressView.progress = progress
        let maxWidth = progressView.frame.width
        let overlayWidth = Float(maxWidth) * progress
        loaderImageView?.frame = CGRect(x: 0, y: 0, width: Double(overlayWidth), height: 13.0)
        print("progressOverlayView.width \(progressOverlayView.frame.width)")
        if progress >= 1 {
            timerForIncreaseProgress?.invalidate()
            timerForIncreaseProgress = nil
        }
    }

Надеюсь, это поможет вам

Ответ принят как подходящий

Давайте рассмотрим один подход, который не требует анимированного GIF-изображения или изображения-шаблона.

Мы можем использовать UIBezierPath с CAShapeLayer для создания шаблона в подклассе пользовательского представления:

и мы можем установить цвет фона представления с помощью цвета «между строк»:

Теперь мы можем использовать CABasicAnimation(keyPath: "position.x"), чтобы «сдвинуть» этот слой влево. Мы создаем путь линий шире, чем вид, чтобы не видеть пробела справа (не обращайте внимания на «сбой» в этой анимации, он присутствует только потому, что это частичный захват):

Исходное изображение также имеет небольшой градиент, поэтому мы можем наложить полупрозрачный градиент для этого вида:

Теперь мы можем создать собственное представление «прогресса», встроив это представление анимированной фигуры в представление «контейнера» и отрегулировав ширину контейнера, чтобы отразить прогресс:

Вот пример кода...


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

class AnimatedPatternView: UIView {
    
    private let angledLinesShapeLayer = CAShapeLayer()
    private let overlayGradLayer = CAGradientLayer()
    
    // let's put all of our appearance variables here
    //  in one place, to make it easier to adjust them
    
    // this will be the amount of time (in seconds) that the
    //  animation takes to slide "one tile" to the left
    private let animSpeed: Double = 0.5
    
    // "tile" width
    private let tileWidth: CGFloat = 40.0
    
    // we want an angled line, not the full width
    private let angledLineBottomOffset: CGFloat = 32.0
    
    // thickness of the lines
    private let angledLineThickness: CGFloat = 14.0
    
    // the angled-line and "space between" colors
    private let angledLineColor: UIColor = UIColor(red: 0.557, green: 0.420, blue: 0.863, alpha: 1.0)
    private let spaceBetweenColor: UIColor = UIColor(red: 0.624, green: 0.494, blue: 0.886, alpha: 1.0)
    
    // soft overlay gradient
    private let overlayGradientColors: [CGColor] = [
        UIColor.black.withAlphaComponent(0.2).cgColor,
        UIColor.white.withAlphaComponent(0.1).cgColor,
        UIColor.black.withAlphaComponent(0.05).cgColor,
    ]
    
    // gradient angle
    private let gradStartPoint: CGPoint = .init(x: 0.250, y: 0.0)
    private let gradEndPoint: CGPoint = .init(x: 0.750, y: 1.0)

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        self.backgroundColor = spaceBetweenColor
        
        angledLinesShapeLayer.strokeColor = angledLineColor.cgColor
        
        angledLinesShapeLayer.lineWidth = angledLineThickness
        angledLinesShapeLayer.lineCap = .square

        overlayGradLayer.colors = overlayGradientColors
        overlayGradLayer.startPoint = gradStartPoint
        overlayGradLayer.endPoint = gradEndPoint
        
        self.layer.addSublayer(angledLinesShapeLayer)
        self.layer.addSublayer(overlayGradLayer)

        self.layer.masksToBounds = true
        self.clipsToBounds = true

    }
    override func layoutSubviews() {
        super.layoutSubviews()

        let bez = UIBezierPath()
        var x: CGFloat = 0.0
        while x < bounds.width + tileWidth {
            bez.move(to: .init(x: x, y: bounds.minY))
            bez.addLine(to: .init(x: x + angledLineBottomOffset, y: bounds.maxY))
            x += tileWidth
        }
        angledLinesShapeLayer.path = bez.cgPath

        // gradient layer needs to match the view bounds
        overlayGradLayer.frame = bounds
        
        angledLinesShapeLayer.removeAllAnimations()
        let animation = CABasicAnimation(keyPath: "position.x")
        animation.fromValue = 0.0
        animation.toValue = -tileWidth
        animation.duration = animSpeed
        animation.repeatCount = .infinity
        angledLinesShapeLayer.add(animation, forKey: "stripeAnim")
    }
}

EnergyProgressView: подкласс представления, который использует AnimatedPatternView и представление контейнера для имитации индикатора выполнения:

class EnergyProgressView: UIView {
    public var progress: Float {
        set { _progress = newValue }
        get { return _progress }
    }
    private var _progress: Float = 1.0 {
        didSet {
            wConstraint.isActive = false
            wConstraint = containerView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: CGFloat(_progress))
            wConstraint.isActive = true
        }
    }
    
    private let animPatternView = AnimatedPatternView()
    private let containerView = UIView()
    
    // this will control the width of the container
    //  using progress as a percentage of the width
    private var wConstraint: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        animPatternView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(animPatternView)
        self.addSubview(containerView)
        containerView.clipsToBounds = true

        // we'll be modifying the width of containerView to reflect the progress
        wConstraint = containerView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.0)

        NSLayoutConstraint.activate([

            containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
            containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
            containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),

            wConstraint,
            
            // even though animPatternView is a subview of containerView
            //  we constrain animPatternView to self so it doesn't resize
            //  when the progress changes
            animPatternView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
            animPatternView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
            animPatternView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
            animPatternView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),

        ])

        self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        self.layer.masksToBounds = true
        self.layer.cornerRadius = 8.0
    }
    
    func setProgress(_ p: Float, animated: Bool = false) {
        _progress = p
        if animated {
            UIView.animate(withDuration: 0.3, animations: {
                self.layoutIfNeeded()
            })
        }
    }

}

ViewController: пример контроллера представления с пользовательским индикатором выполнения и ползунком для обновления прогресса:

class ViewController: UIViewController {
    
    let myProgressView = EnergyProgressView()
    
    // let's add a label and a slider so we can dynamically set the progress
    let pctLabel = UILabel()
    let slider = UISlider()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        myProgressView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myProgressView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            myProgressView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            myProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            myProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            myProgressView.heightAnchor.constraint(equalToConstant: 32.0),
        ])

        pctLabel.textAlignment = .center
        
        pctLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pctLabel)
        slider.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(slider)

        NSLayoutConstraint.activate([
            pctLabel.topAnchor.constraint(equalTo: myProgressView.bottomAnchor, constant: 40.0),
            pctLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            pctLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            slider.topAnchor.constraint(equalTo: pctLabel.bottomAnchor, constant: 40.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])
        
        slider.addTarget(self, action: #selector(handleSlider(_:)), for: .valueChanged)
        
        slider.value = 0.5
        myProgressView.progress = 0.5
        updatePctLabel()
    }
    @objc func handleSlider(_ sender: UISlider) {
        myProgressView.progress = sender.value
        updatePctLabel()
    }
    func updatePctLabel() {
        let v = myProgressView.progress
        pctLabel.text = String(format: "%0.2f %%", v * 100.0)
    }
}

Обратите внимание: это только пример кода!!! Его следует использовать в качестве инструмента обучения и не следует считать «готовым к производству».

Дорогой @DonMag, если бы я мог тебя обнять или послать цветы - я бы обязательно это сделал :D Большое-большое спасибо!

Mr.Schtelbe 17.04.2024 17:24

Другие вопросы по теме