UIDynamicAnimator Shake with spring effect - ios

I'm trying to replace my core animation shake effect in this code
let shake = CAKeyframeAnimation(keyPath: "position.x")
shake.values = [0, 20, -20, 20, -15, 15, -5, 5, 0]
shake.keyTimes = [0, 1/10.0, 3/10.0, 5/10.0, 6/10.0, 7/10.0, 8/10.0, 9/10.0, 1]
shake.duration = 0.4
shake.additive = true
circlesContainer.layer.addAnimation(shake, forKey: "shakeYourBooty")
with the spring effect by combining UIPushBehavior and UIAttachmentBehavior like this
if origCirclesContainerCenter == nil {
origCirclesContainerCenter = circlesContainer.center
}
shake = UIDynamicAnimator(referenceView: self.view)
let push = UIPushBehavior(items: [circlesContainer], mode: UIPushBehaviorMode.Continuous)
push.pushDirection = CGVectorMake(100.0, 0.0)
print(origCirclesContainerCenter)
let attachement = UIAttachmentBehavior(item: circlesContainer, attachedToAnchor:origCirclesContainerCenter!)
attachement.frequency = 3.0
attachement.damping = 0.5
shake.addBehavior(attachement)
shake.addBehavior(push)
The problem is the attachment behavior doesn't seem to work because the view does not spring back to the original position after being pushed. The animation actually pushes the view position to the right 100 points. origCirclesContainerCenter is the same on every call. Any idea?

I changed the push behavior mode to UIPushBehaviorMode.Instantaneous to fix the problem.

Related

Changing a CALayer contents before animation finished

I work on a UI component that implements flip-card clock animation. All works fine, but when I change a top CALayer contents to new image, the old image stays visible before changeding. It creates confusion effect. For better explanation I place the gif animation bellow:
This is code with changing a CALayer contents:
firstTopLayer.contents = secondTopLayer.contents
let bottomAnim = CABasicAnimation(keyPath: "transform")
bottomAnim.duration = animDuration/2
bottomAnim.repeatCount = 1
bottomAnim.fromValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation((CGFloat)(M_PI_2), 1, 0, 0))
bottomAnim.toValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation(0, 1, 0, 0))
bottomAnim.isRemovedOnCompletion = true
bottomAnim.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionEaseIn)
firstBottomLayer.add(bottomAnim, forKey: "bottom")
firstBottomLayer.contents = self.bufferContents
For more information I place a link to the repository
I found a solution. Top animation must have this configuration
topAnim.fillMode = kCAFillModeForwards
topAnim.isRemovedOnCompletion = false
and after each start this animation.
firstTopLayer.removeAnimation(forKey: kTopAnimaton)
With this configuration the top layer stays in it last frame animation position

x, y position changes while rotating with gesture recognizer IOS

I am making an IOS App in which I am rotating an imageView. Now, All things are complete. But when the rotation is completed. x, y, of that image View is changing .. How to resolve it . I am attaching code and screenshots so that you can easily find my problem.
let i = defaults.integerForKey("myown")
let g = nViews[i].frame
imageViews[i].transform = CGAffineTransformRotate(imageViews[i].transform, recognizer.rotation)
// nViews[i].transform = CGAffineTransformRotate(imageViews[i].transform, recognizer.rotation)
//
recognizer.rotation = 0
if recognizer.state == UIGestureRecognizerState.Ended
{
let f = imageViews[i].frame
// print(f.size.width) -->111.576689146311
// print(f.size.height) -->111.576689146311
// nViews[i].frame = f
nButtons[i].frame = CGRect(x: f.origin.x - 10, y: f.origin.y - 10, width: 25, height: 25)
// imageView.addSubview(nViews[i])
imageView.addSubview(nButtons[i])
}
Now the another view which is on that imageView become rectangular because of that changes.. As shown in screenshot first time when imageView is not rotated the view is perfect but when it rotates view becomes rectangular.. Hope you will understand problem now
Try to use bounds instead of frame: let g = nViews[i].bounds and let f = imageViews[i].bounds. See When to Use Bound and When to use Frame

Save CoreAnimation sequence frame by frame

