Анимируйте шестиугольник UIBezierPath, например UIActivityIndicatorview

Я пытаюсь добиться такой же анимации, как показано ниже.

введите описание изображения здесь.

и мой результат с использованием UIBezierPath и CABasicAnimation приведен ниже.

введите описание изображения здесь

Вот мой LoaderView код

class LoaderView: UIView {

private let lineWidth : CGFloat = 5
internal var backgroundMask = CAShapeLayer()


override init(frame: CGRect) {
    super.init(frame: frame)
    setUpLayers()
    createAnimation()
}


required init?(coder: NSCoder) {
    super.init(coder: coder)
    setUpLayers()
    createAnimation()
}

func setUpLayers()
{
    backgroundMask.lineWidth = lineWidth
    backgroundMask.fillColor = nil
    backgroundMask.strokeColor = UIColor.blue.cgColor
    layer.mask = backgroundMask
    layer.addSublayer(backgroundMask)
}

func createAnimation()
{
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.fromValue = 0
    animation.duration = 1
    animation.repeatCount = .infinity
    backgroundMask.add(animation, forKey: "MyAnimation")
}

override func draw(_ rect: CGRect) {
    let sides = 6
    let rect = self.bounds
    let path = UIBezierPath()
    
    let cornerRadius : CGFloat = 10
    let rotationOffset = CGFloat(.pi / 2.0)
    
    let theta: CGFloat = CGFloat(2.0 * .pi) / CGFloat(sides) // How much to turn at every corner
    let width = min(rect.size.width, rect.size.height)        // Width of the square
    
    let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
    
    // Radius of the circle that encircles the polygon
    // Notice that the radius is adjusted for the corners, that way the largest outer
    // dimension of the resulting shape is always exactly the width - linewidth
    let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
    
    
    // Start drawing at a point, which by default is at the right hand edge
    // but can be offset
    var angle = CGFloat(rotationOffset)
    
    let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
    path.move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
    
    for _ in 0..<sides {
        angle += theta
        
        let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
        let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
        let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
        let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
        
        path.addLine(to: start)
        path.addQuadCurve(to: end, controlPoint: tip)
        
    }
    path.close()
    backgroundMask.path = path.cgPath
}}

person govind kumar    schedule 03.06.2021    source источник
comment
Почему голоса закрывают этот вопрос? В этом вопросе четко сформулирована цель и представлен текущий код OP, а также описание того, как он не отвечает их потребностям.   -  person Duncan C    schedule 04.06.2021
comment
Кстати, CGMutablePath имеет очень классную функцию addArc(tangent1End:tangent2End:radius:), которая упрощает добавление закругленных углов под произвольными углами. Использование этого значительно упростило бы ваш код, строящий ваш шестиугольник. Единственный триггер, который вам понадобится, это триггер для вычисления ваших вершин. См. Мой проект github.com/DuncanMC/RoundedCornerPolygon для универсального способа создания произвольных многоугольников с смесь закругленных и нескругленных углов.   -  person Duncan C    schedule 04.06.2021


Ответы (3)


Вам нужно либо реализовать draw(_:), либо использовать CAAnimation, но не то и другое одновременно.

Как правило, не реализуйте draw(_:) для классов представлений. Это заставляет систему выполнять всю рендеринг на ЦП и не использует преимущества аппаратного ускорения рендеринга на основе плиток на устройствах iOS. Вместо этого используйте CALayers и CAAnimation, и пусть оборудование сделает за вас тяжелую работу.

Используя CALayers и CAAnimation, вы можете получить такой эффект:

Анимация шестиугольника

