Search code examples
iosswiftanimationtransformcgaffinetransform

How to correctly animate transformations on an priorly transformed UIView


Summary of the issue

Unable to correctly perform a move-and-scale animation on an already translated UIView.

When the view has not been priorly moved, in order to achieve the desired result, I apply the scale first and then the translation. I animate the resulting transformation using a UIViewPropertyAnimator: the View scales up or down whilst moving at the same time accordingly.

However, if the view has been moved (translated) from its original position before applying this animated transformation, I failed to achieve the result consisting in scaling up or down whilst moving the View from its new position.

¡Although transform issues are well documented - and I have done my due diligence before submitting the question - I have failed to find a successful solution so far!

Transformation Animation Code

In order to facilitate the understanding as well as the resolution of the issue, the codes have been simplified.

extension UIView {

   func zoomAndMove(vector: CGPoint, scale: CGPoint){
      self.transform = self.transform.concatenating(CGAffineTransform(scaleX: scale.x, y: scale.y).concatenating(CGAffineTransform(translationX: vector.x, y: vector.y))
   }


   func animateZoomAndMove(from origin: CGPoint, for duration: TimeInterval, cameraZoom: CGPoint, timingFunction: UITimingCurveProvider, controlPoint2: CGPoint(x: 0.8, y: 0.7)) autoplay: Bool = false) -> UIViewPropertyAnimator {
      let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timingFunction)
      let vector = CGPoint(x: self.frame.midX - origin.x, y: self.frame.midY - origin.y)

      animator.addAnimations {
         self.zoomAndMove(vector: vector, scale: cameraZoom)
      }

      if autoplay { animator.startAnimation()}
      return animator
   }
}

Attempts so far

I have tried to modify my code to return a transform that takes into account a previous translation before the zoomAndMove occurs:

extension UIView {

   func zoomAndMove(vector: CGPoint, scale: CGPoint){

      if self.transform == CGAffineTransform.identity {
          self.transform = self.transform.concatenating(CGAffineTransform(scaleX: scale.x, y: scale.y).concatenating(CGAffineTransform(translationX: vector.x, y: vector.y)) 
      } else {
          let preTransform = self.transform
          self.transform = CGAffineTransform(a: scale.x, b: 0, c: 0, d: scale.y, tx: vector.x + preTransform.tx, ty: vector.y + preTransform.ty)
      }
   }

This code does not result in the desired effect: the view jumps to a new location, scales correctly and moves "randomly".

I am definitely missing something - I might be aiming at the wrong end result matrix - but all in all I am currently stuck.

Should anyone have a clue on how to perform such a simple task as scaling and moving a UIView from an already translated UIView, I would be most grateful for their input!

Best,


EDIT

A picture can be worth a 1,000 words, so here is what happens when I try to implement the various suggestions that have been kindly made so far (in particular the .scaledBy(:) method):

enter image description here

You can notice the final transformation is right, but the animation is not.


Solution

  • First of all, thanks to @gmogames for taking the time to offer suggestions. It always helps to be able to exchange ideas!!!

    The issue had indeed something to do with resetting the anchorPoint (or the center) of the view before applying the new transformation so that the animation runs correctly. Therefore, using a much simpler example of scaling a view after having moved it, here is what the new method looks like:

    extension UIView {
        func scaleView(scaleFactor: CGPoint, duration: TimeInterval) {
            // store the view original center
            let oCenter = self.center
    
            // get the current transformation
            let cTransform = self.transform
    
            // set the new center of the view 
            self.center = CGPoint(x: self.frame.midX, y: self.frame.midY)
    
            // clears the transform matrix from potential prior translation
            // Note that you need to take into account potential prior scale of the view into the translation vector!!
            self.transform = self.transform.translatedBy(x: -cTransform.tx / cTransform.a, y: -cTransform.ty / cTransform.d)
    
    
            // Animates the transformation
            let animator = UIViewPropertyAnimator(duration: duration, timingParameters: UICubicTimingParameters(controlPoint1: CGPoint(x: 0, y: 0), controlPoint2: CGPoint(x: 1, y: 1))
            animator.addAnimations {
                self.transform = self.transform.scaledBy(x: scaleFactor.x, y: scaleFactor.y)
            }
            // resets the View to its original center and apply the transformation so that the view stays in the right end position
            animator.addCompletion { (position) in
                if position == UIViewAnimatingPosition.end {
                    self.center = oCenter
                    self.transform = cTransform.scaledBy(x: scaleFactor.x, y: scaleFactor.y)
                }
            }
            animator.startAnimation()
        }
    }
    

    Here the result in animation: move + scale + scale + revert to original

    enter image description here