Как настроить анимацию изменения угла в SwiftUI
У меня есть приложение, которое показывает группу людей, у каждого из которых есть свое происхождение и угол зрения.
struct Location {
var centre:CGPoint
var facing:Angle
}
SwiftUI волшебным образом и автоматически выполняет большую часть анимации, когда они перемещаются из местоположения A в местоположение B
withAnimation {
person.location = newLocation
}
Однако для свойства Angle (облицовка) я хочу, чтобы анимация проходила по кратчайшему пути (имея в виду, что в реальном мире углы повторяются).
например, Swift UI правильно анимирует при изменении угла 5 -> 10 (градусов)
5,6,7,8,9,10
но переходя от 2 до 358, нужно пройти долгий путь
SwiftUI делает 2,3,4,5,6,7.......,357,358
где я бы хотел это сделать
2,1,0,359,358
как я могу это сделать?
благодарю вас
update: я надеюсь на решение, которое позволит мне работать с системой анимации, возможно, используя новую структуру MyAngle, которая напрямую предоставляет шаги анимации, возможно, используя какой-то модификатор анимации..easeInOut изменяет шаги - есть ли эквивалентный подход, при котором я могу создать анимацию.goTheRightWay?
4 ответа
Хорошо - отправляю свой ответ. Это немного похоже на ответ @Ben, но переносит управление "углом тени" на эффект вращения.
Все, что вам нужно сделать, это переключиться rotationEffect(angle:Angle)
за shortRotationEffect(angle:Angle,id:UUID)
это похоже
Image(systemName: "person.fill").resizable()
.frame(width: 50, height: 50)
.shortRotationEffect(self.person.angle,id:person.id)
.animation(.easeInOut)
ShortRotationEffect использует предоставленный идентификатор для поддержки словаря предыдущих углов. Когда вы устанавливаете новый угол, он определяет эквивалентный угол, который обеспечивает короткий поворот, и применяет его к нормальномуrotationEffect(...)
Вот:
extension View {
/// Like RotationEffect - but when animated, the rotation moves in the shortest direction.
/// - Parameters:
/// - angle: new angle
/// - anchor: anchor point
/// - id: unique id for the item being displayed. This is used as a key to maintain the rotation history and figure out the right direction to move
func shortRotationEffect(_ angle: Angle,anchor: UnitPoint = .center, id:UUID) -> some View {
modifier(ShortRotation(angle: angle,anchor:anchor, id:id))
}
}
struct ShortRotation: ViewModifier {
static var storage:[UUID:Angle] = [:]
var angle:Angle
var anchor:UnitPoint
var id:UUID
func getAngle() -> Angle {
var newAngle = angle
if let lastAngle = ShortRotation.storage[id] {
let change:Double = (newAngle.degrees - lastAngle.degrees) %% 360.double
if change < 180 {
newAngle = lastAngle + Angle.init(degrees: change)
}
else {
newAngle = lastAngle + Angle.init(degrees: change - 360)
}
}
ShortRotation.storage[id] = newAngle
return newAngle
}
func body(content: Content) -> some View {
content
.rotationEffect(getAngle(),anchor: anchor)
}
}
это зависит от моей функции положительного модуля:
public extension Double {
/// Returns modulus, but forces it to be positive
/// - Parameters:
/// - left: number
/// - right: modulus
/// - Returns: positive modulus
static func %% (_ left: Double, _ right: Double) -> Double {
let truncatingRemainder = left.truncatingRemainder(dividingBy: right)
return truncatingRemainder >= 0 ? truncatingRemainder : truncatingRemainder+abs(right)
}
}
Как насчет того, чтобы отрегулировать значение newLocation, чтобы оно оставалось в пределах 180 градусов от начала? Вот функция, позволяющая проверить, больше ли анимированное расстояние, чем половина, и предоставить новую конечную точку, которая ей соответствует.
func adjustedEnd(from start: CGFloat, to target: CGFloat) -> CGFloat {
// Shift end to be greater than start
var end = target
while end < start { end += 360 }
// Mod the distance with 360, shifting by 180 to keep on the same side of a circle
return (end - start + 180).truncatingRemainder(dividingBy: 360) - 180 + start
}
Некоторые примеры тестов:
let startValues: [CGFloat] = [2, -10, 345, 365, 700]
let endValues: [CGFloat] = [2, 10, 180, 185, 350, -10, 715, -700]
for start in startValues {
print("From \(start):")
for end in endValues {
let adjusted = adjustedEnd(from: start, to: end)
print("\t\(end) \tbecomes \(adjusted);\tdistance \(abs(adjusted - start))")
}
}
печатает следующее:
From 2.0:
2.0 becomes 2.0; distance 0.0
10.0 becomes 10.0; distance 8.0
180.0 becomes 180.0; distance 178.0
185.0 becomes -175.0; distance 177.0
350.0 becomes -10.0; distance 12.0
-10.0 becomes -10.0; distance 12.0
715.0 becomes -5.0; distance 7.0
-700.0 becomes 20.0; distance 18.0
From -10.0:
2.0 becomes 2.0; distance 12.0
10.0 becomes 10.0; distance 20.0
180.0 becomes -180.0; distance 170.0
185.0 becomes -175.0; distance 165.0
350.0 becomes -10.0; distance 0.0
-10.0 becomes -10.0; distance 0.0
715.0 becomes -5.0; distance 5.0
-700.0 becomes 20.0; distance 30.0
From 345.0:
2.0 becomes 362.0; distance 17.0
10.0 becomes 370.0; distance 25.0
180.0 becomes 180.0; distance 165.0
185.0 becomes 185.0; distance 160.0
350.0 becomes 350.0; distance 5.0
-10.0 becomes 350.0; distance 5.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 35.0
From 365.0:
2.0 becomes 362.0; distance 3.0
10.0 becomes 370.0; distance 5.0
180.0 becomes 540.0; distance 175.0
185.0 becomes 185.0; distance 180.0
350.0 becomes 350.0; distance 15.0
-10.0 becomes 350.0; distance 15.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 15.0
From 700.0:
2.0 becomes 722.0; distance 22.0
10.0 becomes 730.0; distance 30.0
180.0 becomes 540.0; distance 160.0
185.0 becomes 545.0; distance 155.0
350.0 becomes 710.0; distance 10.0
-10.0 becomes 710.0; distance 10.0
715.0 becomes 715.0; distance 15.0
-700.0 becomes 740.0; distance 40.0
(Отредактировано для учета отрицательных конечных значений)
Изменить: из вашего комментария о сохранении второго значения, как насчет настройкиLocation.facing
до настроенного угла, а затем добавив в Location что-то вроде
var prettyFacing: Angle {
var facing = self.facing
while facing.degrees < 0 { facing += Angle(degrees: 360) }
while facing.degrees > 360 { facing -= Angle(degrees: 360) }
return facing
}
с анимацией в SwiftUI
Вышеуказанный метод не будет работать, если вы используетеwithAnimation
в SwiftUI. Мне удалось найти обходной путь следующим образом: чтобы избежать «всплывания» анимации при пересечении предела 360/0, мы устанавливаем значение угла непосредственно на 0 (или 360), чтобы избежать анимации, тогда следующая анимация просто исправит поведение:
Супер жадно, но работает, и разрез не особо заметен
private func onDragGesture(value: DragGesture.Value, centerX: Double, centerY: Double) {
let current = value.location
// calculate difference in angle based on gesture
var theta = (atan2(current.x - centerX, centerY - current.y) - atan2(value.startLocation.x - centerX, centerY - value.startLocation.y)) * 180.0 / Double.pi
if (theta < 0) { theta += 360 }
if(self.angle > 330 && self.lastAngle + theta < 30) {
self.angle = 0
} else if (self.angle < 30 && self.lastAngle + theta > 330) {
self.angle = 360
}else{
withAnimation{
self.angle = theta + self.lastAngle
}
}
}
Попробовав оба других варианта, мы все еще получали визуальные сбои (менее распространенные, но все же присутствующие!).
Наше решение: используйте UIKit для анимации
Мы создали пакет SPM, который добавляет простой модификатор, .uiRotationEffect()
. Этот модификатор обертывает вашView
в UIView
, и использует UIView
с .animate(...)
функция, чтобы получить правильное поведение.
Вы можете установить пакет здесь или просто скопировать и вставить код сюда, это не очень долго.
GIF рабочего решения: