Привет, ребята, сегодня мы собираемся изучить, как мы можем реализовать классную анимацию в SwiftUI.

Это третья часть серии статей об анимации SwiftUI. Если вы еще этого не сделали, пожалуйста, прочитайте первую и вторую статьи.





Этот пост состоит из 3 анимаций:

  1. Анимация вращающейся точки — точка вращается по кругу и работает как загрузчик.
  2. Анимация трех прыгающих точек — 3 точки, которые прыгают в потоке одна за другой.
  3. Анимация часов — говорит само за себя, верно?

Окончательная реализация будет выглядеть так:

Не интересует реализация? Ознакомьтесь с полным исходным кодом этих анимаций на GitHub. Не стесняйтесь создавать ответвления или напрямую использовать необходимые анимации в своих приложениях.

Хорошо!!

Начнем с первой анимации.

1. Анимация вращающихся точек

Эта анимация имеет 2 круга, первый как контур, а второй как закрашенный круг.

Давайте сначала начнем с добавления первоначального дизайна.

struct RotatingDotAnimation: View {
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 4)
                .foregroundColor(.white.opacity(0.5))
                .frame(width: 150, height: 150, alignment: .center)

            Circle()
                .fill(.white)
                .frame(width: 18, height: 18, alignment: .center)
                .offset(x: -63)
        }
    }
}

Теперь давайте посмотрим, как мы можем анимировать эти круги.

В этой анимации мы собираемся анимировать верхний круг ZStack.

struct RotatingDotAnimation: View {
    
    @State private var startAnimation = false
    @State private var duration = 1.0 // Works as speed, since it repeats forever
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 4)
                .foregroundColor(.white.opacity(0.5))
                .frame(width: 150, height: 150, alignment: .center)

            Circle()
                .fill(.white)
                .frame(width: 18, height: 18, alignment: .center)
                .offset(x: -63)
                .rotationEffect(.degrees(startAnimation ? 360 : 0))
                .animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
                           value: startAnimation
                )
        }
        .onAppear {
            self.startAnimation.toggle()
        }
    }
}

Здесь у нас есть —

  1. Добавлено 2 свойства состояния: duration для наблюдения за скоростью анимации и startAnimation логическое значение для запуска анимации.
  2. Мы собираемся анимировать заданный маленький шарик к краю круга на протяжении всего раунда,
  • Мы использовали модификатор rotationEffect, чтобы вращать мяч на 360 градусов по кругу, когда начинается анимация.

3. Этого недостаточно для анимации, мы установили анимацию, которая делается модификатором animation для анимации нашей анимации.

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

Если вы хотите увидеть разницу, вы можете изменить тип анимации с .easeInOut на .linear или любой другой.

  • repeatForever — используется для непрерывной анимации, а аргумент autoreverses предназначен для отключения обратной анимации, когда она заканчивается один раз.
  • value — Аргумент для отслеживания изменений в анимации.

Вот и все!

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

2. Анимация трех прыгающих точек

Эта анимация имеет 3 точки.

Давайте сначала добавим DotView, который будет отвечать за отрисовку одной точки.

private struct DotView: View {
    
    @Binding var scale: CGFloat

    var body: some View {
        Circle()
            .scale(scale)
            .fill(.white.opacity(scale >= 0.7 ? scale : scale - 0.1))
            .frame(width: 50, height: 50, alignment: .center)
    }
}

Здесь мы добавили scale в качестве привязки, так как собираемся изменить это свойство, чтобы анимировать внешний вид точки.

Теперь давайте посмотрим, как мы можем анимировать эти точки!

Если вы наблюдаете анимацию, мы должны добавить свойство, которое будет разным для каждой точки — Delay.

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

Давайте добавим структуру и массив данных структуры для хранения этой информации.

struct AnimationData {
    var delay: TimeInterval
}

static let DATA = [
    AnimationData(delay: 0.0),
    AnimationData(delay: 0.2),
    AnimationData(delay: 0.4),
]

Теперь давайте посмотрим на корневой вид.

struct ThreeBounceAnimation: View {

    @State var scales: [CGFloat] = DATA.map { _ in return 0 }

    var animation = Animation.easeInOut.speed(0.5)

    var body: some View {
        HStack {
            DotView(scale: .constant(scales[0]))
            DotView(scale: .constant(scales[1]))
            DotView(scale: .constant(scales[2]))
        }
        .onAppear {
            animateDots() // Not defined yet
        }
    }
}

