Пытаюсь заставить весеннюю анимацию работать правильно. Я сделал репозиторий git. В основном красный вид ограничен следующим образом:
let ac = brokenView.widthAnchor.constraint(equalTo: brokenView.heightAnchor, multiplier: 9/16)
let xc = brokenView.centerXAnchor.constraint(equalTo: animView.centerXAnchor)
let yc = brokenView.centerYAnchor.constraint(equalTo: animView.centerYAnchor)
let widthC = brokenView.widthAnchor.constraint(equalTo: animView.widthAnchor)
widthC.priority = .defaultLow
let gewc = brokenView.widthAnchor.constraint(greaterThanOrEqualTo: animView.widthAnchor)
let geHC = brokenView.heightAnchor.constraint(greaterThanOrEqualTo: animView.heightAnchor)
geHC.priority = .required
Синий вид начинается с соотношения сторон != 9/16 -> анимируется до 9/16. Я хотел бы видеть, что когда синий вид становится слишком высоким, красный начинает толстеть. Вместо этого он просто придерживается привязки ширины с более низким приоритетом. Любой совет приветствуется.
Код, который не так полезен, как мог бы быть без файла раскадровки:
import UIKit
class MyViewController: UIViewController {
@IBOutlet weak var animView: UIView!
@IBOutlet weak var brokenView: UIView!
var isBig = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
brokenView.border(3, color: .red)
animView.border(3, color: .blue)
animView.frame = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 180, width: 240, height: 360)
brokenView.translatesAutoresizingMaskIntoConstraints = false
let ac = brokenView.widthAnchor.constraint(equalTo: brokenView.heightAnchor, multiplier: 9/16)
let xc = brokenView.centerXAnchor.constraint(equalTo: animView.centerXAnchor)
let yc = brokenView.centerYAnchor.constraint(equalTo: animView.centerYAnchor)
let widthC = brokenView.widthAnchor.constraint(equalTo: animView.widthAnchor)
widthC.priority = .defaultLow
let gewc = brokenView.widthAnchor.constraint(greaterThanOrEqualTo: animView.widthAnchor)
let geHC = brokenView.heightAnchor.constraint(greaterThanOrEqualTo: animView.heightAnchor)
geHC.priority = .required
NSLayoutConstraint.activate([
ac,
xc,
yc,
widthC,
gewc,
geHC
])
let gr = UITapGestureRecognizer(target: self, action: #selector(MyViewController.handleTap(_:)))
view.addGestureRecognizer(gr)
}
@objc func handleTap(_ gr: UITapGestureRecognizer) {
let newFrame: CGRect
if isBig {
newFrame = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 180, width: 240, height: 360)
} else {
newFrame = CGRect(x: view.frame.midX - (270/2), y: view.frame.midY - (480/2), width: 270, height: 480)
}
let timing = UISpringTimingParameters.init(dampingRatio: 0.01, frequencyResponse: 10)
let anim = UIViewPropertyAnimator(duration: 10, timingParameters: timing)
anim.addAnimations {
self.animView.frame = newFrame
}
anim.addCompletion { _ in
self.isBig = !self.isBig
}
anim.startAnimation()
}
}
extension UIView {
func border(_ width: Float, color: UIColor) {
layer.borderColor = color.cgColor
layer.borderWidth = CGFloat(width)
}
}
extension UISpringTimingParameters {
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat) {
let mass = 1 as CGFloat
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
let damping = 4 * .pi * dampingRatio * mass / frequencyResponse
self.init(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: .zero)
}
}
Весь соответствующий код должен быть указан в самом вопросе, а не в ссылке на репозиторий git.
@matt, это буквально не так....
@Paulw11, как это работает с файлами раскадровки? Разве SO просто не решает проблемы, более сложные, чем один файл?
Мы можем сделать некоторые выводы из файла раскадровки. В этом случае вы можете легко создавать представления и ограничения полностью в коде. Это было бы лучше, чем раскадровка, потому что тогда мы могли бы легко увидеть, какие ограничения у вас есть. Репозиторий GitHub не поможет, если мы не захотим загрузить его и открыть проект.
Не совсем понятно, какова ваша конечная цель, но...
UIViewPropertyAnimator
с UISpringTimingParameters
— это визуальный эффект... он колеблется «пружинкой», пока не достигнет своего «конечного пункта назначения».
Вот как это может выглядеть:
он не меняет постоянно рамку обзора.
Вот ваш код, измененный, чтобы мы могли его объяснить:
class SomeViewController: UIViewController {
let blueAnimView: UIView = UIView()
let redFollowView: UIView = UIView()
let smallFrameView: UIView = UIView()
let bigFrameView: UIView = UIView()
var bigRect: CGRect = .zero
var smallRect: CGRect = .zero
var isBig = false
override func viewDidLoad() {
super.viewDidLoad()
// define the small and big frames (rects)
smallRect = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 180, width: 240, height: 360)
bigRect = CGRect(x: view.frame.midX - (270/2), y: view.frame.midY - (480/2), width: 270, height: 480)
// 3-point red border
redFollowView.border(3, color: .red)
// let's make blue view's border thicker to make it easier to see what's going on
blueAnimView.border(8, color: .blue)
view.addSubview(bigFrameView)
view.addSubview(smallFrameView)
view.addSubview(blueAnimView)
view.addSubview(redFollowView)
// set the "start" frame of the blue view
blueAnimView.frame = smallRect
redFollowView.translatesAutoresizingMaskIntoConstraints = false
// for simplicity, let's constrain redView to blueView on all 4 sides
NSLayoutConstraint.activate([
redFollowView.topAnchor.constraint(equalTo: blueAnimView.topAnchor),
redFollowView.leadingAnchor.constraint(equalTo: blueAnimView.leadingAnchor),
redFollowView.trailingAnchor.constraint(equalTo: blueAnimView.trailingAnchor),
redFollowView.bottomAnchor.constraint(equalTo: blueAnimView.bottomAnchor),
])
let gr = UITapGestureRecognizer(target: self, action: #selector(MyViewController.handleTap(_:)))
view.addGestureRecognizer(gr)
// so we can see the small and big frames
bigFrameView.backgroundColor = .systemYellow
smallFrameView.backgroundColor = .yellow
bigFrameView.frame = bigRect
smallFrameView.frame = smallRect
}
@objc func handleTap(_ gr: UITapGestureRecognizer) {
let newFrame: CGRect
if isBig {
newFrame = smallRect
} else {
newFrame = bigRect
}
// slow spring
let timing = UISpringTimingParameters.init(dampingRatio: 0.01, frequencyResponse: 10)
// quick spring
//let timing = UISpringTimingParameters.init(dampingRatio: 0.1, frequencyResponse: 0.5)
let anim = UIViewPropertyAnimator(duration: 10, timingParameters: timing)
anim.addAnimations {
self.blueAnimView.frame = newFrame
}
anim.addCompletion { _ in
print("completion")
self.isBig = !self.isBig
}
anim.startAnimation()
}
}
extension UIView {
func border(_ width: Float, color: UIColor) {
layer.borderColor = color.cgColor
layer.borderWidth = CGFloat(width)
}
}
extension UISpringTimingParameters {
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat) {
let mass = 1 as CGFloat
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
let damping = 4 * .pi * dampingRatio * mass / frequencyResponse
self.init(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: .zero)
}
}
Первый,
blueAnimView
и redFollowView
redFollowView
всеми 4 сторонами blueAnimView
Давайте также добавим представления Yellow и SystemYellow, чтобы мы могли видеть маленькие и большие рамки:
Когда мы добавим красный и синий виды, поначалу это будет выглядеть так:
Теперь, как только мы вызываем anim.startAnimation()
, мы видим, что ограничения redFollowView
заставляют его соответствовать кадру «конечного пункта назначения» blueAnimView
:
когда происходит пружинная анимация, redFollowView
не меняет размер, потому что рамка blueAnimView
не меняет размер во время анимации:
Здесь замедлено для ясности:
Если вы хотите, чтобы красный вид следовал за синим во время анимации, вам придется применить другой подход.
Редактировать
Чтобы получить желаемый эффект, синий вид использует анимацию Spring для изменения размера, а красный (подвид) соответствует либо ширине, либо высоте, сохраняя при этом соотношение 9:16 (фактически «подгонка с обратным соотношением сторон»):
Нам нужно отказаться от идеи ограничения взглядов (см. обсуждение выше).
Вместо этого мы:
CADisplayLink
Пример кода (без соединений @IBOutlet
или @IBAction
)... просто назначьте новый пустой контроллер представления UsingDisplayLinkVC
:
class UsingDisplayLinkVC: UIViewController {
let blueAnimView: UIView = UIView()
let redFollowView: UIView = UIView()
let smallFrameView: UIView = UIView()
let bigFrameView: UIView = UIView()
var bigRect: CGRect = .zero
var smallRect: CGRect = .zero
var isBig = false
var displayLink: CADisplayLink!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// define the small and big frames (rects)
smallRect = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 180, width: 240, height: 360)
bigRect = CGRect(x: view.frame.midX - (270/2), y: view.frame.midY - (480/2), width: 270, height: 480)
// 3-point red border
redFollowView.border(8, color: .red)
// let's make blue view's border thicker to make it easier to see what's going on
blueAnimView.border(3, color: .blue)
view.addSubview(bigFrameView)
view.addSubview(smallFrameView)
// add redFollowView as subview of blueAnimView
blueAnimView.addSubview(redFollowView)
view.addSubview(blueAnimView)
// set the "start" frame of the blueAnimView
blueAnimView.frame = smallRect
// calculate and set the redFollowView frame
let h: CGFloat = blueAnimView.frame.width * 16.0 / 9.0
redFollowView.frame = .init(x: 0.0, y: 0.0, width: blueAnimView.frame.width, height: h)
redFollowView.center.x = blueAnimView.bounds.midX
redFollowView.center.y = blueAnimView.bounds.midY
let gr = UITapGestureRecognizer(target: self, action: #selector(MyViewController.handleTap(_:)))
view.addGestureRecognizer(gr)
// so we can see the small and big frames
bigFrameView.backgroundColor = .systemYellow
smallFrameView.backgroundColor = .yellow
bigFrameView.frame = bigRect
smallFrameView.frame = smallRect
// init displayLink
displayLink = CADisplayLink(target: self, selector: #selector(update))
// start displayLink paused
displayLink.isPaused = true
// add to run loop
displayLink.add(to: .current, forMode: .common)
}
@objc func handleTap(_ gr: UITapGestureRecognizer) {
// if animation is running, return
if !displayLink.isPaused { return }
let newFrame: CGRect
if isBig {
newFrame = smallRect
} else {
newFrame = bigRect
}
// slow spring
let timing = UISpringTimingParameters.init(dampingRatio: 0.01, frequencyResponse: 10)
// quick spring
//let timing = UISpringTimingParameters.init(dampingRatio: 0.1, frequencyResponse: 0.5)
let anim = UIViewPropertyAnimator(duration: 10, timingParameters: timing)
anim.addAnimations {
self.blueAnimView.frame = newFrame
}
anim.addCompletion { _ in
print("completion")
self.isBig = !self.isBig
// pause displayLink
self.displayLink.isPaused = true
}
// un-pause displayLink
displayLink.isPaused = false
anim.startAnimation()
}
@objc func update() {
// get the current frame of blueAnimView's presentation layer
if let curBlueAnimFrame = blueAnimView.layer.presentation()?.frame {
let aspect: CGFloat = curBlueAnimFrame.width / curBlueAnimFrame.height
var w: CGFloat = 0.0
var h: CGFloat = 0.0
// if curBlueAnimFrame has aspect ratio greater than 9:16
// use curBlueAnimFrame width and calculate the new redFollowView height
// else
// use curBlueAnimFrame height and calculate the new redFollowView width
if aspect > 9.0 / 16.0 {
w = curBlueAnimFrame.width
h = w * 16.0 / 9.0
} else {
h = curBlueAnimFrame.height
w = h * 9.0 / 16.0
}
// keep redFollowView centered
let x: CGFloat = curBlueAnimFrame.midX - curBlueAnimFrame.origin.x
let y: CGFloat = curBlueAnimFrame.midY - curBlueAnimFrame.origin.y
redFollowView.frame = .init(x: 0.0, y: 0.0, width: w, height: h)
redFollowView.center = .init(x: x, y: y)
}
}
}
Примечание. Это только пример кода! Он не предназначен и не должен считаться «готовым к производству».
Редактировать 2
Как обсуждалось в комментариях, сказанное выше не совсем верно. При обратном звонке update()
мы:
presentation layer
К сожалению, когда мы устанавливаем рамку, она визуально не меняется до следующего цикла отрисовки... когда анимированное представление уже имеет следующий размер. Таким образом, следующий вид всегда находится на один кадр позади анимированного представления.
Одним из вариантов было бы написать собственный (или найти) код Spring Animation, а затем вычислить и установить оба кадра при каждом обновлении Display Link.
Или мы можем «обмануть»…
UIView
Теперь рамки обоих контурных представлений будут установлены одновременно. Мы по-прежнему будем находиться «на один кадр позади» кадра анимации, но, поскольку он очевиден, мы его не увидим.
Быстрый пример кода для демонстрации:
class CheatVC: UIViewController {
let blueView = UIView()
let redView = UIView()
let cheatAnimView = UIView()
let smallFrameView: UIView = UIView()
let bigFrameView: UIView = UIView()
var bigRect: CGRect = .zero
var smallRect: CGRect = .zero
var isBig = false
var displayLink: CADisplayLink!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// define the small and big frames (rects)
smallRect = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 180, width: 240, height: 360)
//smallRect = CGRect(x: view.frame.midX - 120, y: view.frame.midY - 80, width: 240, height: 160)
bigRect = CGRect(x: view.frame.midX - (270/2), y: view.frame.midY - (480/2), width: 270, height: 480)
// 3-point red border
redView.border(3, color: .red)
// let's make blueView's border thicker to make it easier to see what's going on
blueView.border(8, color: .blue)
view.addSubview(bigFrameView)
view.addSubview(smallFrameView)
// add blueView and redView as siblings
view.addSubview(blueView)
view.addSubview(redView)
// set the "start" frame of the blueAnimView
blueView.frame = smallRect
// calculate and set the redView frame
let h: CGFloat = blueView.frame.width * 16.0 / 9.0
redView.frame = .init(x: 0.0, y: 0.0, width: blueView.frame.width, height: h)
redView.center = blueView.center
cheatAnimView.frame = smallRect
view.addSubview(cheatAnimView)
cheatAnimView.backgroundColor = .clear
let gr = UITapGestureRecognizer(target: self, action: #selector(MyViewController.handleTap(_:)))
view.addGestureRecognizer(gr)
// so we can see the small and big frames
bigFrameView.backgroundColor = .systemYellow
smallFrameView.backgroundColor = .yellow
bigFrameView.frame = bigRect
smallFrameView.frame = smallRect
// init displayLink
displayLink = CADisplayLink(target: self, selector: #selector(update))
// start displayLink paused
displayLink.isPaused = true
// add to run loop
displayLink.add(to: .current, forMode: .common)
}
@objc func handleTap(_ gr: UITapGestureRecognizer) {
// if animation is running, return
if !displayLink.isPaused { return }
let newFrame: CGRect
if isBig {
newFrame = smallRect
} else {
newFrame = bigRect
}
// slow spring
var timing = UISpringTimingParameters.init(dampingRatio: 0.01, frequencyResponse: 10)
// quick spring
timing = UISpringTimingParameters.init(dampingRatio: 0.1, frequencyResponse: 0.15)
let anim = UIViewPropertyAnimator(duration: 2, timingParameters: timing)
anim.addAnimations {
self.cheatAnimView.frame = newFrame
}
anim.addCompletion { _ in
print("completion")
self.isBig = !self.isBig
// pause displayLink
self.displayLink.isPaused = true
}
// un-pause displayLink
displayLink.isPaused = false
anim.startAnimation()
}
@objc func update() {
// get the current frame of cheatAnimView's presentation layer
if let curAnimFrame = cheatAnimView.layer.presentation()?.frame {
let aspect: CGFloat = curAnimFrame.width / curAnimFrame.height
var w: CGFloat = 0.0
var h: CGFloat = 0.0
// if blueView's frame has aspect ratio greater than 9:16
// use blueView width and calculate the new redView height
// else
// use blueView height and calculate the new redView width
if aspect > 9.0 / 16.0 {
w = curAnimFrame.width
h = w * 16.0 / 9.0
} else {
h = curAnimFrame.height
w = h * 9.0 / 16.0
}
blueView.frame = curAnimFrame
redView.frame = .init(x: 0.0, y: 0.0, width: w, height: h)
redView.center = blueView.center
}
}
}
Объяснение Дона по-прежнему верно. Процесс анимации не применяет ограничения повторно. Он просто берет разницу между начальным и конечным состояниями, а затем интерполирует ключевые кадры. В случае с пружинящей анимацией он также экстраполирует некоторые ключевые кадры, но эта экстраполяция основана на предположении, что параметр будет продолжать меняться так же, как и между начальной и конечной точками. В вашем случае это не то, чего вы хотите. Я не думаю, что есть простой способ сделать это.
@nickneedsaname — да, анимация будет другой, если представления будут одноуровневыми, а не родительско-дочерними. Однако я думал, что вы ищете объяснение того, почему ограничения не следуют за анимацией. Пока не совсем понятно, какова ваша цель... Взгляните на это: i.imgur.com/P1LwtdK.mp4 «Вид с красной рамкой» — это подвид «Вид с синей рамкой» и «Вид с синей рамкой». анимируется. Это то, чего ты добиваешься?
@DonMag ДА, КАК ВЫ ЭТО СДЕЛАЛИ Это именно то желаемое поведение. «Когда синий вид становится слишком высоким, красный начинает толстеть».
@nickneedsaname - см. Редактирование моего ответа.
🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳
@DonMag, так что это работает очень хорошо. Если вы заставите пружину двигаться быстро и с большой упругостью, это не будет работать так хорошо, поскольку мы вычисляем наш кадр на основе текущей позиции, которая будет сильно отличаться от следующей позиции, поэтому следующий вид будет выглядеть так, как будто это всегда немного отстает.
@DonMag На практике это не проблема (без сумасшедших весенних анимаций), но меня беспокоит возможность возникновения проблем в некоторых случаях (CADisplayLink не гарантируется с постоянной скоростью, зависит от системных проблем, я могу видите, что это вызывает потенциальные сбои, которые будет трудно отследить/диагностировать/воспроизвести) Знаете ли вы, есть ли способ синхронизировать ссылку на дисплей с анимацией? Или получить следующие кадры презентации? Или заставить redFollowView.frame = .init(x: 0.0, y: 0.0, width: w, height: h)
отрисовываться немедленно?
@nickneedsaname — Я думаю, это максимально близко к тому, что вы можете сделать... используя подпредставления. В зависимости от вашей конечной цели вы можете использовать одно представление и нарисовать контурные рамки, но я не знаю, будет ли это работать с вашей фактической реализацией.
@DonMag, как выглядит рисунок? Я не могу поверить, что нет способа сказать движку анимации: «Подожди, позволь мне сделать что-нибудь очень быстро» перед каждым рендерингом.
Например, я склоняюсь к тому, чтобы каким-то образом попытаться перепроектировать кадры пружинной анимации, а затем использовать информацию о синхронизации displaylink, чтобы «угадать» (поскольку время будет почти, но не совсем идеальным), где будет следующий кадр. и используйте это (по сравнению с презентацией) в ссылке на дисплей
@DonMag Вот сумасшедшая мысль: поскольку начальный размер синего прямоугольника фиксирован, я мог бы просто построить большую таблицу значений blueRect на каждом этапе анимации и использовать ее для вычисления красного кадра в ссылке на дисплей. По сути, это «обратное проектирование» без необходимости выполнения какого-либо обратного проектирования.
@nickneedsaname, какова твоя настоящая цель? Вам нужна только эта прямоугольная анимация? Или у этих двух представлений будут подпредставления (метки, представления изображений и т. д.)?
@nickneedsaname — вы можете применить «читский» подход… анимировать четкое представление и установить обе рамки контурного представления при каждом обновлении ссылки на дисплей. По-прежнему будет «на один кадр позади»… но анимированный вид не будет виден, и два контура останутся синхронизированными. Или... Я взял какой-то ручной код весенней анимации, который, кажется, работает очень хорошо. Если вы хотите продолжить обсуждение этого, нам, вероятно, следует перейти в чат.
Ладно, не буду утруждать себя редактированием, так как это не работает. Моя математика настолько близка к идеальной, насколько это возможно. Ошибки — это самые большие первые ~2 такта DLink, которые не всегда гарантированы; ~0,1 при максимальном разрешении, что меньше пикселя при разрешении 3x, так что я думаю, что это нормально. Проблема в том, что что бы я ни делал, все не выравнивается должным образом, я вижу визуальные ошибки порядка 10+ пикселей, что просто не имеет никакого смысла. «Анимировать четкую рамку» отлично работает, поэтому я просто сделаю это. Еще раз спасибо!!!
@nickneedsaname — да, анимация четкого изображения и обновление красной и синей рамок кажется разумным вариантом. Я дал свой ответ Edit 2, описывающий проблему, а также пример кода для опции «обман».
@DonMag, если вы хотите покопаться в математике: desmos.com/calculator/sxlgip2prq -- g(x) — это «наивная» весенняя анимация, которую вы найдете по всему Интернету. f(x) — это тот, где вычисляется фи, поэтому у нас есть скорость 0 @ t=0, а B вычисляется так, что f(x) = A_init @ t=0. Очень доволен тем, насколько хорошо f(x) соответствует x_actual.
Единственное, чего я не смог понять, это то, как рассчитывается длительность... Для этого и существуют все 1-я, 2-я и 3-я производные. Не особо задумываясь, анимация заканчивается, когда она сильно меняется, так что, может быть, когда производная (1-я или 2-я?) находится ниже порогового значения? или что-то?
@DonMag Кроме того, я думаю, что вы правы, правильный способ сделать это - запустить нашу собственную анимацию поверх CADisplayLink, чтобы мы могли делать что-то вручную, но я не хочу иметь с этим дело прямо сейчас. Чит работает достаточно хорошо.
??? Почему проголосовали за закрытие из-за отсутствия ясности?? Проблема четко сформулирована (ограничения при анимации игнорируются), есть и репозиторий кода, и изображение.