Я анимирую линию по горизонтали, и она работает нормально, за исключением того, что эффект, к которому я стремлюсь, - это небольшой упругий отскок перед тем, как она поменяет стороны. Я думал, что эффект .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
Я обновил пост, добавив ссылку на видео, показывающее анимацию.
Итак, если я правильно понимаю, вы хотите анимировать не только offset
, но и ширину линии?
Правильный. Я добился анимации смещения, наложив линию на другую линию, которая обрезана, и обновив минимальное и максимальное смещения до трети анимируемой линии. Просто не могу понять, как добиться эффекта отскока. Мне кажется, что эффект отскока может быть просто результатом замедления анимации на каждом конце перед тем, как набрать скорость, чего я и думал добиться с помощью .easeInOut
Вы на правильном пути, но вы переусердствовали в разработке своего решения. По сути, вам нужно сделать 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()
}
}
}
}
Это также можно сделать, установив смещение по оси 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()
}
Так что просто используйте обычную весеннюю анимацию, например
.bouncy
? Что в этом плохого? Что вы подразумеваете под «объединить с.easeInOut
»? Имеете ли вы в виду математическое определение кривой анимации?