How can I save each frame of a CoreAnimation based animation (as image files)?
Here's my little playground scene. The animation lasts 2.4 seconds.
let stage = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300, height: 300))
stage.backgroundColor = UIColor.blueColor();
var dot = UIView(frame: CGRectMake(0, 0, 10, 10))
dot.backgroundColor = UIColor.redColor()
dot.center = stage.center
UIView.animateWithDuration(2.4, animations: { () -> Void in
dot.center.y = dot.center.y + 50
})
I know how to save a static UIView frame as PDF but I am not sure how to hook into the animation sequence while it happens and capture/save the view frame by frame.
Option 1
As far as I see it I need to hook into the animation block and save the stage view for each frame (at a given frame rate?). How can I do this? Is there some sort of callback I can use?
Option 2
While looking for a solution I came across another option that looks even more promising (based on this question and this awesome blog post). Using a CBAnimation, setting a timingFunction and then setting the timeOffset to the progress states I want to render.
Here's an example for the frame at 50% progress (using a different example here).
var drawAnimation = CABasicAnimation(keyPath: "strokeEnd")
…
drawAnimation.fromValue = NSNumber(float: 0.0)
drawAnimation.toValue = NSNumber(float: 1.0)
drawAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
circle.addAnimation(drawAnimation, forKey: "drawCircleAnimation")
circle.speed = 0.0;
circle.timeOffset = 0.5
The problem is that I cannot capture this timeOffset based animation state. When I render the view I only get the final state instead of the frozen animation frame at 0.5.
Any help to resolve this is appreciated.

ios animating already transformed view

I have a collection view with a collection of images (which are views). The layout causes transformations to occur on every visible item. The item at the center is both zoomed and translated while the others are just translated.
The point is, the layout is setting the layer's transform property for each item.
Later, when the user touches an item, I want to animate the item using a keyframe animation.
The behavior I am experiencing is that the item seems to revert back to its untransformed state, the animation occurs on its untransformed state and then the item's layout transformation returns.
Why?
I've tried using the view's transform property, the backing layer's affine transform property, and the backing layer's 3D transform property all with this same behavior. The keyframe animation uses 3D transforms.
I clearly have a gap in my understanding of transforms and animations, though I have seen this question posed for platforms other than iOS without answers.
Layout transformation code (in a flow layout subclass):
// Calculate the distance from the center of the visible rect to the center of the attributes.
// Then normalize it so we can compare them all. This way, all items further away than the
// active get the same transform.
let distanceFromVisibleRectToItem = CGRectGetMidX(visibleRect) - attributes.center.x
let normalizedDistance = distanceFromVisibleRectToItem / ACTIVE_DISTANCE
let isLeft = distanceFromVisibleRectToItem > 0
var transform = CATransform3DIdentity
var maskAlpha: CGFloat = 0.0
if (abs(distanceFromVisibleRectToItem) < ACTIVE_DISTANCE) {
// We're close enough to apply the transform in relation to how far away from the center we are.
transform = CATransform3DTranslate(transform,
(isLeft ? -FLOW_OFFSET : FLOW_OFFSET) * abs(distanceFromVisibleRectToItem/TRANSLATE_DISTANCE),
0, (1 - abs(normalizedDistance)) * 40000 + (isLeft ? 200 : 0))
// Set the zoom factor.
let zoom = 1 + ZOOM_FACTOR * (1 - abs(normalizedDistance))
transform = CATransform3DScale(transform, zoom, zoom, 1)
attributes.zIndex = 1
let ratioToCenter = (ACTIVE_DISTANCE - abs(distanceFromVisibleRectToItem)) / ACTIVE_DISTANCE
// Interpolate between 0.0f and INACTIVE_GREY_VALUE
maskAlpha = INACTIVE_GREY_VALUE + ratioToCenter * (-INACTIVE_GREY_VALUE)
} else {
// We're too far away - just apply a standard perspective transform.
transform = CATransform3DTranslate(transform, isLeft ? -FLOW_OFFSET : FLOW_OFFSET, 0, 0)
attributes.zIndex = 0
maskAlpha = INACTIVE_GREY_VALUE
}
attributes.transform3D = transform
Animation code (note this is a Swift extension to the UIView class):
func bounceView(completion: (() -> Void)? = nil) {
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.addAnimation(animation, forKey: "bounce")
}
func bounceAnimation(itemHeight: CGFloat) -> CAKeyframeAnimation {
let factors: [CGFloat] = [0, 32, 60, 83, 100, 114, 124, 128, 128, 124, 114, 100, 83, 60, 32,
0, 24, 42, 54, 62, 64, 62, 54, 42, 24, 0, 18, 28, 32, 28, 18, 0]
var transforms: [AnyObject] = [NSValue(CATransform3D: self.layer.transform)]
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
let transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transforms.append(NSValue(CATransform3D: transform))
}
let animation = CAKeyframeAnimation(keyPath: "transform")
animation.repeatCount = 1
animation.duration = CFTimeInterval(factors.count)/30.0
animation.fillMode = kCAFillModeForwards
animation.values = transforms
animation.removedOnCompletion = true // final stage is equal to starting stage
animation.autoreverses = false
return animation;
}
Note: I should state more simply that I want this process to begin and end in the collection view layout transformed state. I want the animation to occur on the layout transformed state as well (i.e. no reverting to the untransformed state at all).
It seems that in your animations you are not syncing your model with your presentation layer.
When you animate with Core Animation (CABasicAnimation, for instance) it only changes the presentation layer (the thing you see on the screen) but in fact the state your layers have is different from the visible one.
(You can read more about that in this objc.io issue)
So basically to fix this you need to update your model in your CAAnimation. You need to set a from and toValue, and then, after you set those, you update your transform so it can match the final state of your presentation layer.
EDIT
Now that we have some code I can be concrete
To sync the model layer to the presentation layer you need to set your model to the final state before adding the animation:
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.transform = CATransform3DMakeTranslation(0, 0, 0)
layer.addAnimation(animation, forKey: "bounce")
David Rönnqvist has a good rant about this issue in this gist.
SOLUTION:
The help Tiago Almeida offered was very useful to get me thinking about what the animation was actually doing (how it was actually working). My basic assumption that the animation would begin with the current model layer transform was incorrect. As Tiago points out, the presentation layer is a separate and distinct layer from the model layer.
Once I got that through my thick head, I realized that I had to manually concatenate the current model transform with the transform being built for the animation frame and viola! Things work as expected.
The updated animation code:
var transforms: [AnyObject] = []
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
var transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transform = CATransform3DConcat(self.layer.transform, transform)
transforms.append(NSValue(CATransform3D: transform))
}