Я бы посоветовал сделать следующее:

  • Создайте полный круг шестиугольника как CAShapeLayer. (Код в вашем draw() методе уже генерирует путь шестиугольника. Вы можете легко адаптировать его, чтобы установить свой путь шестиугольника в CAShapeLayer.)

  • Добавьте этот слой-фигуру в качестве подслоя вида.

  • Создайте конус CAGradientLayer с начальной точкой центра слоя и конечной точкой верхнего центра.

  • Добавьте к слою градиента цвета в диапазоне от прозрачных до любых непрозрачных, используя массив locations, который растушевывает градиент по желанию.

  • установите градиентный слой в качестве маски на слой с шестиугольником.

  • Создайте CABasicAnimation, который поворачивает слой градиента вокруг оси Z на 1/4 оборота за раз. Запускайте эту анимацию постоянно, пока не закончите с анимацией.

Код для создания слоя градиента может выглядеть так:

    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = self.bounds
    gradientLayer.type = .conic
    gradientLayer.colors = [UIColor.clear.cgColor,
                            UIColor.clear.cgColor,
                            UIColor.white.cgColor,
                            UIColor.white.cgColor]
    let center = CGPoint(x: 0.5, y: 0.5)
    gradientLayer.locations = [0, 0.3, 0.7, 0.9]
    gradientLayer.startPoint = center
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)

(Вам нужно будет обновить границы слоя градиента, если границы представления-владельца изменятся.)

Код для поворота слоя градиента может выглядеть так:

private func animateGradientRotationStep() {
    let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
    animationStepsRemaining -= 1
    rotation.fromValue =  rotationAngle
    rotationAngle += CGFloat.pi / 2
    rotation.toValue =  rotationAngle
    rotation.duration = 0.5
    rotation.delegate = self
    gradientLayer.add(rotation, forKey: nil)

    // After a tiny delay, set the layer's transform to the state at the end of the animation
    // so it doesnt jump back once the animation is complete.
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {

        // You have to wrap this step in a CATransaction with setDisableActions(true)
        // So you don't get an implicit animation
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        self.gradientLayer.transform = CATransform3DMakeRotation(self.rotationAngle, 0, 0, 1)
        CATransaction.commit()
    }
}

И вам нужно, чтобы ваше представление соответствовало протоколу CAAnimationDelegate:

extension GradientLayerView: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation,
                          finished flag: Bool) {
        if animating && animationStepsRemaining > 0 {
            animateGradientRotation()
        }
    }
}

Обратите внимание, что свойство преобразования слоя неявно анимируется, что означает, что по умолчанию система генерирует анимацию изменения. Мы можем воспользоваться этим фактом и просто внести некоторые изменения в неявную анимацию. Это упрощает функцию анимации:

// This version of the function takes advantage of the fact
// that a layer's transform property is implicitly animated
private func animateGradientRotationStep() {
    animationStepsRemaining -= 1
    rotationAngle += CGFloat.pi / 2
    // MARK: - CATransaction begin
    // Use a CATransaction to set the animation duration, timing function, and completion block
    CATransaction.begin()
    CATransaction.setAnimationDuration(0.5)
    CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear))
    CATransaction.setCompletionBlock {
        self.animationDidStop(finished:true)
    }
    self.gradientLayer.transform = CATransform3DMakeRotation(self.rotationAngle, 0, 0, 1)
    CATransaction.commit()
    // MARK: CATransaction end -
}

Эта версия требует немного другой функции завершения, поскольку она не использует CAAnimation:

func animationDidStop(finished flag: Bool) {
    delegate?.animationStepComplete(animationStepsRemaining)
    if animating && animationStepsRemaining > 0 {
        animateGradientRotationStep()
    }

Я создал небольшой пример приложения, которое создает такую ​​анимацию.

Вы можете загрузить демонстрационное приложение с Github по по этой ссылке .

Одна часть вашего примера анимации, которую я не знаю, как скопировать, - это тот факт, что цвет шестиугольника вначале кажется ярко-белым, а затем переходит в желтый. В моем примере приложения создается анимация, в которой шестиугольник имеет фиксированный цвет и переходит от непрозрачного к прозрачному.

Вот README из проекта:


ПолярныйГрадиентMaskView

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

Он использует CAGradientLayer типа .conic, настроенный как в основном непрозрачный, причем последняя половина становится прозрачной. Он устанавливает слой градиента как маску на слой-фигуру, содержащий желтый шестиугольник.

Слой гадиента выглядит так:

введите описание изображения здесь

(Отображается синим цветом на сером фоне шахматной доски, чтобы вы могли видеть переход от непрозрачного к прозрачному.)

Непрозрачные (синие) части градиента делают видимым слой-фигуру. Прозрачные части градиента скрывают (маскируют) эти части слоя-фигуры, а частично прозрачные части градиентного слоя делают эти части слоя-фигуры частично прозрачными.

Анимация просто поворачивает слой градиента по оси Z вокруг центра слоя. Он поворачивает слой на 1/4 оборота за раз, и каждый раз, когда этап анимации завершается, он просто создает новую анимацию, которая поворачивает маску еще на 1/4 оборота.

Немного сложно понять, что происходит, когда вы маскируете форму шестиугольника. Я создал вариант, в котором я добавил представление изображения как часть пользовательского представления. Анимация для этого выглядит так:

введите описание изображения здесь

Окно приложения выглядит так:

введите описание изображения здесь

person Duncan C    schedule 03.06.2021
comment
Не OP, но это потрясающе. Не могли бы вы также связать образец приложения? - person aheze; 03.06.2021
comment
@aheze Я только что отредактировал свой ответ, включив ссылку на репо. - person Duncan C; 04.06.2021
comment
@DuncanC Я новичок в использовании и понимании анимации. Я пробовал реализовать ваш код с утра, для меня это ракетостроение. Буду очень признателен, если вы поделитесь ссылкой на приведенный выше код GIF. Спасибо еще раз - person govind kumar; 04.06.2021
comment
@govindkumar см. последнее изменение моего ответа. В нем говорится, что вы можете скачать демонстрационное приложение с Github по этой ссылке. Последняя часть кликабельна и переносит вас в репозиторий на Github: github.com/DuncanMC/PolarGradientMaskView .git - person Duncan C; 04.06.2021
comment
@DuncanC попытался запустить ваш код и включил кнопку «Повторить анимацию» и «Анимировать сейчас», но я не вижу желтого загрузчика, попытался немного подправить код, но не смог увидеть анимацию желтого загрузчика. Будут ли они внесены в какие-либо изменения? - person govind kumar; 04.06.2021
comment
Я только что прошил обновление. Попробуйте вытащить и перепроверить. Я мог внести неполный набор изменений. - person Duncan C; 04.06.2021
comment
@DuncanC Работает как шарм. Ты спас мне день и неделю. Благодаря тонну. Потрясающие. - person govind kumar; 04.06.2021
comment
Этот проект включает возможность останавливать анимацию после полного круга. Я использовал это при создании анимации GIF, но вы можете вырезать эту логику для своего фактического использования. - person Duncan C; 04.06.2021
comment
Также было бы довольно легко обобщить эту анимацию для анимации развернутой анимации круговой маски на ЛЮБОМ виде, а не только на вашем шестиугольнике. Вы можете использовать круг или даже UIImage, какой-нибудь текст или что угодно. - person Duncan C; 04.06.2021

Видео ОП показывает анимацию, в которой шестиугольник имеет белое выделение в начале, которое переходит в желтый.

Я создал вариант своей предыдущей анимации, в котором передняя кромка анимации была выделена белым цветом. Это выглядит так:

введите описание изображения здесь

Обе версии анимации очень похожи.

Другая, вся желтая анимация, использует один слой формы с коническим градиентом CAGradientLayer, установленным в качестве слоя маски, который заставляет шестиугольник затухать на последней трети или около того. Анимация просто вращает слой маски вокруг его центра.

Градиент основной маски выглядит так:

Конический градиент

(Он нарисован синим цветом на клетчатом фоне, чтобы вам было легче видеть непрозрачные и прозрачные части.

Этот вариант анимации добавляет второй слой-фигуру поверх первого. Назовем его слоем с формой выделения. Слой формы выделения содержит шестиугольник с немного меньшей шириной линии, нарисованный белым. Слой формы выделения также имеет коническую форму CAGradientLayer в качестве маски, но слой маски маскирует все, кроме начала формы шестиугольника. Он показывает только небольшую часть белого шестиугольника и никогда не при полной непрозрачности.

Поскольку слой формы выделения не является полностью непрозрачным, два слоя формы сливаются вместе, и части слоя выделения с более высокой непрозрачностью делают пиксели в объединенном изображении более белыми.

Маска градиента для слоя с формой выделения выглядит следующим образом (снова показана синим цветом на клетчатом фоне, чтобы вы могли определить непрозрачность маски).

введите описание изображения здесь

Эта версия проекта также находится на github по адресу https://github.com/DuncanMC/PolarGradientMaskView.git < / а>

но в ветке с именем AddHighlightLayer.

Код настройки для highlightGradientLayer следующий:

    highlightGradientLayer.type = .conic
    highlightGradientLayer.colors = [UIColor.clear.cgColor,
                                     UIColor.clear.cgColor,
                                     UIColor(red: 0, green: 0, blue: 1, alpha: 0.5).cgColor,
                                     UIColor(red: 0, green: 0, blue: 1, alpha: 0.9).cgColor,
                            ]
    highlightGradientLayer.locations = [0.00, 0.85, 0.90, 1.00]
    highlightGradientLayer.startPoint = center
    highlightGradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
    self.layer.addSublayer(highlightShapeLayer)
    highlightShapeLayer.mask = highlightGradientLayer
person Duncan C    schedule 05.06.2021
comment
@govindkumar вы видели второй ответ? Я думаю, что во второй версии эффект немного ближе. (Белые блики на желтом шестиугольнике.) - person Duncan C; 10.06.2021

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

Идея для загрузчиков - начальная точка должна меняться после каждой анимации - что-то вроде -

  1. Начать под углом 0
  2. Поднимитесь на угол 45 /// или 60, как хотите.
  3. Измените начальный угол до следующего логического шага - скажем, 30
  4. Затем выполните рендеринг до 75 /// или 90 в зависимости от того, что вы выбрали ранее.

В этом расположении вы должны продолжать рисовать определенную часть формы, непрерывно меняя начальную точку.

На практике достижение плавного перехода между разными значениями начальной точки может оказаться трудным, чем кажется. Вы можете найти пример здесь - https://github.com/SVProgressHUD/SVProgressHUD/blob/master/SVProgressHUD/SVIndefiniteAnimatedView.m#L48-L102

ОБНОВЛЕНИЕ


В ссылке, которой я поделился выше, есть все намеки. Эта библиотека использует изображение в качестве маски, а затем непрерывно вращает его. Форма маски может быть любой - у вас уже есть код для этого.

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

и ПОСЛЕ маскирования того, как выглядит их анимация -  введите описание изображения здесь


person Tarun Tyagi    schedule 03.06.2021
comment
Я попытался сделать то же самое, это частично сработало, но мне не удалось исправить или удалить эффект мерцания, который возникает, когда контур заканчивает вращение. - person govind kumar; 03.06.2021
comment
См. ОБНОВЛЕНИЕ моего ответа. - person Tarun Tyagi; 03.06.2021
comment
Нет необходимости использовать изображение в качестве маски. Вы можете использовать конический CAGradientLayer в качестве маски и просто применить к этому слою анимацию вращения. Слой градиента масштабируется до любого необходимого размера. - person Duncan C; 03.06.2021
comment
См. Мой ответ на рабочий пример, который использует конический CAGradientLayer в качестве маски и анимирует вращение этого слоя. - person Duncan C; 04.06.2021