簡體   English   中英

當我更改 model 層的屬性時 animation 中的意外行為

[英]Unexpected behaviour in animation when i change the properties of the model layers

參考這篇文章,我正在嘗試將動畫調整為橫向模式。 基本上我想要的是旋轉-90°(順時針90°)的所有圖層,並且動畫水平運行而不是垂直運行。 作者沒有費心解釋背后的邏輯,obj-c 中有十幾個紙張折疊庫,它們都基於相同的架構,所以顯然這是 go 進行折疊的方式。

編輯:為了進一步闡明我想要實現的目標,在這里您可以查看我想要的動畫的三個快照(起點、半場和終點)。 在上面鏈接的問題中,animation 從下到上折疊,而我希望它從左到右折疊。

在下面,您可以查看稍加調整的原始項目:

  • 我改變了灰色bottomSleeve層的最終角度值,以及紅色和藍色的角度;
  • 我通過將perspectiveLayer speed設置為0來暫停動畫初始化並添加了 slider,然后將 slider 值設置為等於perspectiveLayer timeOffset以便您可以通過滑動交互地運行動畫的每一幀。 當 slider 上的觸摸事件結束時,動畫將從相對於當前timeOffset的幀恢復到最終值。
  • 在運行使用CATransaction添加到相關表示層的每個 animation 之前,我更改了所有 model 層值。 此外,完成后, perspectiveLayer速度再次設置為0
  • 為了更好的視覺理解,我將perspectiveLayer backgroundColor設置為cyan

只是指出,有兩個主要功能:

  1. viewDidLoad()中調用的setupLayers()負責設置圖層位置和錨點,以及將它們作為子圖層添加到mainView圖層。
  2. animate() ,在setupLayers()中遞歸調用,負責添加動畫。 在這里,我還將 model 圖層值設置為相關動畫的最終值,然后再添加它們。

只需復制、粘貼並運行:

class ViewController: UIViewController {

var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 300
let height: CGFloat = 150
var firstJointLayer: CATransformLayer = CATransformLayer()
var secondJointLayer:CATransformLayer = CATransformLayer()
var sizeHeight: CGFloat = 0
var positionY: CGFloat = 0

var perspectiveLayer: CALayer = {
    let perspectiveLayer = CALayer()
    perspectiveLayer.speed = 0.0
    perspectiveLayer.fillMode = .removed
    return perspectiveLayer
}()

var mainView: UIView = {
    let view = UIView()
    return view
}()

private let slider: UISlider = {
    let slider = UISlider()
    slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
    return slider
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(slider)
    setupLayers()
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    slider.frame = CGRect(x: view.bounds.size.width/3,
                          y: view.bounds.size.height/10*8,
                          width: view.bounds.size.width/3,
                          height: view.bounds.size.height/10)
}

@objc private func slide(sender: UISlider, event: UIEvent) {
    if let touchEvent = event.allTouches?.first {
        
        switch touchEvent.phase {
        case .ended:
            resumeLayer(layer: perspectiveLayer)
        default:
            perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
        }
        
    }
}

private func resumeLayer(layer: CALayer) {
    let pausedTime = layer.timeOffset
    layer.speed = 1.0
    layer.timeOffset = 0.0
    layer.beginTime = 0.0
    let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
    layer.beginTime = timeSincePause
}

private func setupLayers() {
    
    mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
    mainView.backgroundColor = UIColor.yellow
    view.addSubview(mainView)
    
    perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
    perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
    mainView.layer.addSublayer(perspectiveLayer)
    
    firstJointLayer.fillMode = .removed
    firstJointLayer.frame = mainView.bounds
    perspectiveLayer.addSublayer(firstJointLayer)
    
    topSleeve.fillMode = .removed
    topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
    topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
    topSleeve.backgroundColor = UIColor.red.cgColor
    topSleeve.position = CGPoint(x: width/2, y: 0)
    firstJointLayer.addSublayer(topSleeve)
    topSleeve.masksToBounds = true
    
    secondJointLayer.fillMode = .removed
    secondJointLayer.frame = mainView.bounds
    secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
    secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
    secondJointLayer.position = CGPoint(x: width/2, y: height)
    firstJointLayer.addSublayer(secondJointLayer)
    
    secondJointLayer.fillMode = .removed
    middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
    middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
    middleSleeve.backgroundColor = UIColor.blue.cgColor
    middleSleeve.position = CGPoint(x: width/2, y: 0)
    secondJointLayer.addSublayer(middleSleeve)
    middleSleeve.masksToBounds = true
    
    bottomSleeve.fillMode = .removed
    bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
    bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
    bottomSleeve.backgroundColor = UIColor.gray.cgColor
    bottomSleeve.position = CGPoint(x: width/2, y: height)
    secondJointLayer.addSublayer(bottomSleeve)
    
    firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
    firstJointLayer.position = CGPoint(x: width/2, y: 0)
    
    topShadow.fillMode = .removed
    topSleeve.addSublayer(topShadow)
    topShadow.frame = topSleeve.bounds
    topShadow.backgroundColor = UIColor.black.cgColor
    topShadow.opacity = 0
    
    middleShadow.fillMode = .removed
    middleSleeve.addSublayer(middleShadow)
    middleShadow.frame = middleSleeve.bounds
    middleShadow.backgroundColor = UIColor.black.cgColor
    middleShadow.opacity = 0
    
    transform.m34 = -1/700
    perspectiveLayer.sublayerTransform = transform
    
    sizeHeight = perspectiveLayer.bounds.size.height
    positionY = perspectiveLayer.position.y
    
    animate()
}


private func animate() {
    
    CATransaction.begin()
    
    CATransaction.setDisableActions(true)
    
    CATransaction.setCompletionBlock{ [weak self] in
        if self == nil { return }
        self?.perspectiveLayer.speed = 0
    }
    
    firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 1, 0, 0)
    secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 1, 0, 0)
    bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 1, 0, 0)
    perspectiveLayer.bounds.size.height = 0
    perspectiveLayer.position.y = 0
    topShadow.opacity = 0.5
    middleShadow.opacity = 0.5
    
    var animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -85*Double.pi/180
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 170*Double.pi/180
    secondJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -165*Double.pi/180
    bottomSleeve.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "bounds.size.height")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = sizeHeight
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    
    animation = CABasicAnimation(keyPath: "position.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = positionY
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    topShadow.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    middleShadow.add(animation, forKey: nil)
    CATransaction.commit()

}
}

