David Seek
David Seek

Reputation: 17132

How to properly transform UIView's scale on UIScrollView movement

To have a similar effect to Snapchat's HUD movement, I have created a movement of the HUD elements based on UIScollView's contentOffset. Edit: Link to the Github project.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.view.layoutIfNeeded()

    let factor = scrollView.contentOffset.y / self.view.frame.height

    self.transformElements(self.playButton,
                           0.45 + 0.55 * factor, // 0.45 = desired scale + 0.55 = 1.0 == original scale
                           Roots.screenSize.height - 280, // 280 == original Y
                           Roots.screenSize.height - 84, // 84 == minimum desired Y
                           factor)
}

func transformElements(_ element: UIView?,
                       _ scale: CGFloat,
                       _ originY: CGFloat,
                       _ desiredY: CGFloat,
                       _ factor: CGFloat) {
    if let e = element {
        e.transform = CGAffineTransform(scaleX: scale, y: scale) // this line lagging

        let resultY = desiredY + (originY - desiredY) * factor
        var frame = e.frame
        frame.origin.y = resultY
        e.frame = frame
    }
}

With this code implemented the scroll as well as the transition appeared to be "laggy"/not smooth. (Physical iPhone 6S+ and 7+).

Deleting the following line: e.transform = CGAffineTransform(scaleX: scale, y: scale) erased the issue. The scroll as well as the Y-movement of the UIView object is smooth again.

What's the best approach to transform the scale of an object?

enter image description here

There are no Layout Constraints.

func setupPlayButton() {
        let rect = CGRect(x: Roots.screenSize.width / 2 - 60,
                          y: Roots.screenSize.height - 280,
                          width: 120,
                          height: 120)
        self.playButton = UIButton(frame: rect)
        self.playButton.setImage(UIImage(named: "playBtn")?.withRenderingMode(.alwaysTemplate), for: .normal)
        self.playButton.tintColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        self.view.addSubview(playButton)
}

Upvotes: 3

Views: 2954

Answers (2)

Aruna Mudnoor
Aruna Mudnoor

Reputation: 4825

This is happening because you are applying both: transform and frame. It will be smoother, if you apply only transform. Update your transformElements function as below:

    func transformElements(_ element: UIView?,
                       _ scale: CGFloat,
                       _ originY: CGFloat,
                       _ desiredY: CGFloat,
                       _ factor: CGFloat) {
    if let e = element {
        e.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 0, y: desiredY * (1 - factor))

    }
}

Upvotes: 1

Jon Rose
Jon Rose

Reputation: 8563

You can make these kinds of animation smoother by creating an animation then setting the speed of the layer to 0 and then changing the timeOffset of the layer.

first add the animation in the setupPlayButton method

let animation = CABasicAnimation.init(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 0.45
animation.duration = 1.0
//Set the speed of the layer to 0 so it doesn't animate until we tell it to
self.playButton.layer.speed = 0.0;
self.playButton.layer.add(animation, forKey: "transform");

next in the scrollViewDidScroll change the timeOffset of the layer and move the center of the button.

if let btn =  self.playButton{
    var factor:CGFloat = 1.0
    if isVertically {
        factor = scrollView.contentOffset.y / self.view.frame.height
    } else {
        factor = scrollView.contentOffset.x / Roots.screenSize.width
        var transformedFractionalPage: CGFloat = 0

        if factor > 1 {
           transformedFractionalPage = 2 - factor
        } else {
            transformedFractionalPage = factor
        }
        factor = transformedFractionalPage;
    }
    //This will change the size
    let timeOffset = CFTimeInterval(1-factor)
    btn.layer.timeOffset = timeOffset

    //now change the positions.  only use center - not frame - so you don't mess up the animation.  These numbers aren't right I don't know why
    let desiredY = Roots.screenSize.height - (280-60);
    let originY = Roots.screenSize.height - (84-60);
    let resultY = desiredY + (originY - desiredY) * (1-factor)
    btn.center = CGPoint.init(x: btn.center.x, y: resultY);
}

I couldn't quite get the position of the button correct - so something is wrong with my math there, but I trust you can fix it.

If you want more info about this technique see here: http://ronnqvi.st/controlling-animation-timing/

Upvotes: 1

Related Questions