CGAffineTransform scale and translation - jump before animation

I am struggling with an issue regarding CGAffineTransform scale and translation where when I set a transform in an animation block on a view that already has a transform the view jumps a bit before animating.
Example:
// somewhere in view did load or during initialization
var view = UIView()
view.frame = CGRectMake(0,0,100,100)
var scale = CGAffineTransformMakeScale(0.8,0.8)
var translation = CGAffineTransformMakeTranslation(100,100)
var concat = CGAffineTransformConcat(translation, scale)
view.transform = transform
// called sometime later
func buttonPressed() {
var secondScale = CGAffineTransformMakeScale(0.6,0.6)
var secondTranslation = CGAffineTransformMakeTranslation(150,300)
var secondConcat = CGAffineTransformConcat(secondTranslation, secondScale)
UIView.animateWithDuration(0.5, animations: { () -> Void in
view.transform = secondConcat
})
}
Now when buttonPressed() is called the view jumps to the top left about 10 pixels before starting to animate. I only witnessed this issue with a concat transform, using only a translation transform works fine.
Edit: Since I've done a lot of research regarding the matter I think I should mention that this issue appears regardless of whether or not auto layout is turned on
I ran into the same issue, but couldn't find the exact source of the problem. The jump seems to appear only in very specific conditions: If the view animates from a transform t1 to a transform t2 and both transforms are a combination of a scale and a translation (that's exactly your case). Given the following workaround, which doesn't make sense to me, I assume it's a bug in Core Animation.
First, I tried using CATransform3D instead of CGAffineTransform.
Old code:
var transform = CGAffineTransformIdentity
transform = CGAffineTransformScale(transform, 1.1, 1.1)
transform = CGAffineTransformTranslate(transform, 10, 10)
view.layer.setAffineTransform(transform)
New code:
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 1.1, 1.1, 1.0)
transform = CATransform3DTranslate(transform, 10, 10, 0)
view.layer.transform = transform
The new code should be equivalent to the old one (the fourth parameter is set to 1.0 or 0 so that there is no scaling/translation in z direction), and in fact it shows the same jumping. However, here comes the black magic: In the scale transformation, change the z parameter to anything different from 1.0, like this:
transform = CATransform3DScale(transform, 1.1, 1.1, 1.01)
This parameter should have no effect, but now the jump is gone.
🎩✨
Looks like Apple UIView animation internal bug. When Apple interpolates CGAffineTransform changes between two values to create animation it should do following steps:
Extract translation, scale, and rotation
Interpolate extracted values form start to end
Assemble CGAffineTransform for each interpolation step
Assembling should be in following order:
Translation
Scaling
Rotation
But looks like Apple make translation after scaling and rotation. This bug should be fixed by Apple.
I dont know why, but this code can work
update:
I successfully combine scale, translate, and rotation together, from any transform state to any new transform state.
I think the transform is reinterpreted at the start of the animation.
the anchor of start transform is considered in new transform, and then we convert it to old transform.
self.v = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50))
self.v?.backgroundColor = .blue
self.view.addSubview(v!)
func buttonPressed() {
let view = self.v!
let m1 = view.transform
let tempScale = CGFloat(arc4random()%10)/10 + 1.0
let tempRotae:CGFloat = 1
let m2 = m1.translatedBy(x: CGFloat(arc4random()%30), y: CGFloat(arc4random()%30)).scaledBy(x: tempScale, y: tempScale).rotated(by:tempRotae)
self.animationViewToNewTransform(view: view, newTranform: m2)
}
func animationViewToNewTransform(view: UIView, newTranform: CGAffineTransform) {
// 1. pointInView.apply(view.transform) is not correct point.
// the real matrix is mAnchorToOrigin.inverted().concatenating(m1).concatenating(mAnchorToOrigin)
// 2. animation begin trasform is relative to final transform in final transform coordinate
// anchor and mAnchor
let normalizedAnchor0 = view.layer.anchorPoint
let anchor0 = CGPoint(x: normalizedAnchor0.x * view.bounds.width, y: normalizedAnchor0.y * view.bounds.height)
let mAnchor0 = CGAffineTransform.identity.translatedBy(x: anchor0.x, y: anchor0.y)
// 0->1->2
//let origin = CGPoint(x: 0, y: 0)
//let m0 = CGAffineTransform.identity
let m1 = view.transform
let m2 = newTranform
// rotate and scale relative to anchor, not to origin
let matrix1 = mAnchor0.inverted().concatenating(m1).concatenating(mAnchor0)
let matrix2 = mAnchor0.inverted().concatenating(m2).concatenating(mAnchor0)
let anchor1 = anchor0.applying(matrix1)
let mAnchor1 = CGAffineTransform.identity.translatedBy(x: anchor1.x, y: anchor1.y)
let anchor2 = anchor0.applying(matrix2)
let txty2 = CGPoint(x: anchor2.x - anchor0.x, y: anchor2.y - anchor0.y)
let txty2plusAnchor2 = CGPoint(x: txty2.x + anchor2.x, y: txty2.y + anchor2.y)
let anchor1InM2System = anchor1.applying(matrix2.inverted()).applying(mAnchor0.inverted())
let txty2ToM0System = txty2plusAnchor2.applying(matrix2.inverted()).applying(mAnchor0.inverted())
let txty2ToM1System = txty2ToM0System.applying(mAnchor0).applying(matrix1).applying(mAnchor1.inverted())
var m1New = m1
m1New.tx = txty2ToM1System.x + anchor1InM2System.x
m1New.ty = txty2ToM1System.y + anchor1InM2System.y
view.transform = m1New
UIView.animate(withDuration: 1.4) {
view.transform = m2
}
}
I also try the zScale solution, it seems also work if set zScale non-1 at the first transform or at every transform
let oldTransform = view.layer.transform
let tempScale = CGFloat(arc4random()%10)/10 + 1.0
var newTransform = CATransform3DScale(oldTransform, tempScale, tempScale, 1.01)
newTransform = CATransform3DTranslate(newTransform, CGFloat(arc4random()%30), CGFloat(arc4random()%30), 0)
newTransform = CATransform3DRotate(newTransform, 1, 0, 0, 1)
UIView.animate(withDuration: 1.4) {
view.layer.transform = newTransform
}
Instead of CGAffineTransformMakeScale() and CGAffineTransformMakeTranslation(), which create a transform based off of CGAffineTransformIdentity (basically no transform), you want to scale and translate based on the view's current transform using CGAffineTransformScale() and CGAffineTransformTranslate(), which start with the existing transform.
The source of the issue is the lack of perspective information to the transform.
You can add perspective information modifying the m34 property of your 3d transform
var transform = CATransform3DIdentity
transform.m34 = 1.0 / 200 //your own perspective value here
transform = CATransform3DScale(transform, 1.1, 1.1, 1.0)
transform = CATransform3DTranslate(transform, 10, 10, 0)
view.layer.transform = transform

Resources