8

I have been using UIPanGestureRecognizer to recognise touches but I want to replace it with a fixed start to end position for my animation. Please see the code below:

panGestureDidMove:

func panGestureDidMove(gesture: UIPanGestureRecognizer) {  
if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {

} else {
    let additionalHeight = max(gesture.translationInView(view).y, 0)

    let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
    let baseHeight = minimalHeight + additionalHeight - waveHeight

    let locationX = gesture.locationInView(gesture.view).x

    layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
    updateShapeLayer()
    }
}

layoutControlPoints:

private func layoutControlPoints(baseHeight baseHeight: CGFloat, waveHeight: CGFloat, locationX: CGFloat) {  
let width = view.bounds.width

let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0)
let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width)

let leftPartWidth = locationX - minLeftX
let rightPartWidth = maxRightX - locationX

l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight)
l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight)
l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
cControlPointView.center = CGPoint(x: locationX , y: baseHeight + waveHeight * 1.36)
r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight)
r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight)
}    

I am trying to replace the panGestureDidMove with CABasicAnimation to animate the start to end position, something like the code below:

let startValue = CGPointMake(70.0, 50.0)
let endValue = CGPointMake(90.0, 150.0)
CATransaction.setDisableActions(true) //Not necessary
view.layer.bounds.size.height = endValue
let positionAnimation = CABasicAnimation(keyPath:"bounds.size.height")
positionAnimation.fromValue = startValue
positionAnimation.toValue = endValue
positionAnimation.duration = 2.0
view.layer.addAnimation(positionAnimation, forKey: "bounds")

A lot of things are affected as the position changes, how can I achieve this?

David Ganster
  • 1,985
  • 1
  • 18
  • 26
Jickery
  • 237
  • 4
  • 15
  • 1
    Apart from `locationX` is there anything within `layoutControlPoints` that would affect the change in position? – ItsMeAgain Mar 11 '16 at 13:37
  • No, but I still just updated the question with the `layoutControlPoints` method code – Jickery Mar 11 '16 at 14:00
  • what exactly is the problem with `CABasicAnimation` you posted? And why don't you use a simply `UIView` animation block where you update the frame or center of the view? – David Ganster Jun 24 '16 at 18:34

1 Answers1

0

If you want fine control of animation, you can use CADisplayLink to do any custom layout or drawing prior to the screens pixel refresh. The run loop attempts to draw 60 frames per second, so with that in mind you can modify your code to simulate touch events.

You'll need to add some properties:

var displayLink:CADisplayLink? // let's us tap into the drawing run loop
var startTime:NSDate? // while let us keep track of how long we've been animating
var deltaX:CGFloat = 0.0 // how much we should update in the x direction between frames
var deltaY:CGFloat = 0.0 // same but in the y direction
var startValue = CGPointMake(70.0, 50.0) // where we want our touch simulation to start
var currentPoint = CGPoint(x:0.0, y:0.0)
let endValue = CGPointMake(90.0, 150.0) // where we want our touch simulation to end

Then whenever we want the animation to run we can call:

    func animate()
    {
        let duration:CGFloat = 2.0
        self.currentPoint = self.startValue
        self.deltaX = (endValue.x - startValue.x) / (duration * 60.0) // 60 frames per second so getting the difference then dividing by the duration in seconds times 60
        self.deltaY = (endValue.y - startValue.y) / (duration * 60.0)
        self.startTime = NSDate()
        self.displayLink = CADisplayLink(target: self, selector: #selector(self.performAnimation))
        self.displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
    }

This will set up the display link and determine my how much our touch simulation should move between frames and begin calling the function that will be called prior to every drawing of the screen performAnimation:

    func performAnimation(){
        if self.startTime?.timeIntervalSinceNow > -2 {
            self.updateViewsFor(self.currentPoint, translation: CGPoint(x:self.currentPoint.x - self.startValue.x, y: self.currentPoint.y - self.startValue.y))
            self.currentPoint.x += self.deltaX
            self.currentPoint.y += self.deltaY
        }
        else
        {
            self.displayLink?.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
            self.currentPoint = self.startValue
        }
    }

Here we check if we are within the duration of the animation. Then call updateViewsFor(point:CGPoint,translation:CGPoint) which is what you had in your gesture target and then update our touch simulation if we're within it, else we just reset our properties.

Finally,

    func updateViewsFor(point:CGPoint,translation:CGPoint)
    {
        let additionalHeight = max(translation.y, 0)

        let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
        let baseHeight = minimalHeight + additionalHeight - waveHeight

        let locationX = point.x

        layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
        updateShapeLayer()
    }

You could also change your panGestureDidMove to:

    @objc func panGestureDidMove(gesture: UIPanGestureRecognizer) {
        if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {

        } else {
            self.updateViewsFor(gesture.locationInView(gesture.view), translation: gesture.translationInView(view))
        }
    }

Edit

There is an easier way of doing this using keyframe animation. But I'm not sure it will animate your updateShapeLayer(). But for animating views we can write a function like:

    func animate(fromPoint:CGPoint, toPoint:CGPoint, duration:NSTimeInterval)
    {            
        // Essentually simulates the beginning of a touch event at our start point and the touch hasn't moved so tranlation is zero
        self.updateViewsFor(fromPoint, translation: CGPointZero)

        // Create our keyframe animation using UIView animation blocks
        UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: .CalculationModeLinear, animations: {

            // We really only have one keyframe which is the end. We want everything in between animated. 
            // So start time zero and relativeDuration 1.0 because we want it to be 100% of the animation
            UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1.0, animations: {

                // we want our "touch" to move to the endValue and the translation will just be the difference between end point and start point in the x and y direction.
                self.updateViewsFor(toPoint, translation: CGPoint(x: toPoint.x - fromPoint.x, y: toPoint.y - fromPoint.y))
            })
            }, completion: { _ in
                // do anything you need done after the animation
        })
    }

This will move the views into place then create a keyframe for where the views end up and animate everything in between. We could call it like:

   override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.animate(CGPointMake(70.0, 50.0), toPoint: CGPointMake(90.0, 150.0), duration: 2.0)
    }
beyowulf
  • 15,101
  • 2
  • 34
  • 40