如您所見,動畫按預期運行,此時要旋轉整個物體,只需更改位置、錨點和最終動畫值即可。 摘自上面鏈接的答案,這是起始項目所有層的一個很好的表示:

在此處輸入圖像描述

然后我繼續重構setupLayers()animate()以水平運行動畫,從左到右(換句話說,我在上面的圖層表示中順時針旋轉 90°)。

更改代碼以旋轉動畫后,我遇到兩個問題:

  1. 當動畫開始時, firstJointLayer position 沿着perspectiveLayer從左向右平移。 公平地說,這應該是一種預期的行為,因為它是perspectiveLayer的子層,實際上我不確定為什么在原始項目中它不會發生。 然而,為了解決這個問題,我添加了另一個 animation 負責在其相關系統中將其從右向左翻譯,因此它實際上看起來是靜止的。 此時,雖然我沒有更改 model 圖層的最終值(下方項目中的注釋行),但動畫按預期水平運行。 如果我不必同時修改 model 層,我的目標就會實現,因為這正是我想要的 animation。 然而...

  2. ...如果我然后嘗試設置動畫的最終值(只需將這些行注釋掉)我會得到一個意想不到的行為。 在動畫的初始幀,紅色、藍色和灰色層看起來相互折疊,因此旋轉不再像預期的那樣工作。 以下是時間 0.0、0.5 和 1.0(持續時間:1.0)的一些快照:

對我來說最不合邏輯的部分是設置 model 層的值等於表示層的最終值會導致錯誤,但它只會影響表示層,因為一旦動畫結束,下面的 model 層是預期的(和想要的)旋轉/位置: 在此處輸入圖像描述

當旋轉圍繞正確的點進行時,錨點肯定會正確放置。 我認為這可能與問題 1 有關,但我嘗試多次重新定位圖層但沒有成功。 直到今天,這仍然沒有解決,兩天后我無法找到主要問題並因此修復它。 對我來說,原始項目(上方)和旋轉項目(下方)在引擎蓋下的邏輯中看起來相同。

EDIT2:我在代碼中發現了一個小錯誤,我正在為 firstJointLayer x position 設置動畫,起始值等於 perspectiveLayer x position 而不是他自己的 x Z4757FE07FD0 已修復,但沒有任何改變。6