Здесь у нас есть —

  1. scales для каждой точки и установите для нее значение 0. Эта переменная отвечает за анимацию масштаба точек, поскольку мы привязали ее к scale круга в DotView.
  2. Hstack, чтобы расположить точки горизонтально.
  3. animateDots для запуска анимации, как только вид станет видимым.

Если вы закомментируете вызов animateDots и запустите код, он покажет 3 статические точки. Теперь давайте реализуем функцию анимации.

func animateDots() {
    for (index, data) in Self.DATA.enumerated() {
        DispatchQueue.main.asyncAfter(deadline: .now() + data.delay) {
            animateDot(binding: $scales[index], animationData: data)
        }
    }

    //Repeat
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        animateDots()
    }
}

func animateDot(binding: Binding<CGFloat>, animationData: AnimationData) {
    withAnimation(animation) {
        binding.wrappedValue = 1
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
        withAnimation(animation) {
            binding.wrappedValue = 0.2
        }
    }
}

Здесь animateDots отвечает за основной цикл, а animateDot отвечает за анимацию одной точки.

Как мы видим, animateDots вызывает animateDot с задержкой для каждого представления согласно AnimationData. Это означает, что вторая и третья точки будут анимироваться поздно для каждой итерации.

Через 1 секунду функция animateDots вызывает себя, чтобы повторить анимацию.

Вот и все!

Запустите код, вы увидите, как красивые точки масштабируются и видны в ритме!

3. Анимация часов

Во-первых, начните с проектирования часов.

Для рисования внешнего круга часов не нужно ничего дополнительно, но по сравнению с рисованием линии это намного сложнее.

Начнем с линейного вида.

public struct Line: Shape {
    
    var lineType: LineType
    
    init(type: LineType) {
        self.lineType = type
    }
    
    public func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        
        path.move(to: center)
        path.addLine(to: CGPoint(x: rect.width * lineType.scale, y: rect.width * lineType.scale))
        return path
    }
}

enum LineType {
    case minute
    case second
    
    var scale: CGFloat {
        switch self {
        case .minute:
            return 0.3
        case .second:
            return 0.2
        }
    }
}

Здесь у нас есть —

  1. LineType — Enum, чтобы на часах отображались разные типы линий.
  • В котором scale Сохраняет размер шкалы такта часов.

2. Структура Line используется для рисования линии, соответствующей классу Shape.

  • В котором мы рисуем линии на основе прямоугольника родительского вида, типа такта часов и его масштаба.

Теперь давайте объединим все вещи в главном представлении.

Наш основной вид будет выглядеть так,

struct ClockAnimation: View {
    
    @State private var duration = 1.2
    @State private var startAnimation = false
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(.black, lineWidth: 2)
                .background(Circle().fill(.white))
            
            Line(type: .second)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 5.5, lineCap: .round, lineJoin: .round))
                .rotationEffect(.degrees(startAnimation ? 360 : 0))
                .animation(.linear(duration: duration).repeatForever(autoreverses: false), value: startAnimation)
            
            Line(type: .minute)
                .stroke(Color.primary, style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round))
                .rotationEffect(.degrees(startAnimation ? 360 : 0))
                .animation(.linear(duration: duration * 6).repeatForever(autoreverses: false), value: startAnimation)
        }
        .frame(width: 150, height: 150, alignment: .center)
        .onAppear {
            self.startAnimation.toggle()
        }
    }
}

Давайте разберемся в этом подробно.

Мы сделали это в два этапа,

  1. Нарисовал кружок на ZStack и на этом добавил вторую галочку

2. В верхней части секундного деления установите маленькое минутное деление.

Но только добавления представления недостаточно, мы должны установить угол поворота линии (галочки) и ее анимацию.

.rotationEffect — Поскольку мы делаем анимацию часов, наша линия будет вращаться на 360 градусов по часовой стрелке после запуска анимации.

.animation — Нам нужна линейная анимация, т.к. такт часов всегда вращается в линейном режиме, а также будет в режиме repeatForever при выключенном autoreverses.

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

Вот и все!

Запустите код, мы закончили. 🎉

Заключить

Это все на сегодня. Надеюсь, вы узнали что-то новое!

Это просто касается основы анимации SwiftUI. С помощью Canvas, API анимации и Math можно создавать гораздо более сложные анимации.

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

Связанные популярные статьи





Спасибо за любовь, которую вы показываете!

Если вам нравится то, что вы читаете, не упустите ни одной возможности поставить 👏 👏👏 ниже — как писатель это означает мир!

Кроме того, как всегда приветствуются отзывы, оставляйте их ниже в разделе комментариев.

Подпишитесь на Canopas, чтобы получать новости об интересных статьях!

Продолжай анимировать!