Объедините весеннюю анимацию с easyInOut в SwiftUI

Я анимирую линию по горизонтали, и она работает нормально, за исключением того, что эффект, к которому я стремлюсь, - это небольшой упругий отскок перед тем, как она поменяет стороны. Я думал, что эффект .easeInOut будет имитировать этот эффект, но он не кажется пружинистым. Как я могу объединить анимацию .spring так, чтобы линия как бы подпрыгивала на долю секунды на каждом конце, прежде чем переместиться на другую сторону?

struct Line: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: 0, y: rect.size.height / 2))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height / 2))
        return path
    }
}

struct LineView: View {
    let height: CGFloat
    @State private var animating = false
    
    var body: some View {
        GeometryReader { geometry in
            let lineWidth = geometry.size.width / 3
            
            Line()
                .stroke(Color.black, lineWidth: height)
                .frame(width: lineWidth)
                .offset(x: animating ? 0 : geometry.size.width - lineWidth)
                .animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
                .onAppear {
                    animating.toggle()
                }
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            LineView(height: 5.0)
                .frame(width: 200)
        }
    }
}

Любая помощь приветствуется.

Предполагаемый эффект (синяя линия): https://streamable.com/ijwe1w

Так что просто используйте обычную весеннюю анимацию, например .bouncy? Что в этом плохого? Что вы подразумеваете под «объединить с .easeInOut»? Имеете ли вы в виду математическое определение кривой анимации?

Sweeper 04.07.2024 14:14

Я обновил пост, добавив ссылку на видео, показывающее анимацию.

batman 04.07.2024 14:44

Итак, если я правильно понимаю, вы хотите анимировать не только offset, но и ширину линии?

Sweeper 04.07.2024 14:48

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

batman 04.07.2024 14:54
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
62
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Вы на правильном пути, но вы переусердствовали в разработке своего решения. По сути, вам нужно сделать 3 слоя. Первый — это фон, как он воспринимается пользователем, но на самом деле это базовый вид. Кроме того, вы добавляете свой визуальный элемент. Для простоты я использовал капсулы. Верхний слой — это маска, именно здесь происходит волшебство.

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

struct LineView: View {
    let height: CGFloat
    @State private var animating = false
    
    var body: some View {
        GeometryReader { geometry in
            Capsule() // Base layer
                .fill(.gray.opacity(0.3))
                .frame(height: height + 2)
                .overlay(alignment: .leading) {
                    let lineWidth = geometry.size.width / 3
                    Capsule()
                        .fill(.blue)
                        .frame(width: lineWidth)
                        // Here I am overshooting by half the line width in both directions
                        .offset(x: animating ? -(lineWidth / 2) : geometry.size.width - lineWidth / 2)
                        .animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
                }
                // Then mask over with a FillStyle(eoFill: true)
                .mask(
                    Capsule()
                        .fill(style: FillStyle(eoFill: true))
                        .frame(height: height + 2)
                )
                .onAppear {
                    animating.toggle()
                }
                .frame(maxHeight: .infinity)
        }
    }
}

В итоге вы получаете вот это:

(Обратите внимание, это гифка, и она не такая плавная, как настоящая анимация)

Это работает очень хорошо, если вы просто анимируете ведущие и конечные отступы и используете отрицательные отступы по бокам. Затем фигуру необходимо обрезать, чтобы в результате отрицательного заполнения линия выглядела короче.

На самом деле нет необходимости использовать форму Line. Вы можете просто использовать Rectangle как в качестве заполненной формы, так и в качестве формы клипа. Альтернативно, если вы хотите закругленные углы, используйте Capsule.

struct LineView: View {
    let height: CGFloat
    @State private var animating = false

    var body: some View {
        GeometryReader { geometry in
            let w = geometry.size.width
            let maxWidth = w / 3
            let minWidth = maxWidth / 2
            Capsule()
                .fill(.blue)
                .padding(.leading, animating ? minWidth - maxWidth : w - minWidth)
                .padding(.trailing, animating ? w - minWidth : minWidth - maxWidth)
                .clipShape(Capsule())
                .frame(height: height)
                .frame(maxHeight: .infinity)
                .animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
                .onAppear {
                    animating.toggle()
                }
        }
    }
}

Animation

Это также можно сделать, установив смещение по оси X вместо заполнения. Это тоже работает (и ближе к тому, что у вас было изначально):

Capsule()
    .fill(.blue)
    .frame(width: maxWidth, height: height)
    .offset(x: animating ? minWidth - maxWidth : w - minWidth)
    .frame(width: w, alignment: .leading)
    .clipShape(Capsule())
    .frame(maxHeight: .infinity)
    .animation(.easeInOut(duration: 1.0).repeatForever(), value: animating)
    .onAppear {
        animating.toggle()
    }

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