EDIT3 : Since setting the model layers values equal to the animation final values is what causes the bug, please note that using animation.fillMode = CAMediaTimingFillMode.forwards and animation.isRemovedOnCompletion = false is not a viable workaround for avoiding to touch the modal layers,因為我需要稍后恢復 animation,因此需要保持演示和 model 層同步。

非常感謝任何幫助。 下面是旋轉項目 - 我還評論了我從上面的項目中更改的塊:

  class ViewController: UIViewController {
    
var transform: CATransform3D = CATransform3DIdentity
var topSleeve: CALayer = CALayer()
var middleSleeve: CALayer = CALayer()
var bottomSleeve: CALayer = CALayer()
var topShadow: CALayer = CALayer()
var middleShadow: CALayer = CALayer()
let width: CGFloat = 200
let height: CGFloat = 300
var firstJointLayer: CALayer = CATransformLayer()
var secondJointLayer: CALayer = CATransformLayer()
var sizeWidth: CGFloat = 0
var positionX: CGFloat = 0
var firstJointLayerPositionX: CGFloat = 0


var perspectiveLayer: CALayer = {
    let perspectiveLayer = CALayer()
    perspectiveLayer.speed = 0.0
    perspectiveLayer.fillMode = .removed
    return perspectiveLayer
}()

var mainView: UIView = {
    let view = UIView()
    return view
}()

private let slider: UISlider = {
    let slider = UISlider()
    slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
    return slider
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(slider)
    setupLayers()
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    slider.frame = CGRect(x: view.bounds.size.width/3,
                          y: view.bounds.size.height/10*8,
                          width: view.bounds.size.width/3,
                          height: view.bounds.size.height/10)

}

@objc private func slide(sender: UISlider, event: UIEvent) {
    if let touchEvent = event.allTouches?.first {
        
        switch touchEvent.phase {
        case .ended:
            resumeLayer(layer: perspectiveLayer)
        default:
                perspectiveLayer.timeOffset = CFTimeInterval(sender.value)

        }
        
    }
}

private func resumeLayer(layer: CALayer) {
    let pausedTime = layer.timeOffset
    layer.speed = 1.0
    layer.timeOffset = 0.0
    layer.beginTime = 0.0
    let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
    layer.beginTime = timeSincePause
}

private func setupLayers() {
    
   // Changing all anchor points and positions here, in order to rotate the whole thing of -90°

    mainView = UIView(frame:CGRect(x: 50, y: 50, width: width*3, height: height))
    mainView.backgroundColor = UIColor.yellow
    view.addSubview(mainView)
    
    perspectiveLayer.frame = CGRect(x: width, y: 0, width: width*2, height: height)
    perspectiveLayer.backgroundColor = UIColor.cyan.cgColor
    mainView.layer.addSublayer(perspectiveLayer)
    
    firstJointLayer.fillMode = .removed
    firstJointLayer.frame = mainView.bounds
    firstJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
    firstJointLayer.position = CGPoint(x: width*2, y: height/2)
    perspectiveLayer.addSublayer(firstJointLayer)
    
    topSleeve.fillMode = .removed
    topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
    topSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
    topSleeve.backgroundColor = UIColor.red.cgColor
    topSleeve.position = CGPoint(x: width*3, y: height/2)
    firstJointLayer.addSublayer(topSleeve)
    topSleeve.masksToBounds = true
    
    secondJointLayer.fillMode = .removed
    secondJointLayer.frame = mainView.bounds
    secondJointLayer.frame = CGRect(x: 0, y: 0, width: width*2, height: height)
    secondJointLayer.anchorPoint = CGPoint(x: 1, y: 0.5)
    secondJointLayer.position = CGPoint(x: width*2, y: height/2)
    firstJointLayer.addSublayer(secondJointLayer)
    
    secondJointLayer.fillMode = .removed
    middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
    middleSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
    middleSleeve.backgroundColor = UIColor.blue.cgColor
    middleSleeve.position = CGPoint(x: width*2, y: height/2)
    secondJointLayer.addSublayer(middleSleeve)
    middleSleeve.masksToBounds = true
    
    bottomSleeve.fillMode = .removed
    bottomSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
    bottomSleeve.anchorPoint = CGPoint(x: 1, y: 0.5)
    bottomSleeve.backgroundColor = UIColor.gray.cgColor
    bottomSleeve.position = CGPoint(x: width, y: height/2)
    secondJointLayer.addSublayer(bottomSleeve)
    
    topShadow.fillMode = .removed
    topSleeve.addSublayer(topShadow)
    topShadow.frame = topSleeve.bounds
    topShadow.backgroundColor = UIColor.black.cgColor
    topShadow.opacity = 0
    
    middleShadow.fillMode = .removed
    middleSleeve.addSublayer(middleShadow)
    middleShadow.frame = middleSleeve.bounds
    middleShadow.backgroundColor = UIColor.black.cgColor
    middleShadow.opacity = 0
    
    transform.m34 = -1/700
    perspectiveLayer.sublayerTransform = transform
    
    sizeWidth = perspectiveLayer.bounds.size.width
    positionX = perspectiveLayer.position.x
    firstJointLayerPositionX = firstJointLayer.position.x

    
    animate()
}


private func animate() {
    
    CATransaction.begin()
    
    CATransaction.setDisableActions(true)
    
    CATransaction.setCompletionBlock{ [weak self] in
        if self == nil { return }
        self?.perspectiveLayer.speed = 0
    }
    
  //        firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
  //        secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
  //        bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
  //        perspectiveLayer.bounds.size.width = 0
  //        perspectiveLayer.position.x = 600
  //        firstJointLayer.position.x = 0
  //        topShadow.opacity = 0.5
  //        middleShadow.opacity = 0.5
    
    var animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -85*Double.pi/180
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 170*Double.pi/180
    secondJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -165*Double.pi/180
    bottomSleeve.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "bounds.size.width")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = sizeWidth
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "position.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = positionX
    animation.toValue = 600
    perspectiveLayer.add(animation, forKey: nil)

  // As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system

    animation = CABasicAnimation(keyPath: "position.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = firstJointLayerPositionX 
    animation.toValue = 0
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    topShadow.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    middleShadow.add(animation, forKey: nil)
    
    CATransaction.commit()

}

}

