I have a UIImageView that I want to scale its outter edges towards the center (the top and bottom edges to be precise). The best I am able to get so far is just one edge collapsing into the other (which doesn't move). I want both edges to collapse towards the middle.
I have tried setting the the anchor point to 0.5,0.5 but that doesnt address the issue (nothing changes).
Is there a way to setup an ImageView such that it will scale inwards towards the middle so the outter edges collapse towards the center?
Here was my best attempt:
override public func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
var uimg = UIImage(named: "img.PNG")!
var img:UIImageView = UIImageView(image: uimg)
img.layer.anchorPoint = CGPointMake(0.5, 0.5)
view.addSubview(img)
UIView.animateWithDuration(5, animations:
{ () -> Void in
img.frame = CGRectMake(img.frame.origin.x,img.frame.origin.y,img.frame.width, 0)
})
{ (finished) -> Void in
//
}
}
You can use scaling using UIView's transform property. It seems like scaling does not allow you to make the value 0 and it goes to the final value without any animation. So, you could basically make the scaling y value negligible like 0.001.
imageView.transform = CGAffineTransformMakeScale(1, 0.001)
You could also use core animation to scale, using CABasicAnimation, this would be more simpler as this,
let animation = CABasicAnimation(keyPath: "transform.scale.y")
animation.toValue = 0
animation.duration = 2.0
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
imageView.layer.addAnimation(animation, forKey: "anim")
Or you can also use the approach you have been using, simply setting the frame,
var finalFrame = imageView.frame
finalFrame.size.height = 0
finalFrame.origin.y = imageView.frame.origin.y + (imageView.frame.size.height / 2)
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.imageView.frame = finalFrame
})
Your new frame has the origin in the same place. You need to move the origin down, at the same time as you reduce the height:
img.frame = CGRectMake(img.frame.origin.x, img.frame.origin.y + (img.frame.size.height/2), img.frame.size.width, 0);
Related
I just want to get some ideas on how to approach the situation I have in hand.
I have created some images which I want to animate, but the animation I want is not default given, so its giving me some hard time.
The animation I want is like this: https://i.stack.imgur.com/byGMC.gif (not from bottom of the screen, from the bottom of images' own frame)
When a button is pressed I want the button/image start appear from bottom of its frame to the top, like the wipe animation in powerpoint :)
So naturaly it will default as hidden and when a button is pressed it will animate in.
I did try a few methods but non of them is actually doing the job I want.
Like this one:
extension UIView{
func animShow(){
UIView.animate(withDuration: 2, delay: 5, options: [.transitionFlipFromBottom],
animations: {
self.center.y -= self.bounds.height
self.layoutIfNeeded()
}, completion: nil)
self.isHidden = false
}
func animHide(){
UIView.animate(withDuration: 1, delay: 0, options: [.curveLinear],
animations: {
self.center.y += self.bounds.height
self.layoutIfNeeded()
}, completion: {(_ completed: Bool) -> Void in
self.isHidden = true
})
}
}
so it slides in the image but this is not what I want, you may try the code, just add this and write image.animShow() in viewDidLoad, button is the button which I want to animate.
I appreciate every bit of help, and I am a newbie in swift programming
Thank you.
There are various ways to do a "soft wipe" animation... here is one.
We can add a gradient layer mask and then animate the gradient.
When using a layer mask, clear (alpha 0) areas hide what's under the mask, and opaque (alpha 1) areas show what's under the mask. This includes parts of the mask that are translucent - alpha 0.5 for example - so the content becomes "translucent".
To get a "soft wipe" we want the gradient to use only a portion of the frame, so if we set the starting Y point to 0.5 (halfway down), for example, we can set the ending Y point to 0.6 ... this would give us a horizontal "gradient band".
So, a yellow-to-red gradient at 0.5 to 0.6 would look like this:
If we set the starting Y to 1.0 and the ending Y to 1.1, the gradient band will start "off the bottom of the view" ... and we can animate it up until it's "off the top of the view" (note that converted to gif loses some of the smooth gradient property):
Now, if we instead use clear-to-red and set it as a layer mask, using the same animation it will "soft wipe" the view, it will look something like this:
Can't get the animated gif small enough to post here, so here's a link to the animation: https://i.imgur.com/jo2DH3Z.mp4
Here's some sample code for you to play with. Lots of comments in there:
class AnimatedGradientImageView: UIImageView {
let maskLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// vertical gradient
// start at bottom
maskLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
// to bottom + 1/10th
maskLayer.endPoint = CGPoint(x: 0.5, y: 1.1)
// use "if true" to see the layer itself
// use "if false" to see the image reveal
if false {
// yellow to red
maskLayer.colors = [UIColor.yellow.cgColor, UIColor.red.cgColor]
// add it as a sublayer
layer.addSublayer(maskLayer)
} else {
// clear to red
maskLayer.colors = [UIColor.clear.cgColor, UIColor.red.cgColor]
// set the mask
layer.mask = maskLayer
}
}
override func layoutSubviews() {
super.layoutSubviews()
// set mask layer frame
maskLayer.frame = bounds
}
func reveal() -> Void {
let anim1: CABasicAnimation = CABasicAnimation(keyPath: "startPoint.y")
// anim1 animates the gradient start point Y
// to -0.1 (1/10th above the top of the view)
anim1.toValue = -0.1
anim1.duration = 1.0
anim1.isRemovedOnCompletion = false
anim1.fillMode = .forwards
let anim2: CABasicAnimation = CABasicAnimation(keyPath: "endPoint.y")
// anim2 animates the gradient end point Y
// to 0.0 (the top of the view)
anim2.toValue = 0.0
anim2.duration = 1.0
anim2.isRemovedOnCompletion = false
anim2.fillMode = .forwards
maskLayer.add(anim1, forKey: nil)
maskLayer.add(anim2, forKey: nil)
}
}
class AnimatedGradientViewController: UIViewController {
let testImageView = AnimatedGradientImageView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
// replace with your image name
guard let img = UIImage(named: "sampleImage") else {
fatalError("Could not load image!!!")
}
// set the image
testImageView.image = img
testImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testImageView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// size: 240 x 240
testImageView.widthAnchor.constraint(equalToConstant: 240.0),
testImageView.heightAnchor.constraint(equalToConstant: 240.0),
// centered
testImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// tap anywhere in the view
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
#objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
testImageView.reveal()
}
}
First, you will have to give the height constraint of the image from the storyboard and set the height to 0
and take the image height constraint into your file like below
#IBOutlet weak var imageHeight: NSLayoutConstraint!
Here is the animation code.
UIView.animate(withDuration: 2, delay: 2, options: [.transitionFlipFromBottom],
animations: {
self.image.frame = CGRect(x: self.image.frame.origin.x, y: self.image.frame.origin.y - 100, width: self.image.frame.size.width, height: 100)
self.imageHeight.constant = 100
self.image.layoutIfNeeded()
}, completion: nil)
Click here to see animation
Using this code:
UIView.animate(withDuration: 0.5, delay: 0, options: [.repeat], animations: {
self.star.transform = self.star.transform.rotated(by: CGFloat(M_PI_2))
})
My view is doing this:
Using this code:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2)
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount=Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
My view is doing this:
Well both are not doing what I want. The view that is rotating is an UIImageView with scale to fill. I want the image to stay exactly in the middle. How can I accomplish that? The functions are executed in viewDidAppear. The last gif looks way better, but notice the star is not perfectly centered... This is the image.
Problem
The center of your image is not the center of your star.
Solutions
There are two possible solutions
Edit the image so that center of the star is at the center of the image.
Set the anchor of the rotating layer to the center of the star (x: 1004px, y: 761px).
Using this code:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = Int(0)
let multiplicand = multiplier * 2
rotation!.toValue = Int(Double(multiplicand) * .pi)
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
}
I get the animation I want. (Either a wheel spins, or a cell rotates fast enough in the opposite direction to always remain exactly right side up).
However, when the 30 seconds (duration) is up, there is a flicker as the view jumps back to how it looked before the animation.
I understand it is supposed to work this way.
How do I apply the rotation to the "before" image so that when the duration expires I don't see any cells jump?
Increasing the duration of the animation slows the wheel's spin, so that is not an appropriate solution.
If #22521690 applies, I don't understand how - I do not have an explicit CATransaction.
Try
rotation!.toValue = Double(multiplicand) * .pi
instead of
rotation!.toValue = Int(Double(multiplicand) * .pi)
The issue is with the radian precision which is lost due to Int conversion.
On the next line after applying the animation to the layer, set the property you're animating to its ending value.
Because an animation is "in-flight", the normal display of the layer is covered the presentation layer, a special animation layer.
Once the animation is complete, the presentation layer is hidden/removed, and the actual layer is exposed. If it is a the same state as the presentation layer at the end of the animation then there's no jump.
That code might look like this:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = 0.0
let multiplicand = multiplier * 2
let finalAngle = Double(multiplicand) * .pi
rotation!.toValue = finalAngle
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
//-------------------
//Set the final transform on the layer to the final rotation,
//but without animation
CATransaction.begin()
CATransaction.setDisableActions(true)
let finalTransform = CATransform3DMakeRotation(CGFloat(finalAngle), 0.0, 0.0, 1.0)
aView.layer.transform = finalTransform
CATransaction.commit()
//-------------------
}
I am creating an animation that I want to use when the app is retrieving some data online. The idea is that I have some dots in a row, they will be scaled smaller that their original sizes then return to their original size and all of this with a small delay between each scaling. The animation is repeated and use auto-reverse mode.
To do that I create some dots using a core graphic method, add them to a view and position them using a CGAffineTransformMakeTranslation transformation. Then I use a loop to animate them one by one with a delay and I use a CGAffineTransformScale transformation for scaling.
Problem: I don't get the expected animation (at least what I'm expecting). When the dots are being scaled, they also move back to their original position.
Can someone enlighten me why there is a translate transformation while in the UIView animation, I'm only specifying a scaling?
Here is the code:
private var dots = [UIImage]()
public init(numberOfDots: Int, hexaColor: Int, dotDiameter: CGFloat = 30, animationDuration: NSTimeInterval = 1) {
self.dotDiameter = dotDiameter
self.animationDuration = animationDuration
for _ in 0 ..< numberOfDots {
dots.append(GraphicHelper.drawDisk(hexaColor, rectForDisk: CGRect(x: 0, y: 0, width: dotDiameter, height: dotDiameter), withStroke: false))
}
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let viewWidth: CGFloat = CGFloat(numberOfDots) * dotDiameter + CGFloat(numberOfDots - 1) * spaceBetweenDisks
super.init(frame: CGRectMake(0, 0, viewWidth, dotDiameter))
setup()
}
private func setup() {
for (i, dot) in dots.enumerate() {
let dotImageView = UIImageView(image: dot)
addSubview(dotImageView)
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let xOffset = CGFloat(i) * (dotDiameter + spaceBetweenDisks)
dotImageView.transform = CGAffineTransformMakeTranslation(xOffset, 0)
}
}
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
EDIT:
I found a fix but I don't understand how come it's fixing it. So if anyone can explain.
I added these 2 lines:
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
before this line in startAnimation:
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
Combining translate and scale transforms is confusing and hard to get right.
I have to spend far too much time with graph paper and deep thought in order to figure it out, and I'm too tired for that right now.
Don't do that. Place your dot image views by moving their center coordinates, and leave the transform at identity. Then when you scale them they should scale in place like you want.
Note that if you want them to move and scale at the same time you can both alter the view's center property and it's transform scale in the same animateWithDuration call and it works correctly. (Not so with changing the frame by the way. If you change the transform then the frame property doesn't work correctly any more. Apple's docs say that the results of reading/writing the frame property of a view with a non-identity transform are "undefined".)
Are you sure its going back to its original position and not scaling based on the original center point instead? Try changing the order of applying transforms by doing this:
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
dotImageView.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
From apple docs:
Note that matrix operations are not commutative—the order in which you concatenate matrices is important. That is, the result of multiplying matrix t1 by matrix t2 does not necessarily equal the result of multiplying matrix t2 by matrix t1.
So, keep in mind that assigning a transformation creates a new affine transformation matrix, and concatenation will modify the existing matrix with the new one - the order you apply these in can create different results.
To make this work, I also updated the value of your translation on dotImageView. It needs to be requiredTranslation / scale.. if applying the translation before the scale. So in your viewDidLoad:
dotImageViewtransform = CGAffineTransformMakeTranslation(1000, 0)
And then the animation:
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
self.card.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)
Title sort of explains it all. I have a SKSpriteNode, called bar. It's assigned an image that is a red longways rectangle. I want it to slowly decrease in length over an interval, while keeping the width the same. In other words, imagine it sort of folding in on itself vertically.
You can use SKActions to do that. SKAction.scaleYTo function can be use to scale the height of the sprite. The anchor point can be shifted to one edge to prevent the rectangle from scaling towards the middle.
var sprite = SKSpriteNode(imageNamed: "redBar.png")
sprite.anchorPoint = CGPointMake(0, 0)
sprite.position = CGPointMake(95, 100)
self.addChild(sprite)
let duration = 10.0
let finalHeightScale:CGFloat = 0.0
let scaleHeightAction = SKAction.scaleYTo(finalHeightScale, duration: duration)
sprite.runAction(scaleHeightAction, completion: { () -> Void in
println("Height is zero")
})