好吧 - 有點玩...

看起來您需要翻轉動畫,因為它們實際上是在“倒退”。

private func animate() {
    
    CATransaction.begin()
    
    CATransaction.setDisableActions(true)
    
    CATransaction.setCompletionBlock{ [weak self] in
        if self == nil { return }
        //self?.perspectiveLayer.speed = 0
    }
    
    firstJointLayer.transform = CATransform3DMakeRotation(CGFloat(-85*Double.pi/180), 0, 1, 0)
    secondJointLayer.transform = CATransform3DMakeRotation(CGFloat(170*Double.pi/180), 0, 1, 0)
    bottomSleeve.transform = CATransform3DMakeRotation(CGFloat(-165*Double.pi/180), 0, 1, 0)
    perspectiveLayer.bounds.size.width = 0
    perspectiveLayer.position.x = 600
    firstJointLayer.position.x = 0
    topShadow.opacity = 0.5
    middleShadow.opacity = 0.5

    var animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = -85*Double.pi/180
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    // flip 180 degrees
    animation.fromValue = 180*Double.pi/180
    // to 180 - 170
    animation.toValue = 10*Double.pi/180
    secondJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "transform.rotation.y")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    // flip -180 degrees
    animation.fromValue = -180*Double.pi/180
    // to 180 - 165
    animation.toValue = -15*Double.pi/180
    bottomSleeve.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "bounds.size.width")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = sizeWidth
    animation.toValue = 0
    perspectiveLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "position.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = positionX
    animation.toValue = 600
    perspectiveLayer.add(animation, forKey: nil)
    
    // As said above, i added this animation which is not included in the original project, as the firstJointLayer was translating his position from left to right along with the perspectiveLayer position, so i make a reverse translation in its relative system so that it is stationary in the mainView system
    
    animation = CABasicAnimation(keyPath: "position.x")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = firstJointLayerPositionX
    animation.toValue = 0
    firstJointLayer.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    topShadow.add(animation, forKey: nil)
    
    animation = CABasicAnimation(keyPath: "opacity")
    animation.fillMode = CAMediaTimingFillMode.removed
    animation.duration = 1
    animation.fromValue = 0
    animation.toValue = 0.5
    middleShadow.add(animation, forKey: nil)
    
    CATransaction.commit()
    
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM