UILabel animation beyond bounds of UILabel - ios

I have a label and I want a "speedometer" effect; when a new value is assigned to the label's text I'd like the old value to scroll up and the new to come in from below. I am agnostic as to how to achieve this. This is what I am using currently;
CATransition *animation = [CATransition animation];
animation.duration = .2f;
animation.type = kCATransitionPush;
animation.subtype = kCATransitionFromBottom;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.removedOnCompletion = YES;
[mylabel.layer addAnimation:animation
forKey:#"changeTextTransition"];
mylabel.text = #"new text";
This works pretty dang well EXCEPT that the transition at top and bottom goes outside the bounds of the label before fading. I'd like the transition effect to be contained within the bounds of the label.
I have tried to create a mask for the layer, and have the layer mask to bounds, but this had no effect:
UIImageView *maskView = [[UIImageView alloc] initWithImage:labelMask];
// this image is a png that is the same dimensions as the label, and is 100% opaque and colored white
maskView.frame = mylabel.frame;
mylabel.layer.mask = maskView.layer;
mylabel.layer.masksToBounds = YES;
I even tried an alternate method of mask creation:
CALayer *maskLayer = [CALayer new];
maskLayer.frame = mylabel.frame;
maskLayer.contents = (id)(labelMask.CGImage); // same image as above
maskLayer.contentsRect = mylabel.bounds;
mylabel.layer.mask = maskLayer;
mylabel.layer.masksToBounds = YES;
This had no effect.
What else may I try to prevent the layer animation from leaking past the bounds of the UILabel?

You are moving the UILabel's layer itself with animation, and the UILabel's clipsToBounds (UIView)/masksToBounds (CALayer) is limiting the UILabel's layer itself as a whole.
To make this clearer: If you would put something inside the UILabel, or try to render something bigger on the label's layer, it will be clipped by the mask. But if you just moved the label to somewhere else on the parent view - then it would stay whole, as the mask moves with it...
Even when animating, this is the exact same situation. The mask moves with the layer.
So the solution is containing the UILabel within another UIView, which itself has the clipsToBounds set.
This would also allow you to create a subclass of UIView which is a mini-controller, manages its own labels, animates them etc. on demand with simple interface on the UIView subclass. (I guess that this is what you're doing and you just forgot to set clipsToBounds on the container view...)

Related

Animate CALayer mask change with fade effect

I have a UIView, with view.layer.mask set to an instance of CAShapeLayer. The shape layer contains a path, and now I want to add a hole to this shape by adding a second shape with even/odd rule, and fade-in the appearance of this hole.
The problem is that adding to path doesn't seem to be animatable:
[UIView animateWithDuration:2 animations:^() {
CGPathAddPath(parentPath, nil, holePath);
[v.layer.mask didChangeValueForKey:#"path"];
}];
How would I animate this?
After some fiddling, found a workaround:
Create a layer with two sublayers with two desired shapes, and use it as a mask
Animate opacity of the first sublayer (without a hole) from 1 to 0.
This works because child CAShapeLayer instances appear to be used as a union. When you hide the first sublayer without a hole, only the hole will be uncovered, the shared area will not change.
CGMutablePathRef p = CGPathCreateMutable();
// First path
CGPathAddPath(p, nil, outerPath);
CAShapeLayer *mask1 = [CAShapeLayer layer];
mask1.path = p;
// 2nd path with a hole
CGPathAddPath(p, nil, innerPath);
CAShapeLayer *mask2 = [CAShapeLayer layer];
mask2.path = p;
mask2.fillRule = kCAFillRuleEvenOdd;
CGPathRelease(p);
// Create actual mask that hosts two submasks
CALayer *mask = [CALayer layer];
[mask addSublayer:mask1];
[mask addSublayer:mask2];
myView.layer.mask = mask;
mask.frame = v.layer.bounds;
mask1.frame = mask.bounds;
mask2.frame = mask.bounds;
// ...
// Hide mask #1
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"opacity"];
a.fromValue = #1.0;
a.toValue = #0.0;
a.duration = 1.0;
a.fillMode = kCAFillModeForwards; // Don't reset back to original state
a.removedOnCompletion = NO;
[mask1 addAnimation:a forKey:#"hideMask1"];
You can't use UIView animation to animate CALayers.
Most layer property changes do animation by default (implicit animation). As I recall, shape layer path changes are an exception to that.
You'll need to create a CAAnimation object where the property you are animating is the path on your mask layer.
However, that probably won't give the effect you want. The reason is that when you change a path on a shape layer, Core Animation tries to animate the change in shape of the path. Furthermore, path changes only work properly when the starting and ending paths have the same number and type of control points.
I'm not sure how you'd achieve a cross-fade between 2 different masks without a lot of work.
Off the top of my head, the only way I can think of to do this would be to create a snapshot of the new view appearance with the changed mask (probably using Core Image filters) and then do a cross-fade of a layer that displays that snapshot. Once the crossfade is complete, you would install the new path in your mask layer without animation and then remove the snapshot bitmap, revealing the real view underneath.
There might be a simpler way to achieve what you're after but I don't know what that would be. Maybe one of the CA experts that contributes to SO could chime in here.

Shadow in UIView without gradient ?

Does any one know how to achieve the shadow effect with no gradient? Like the screenshot show below
Another concern is the sequence of subviews, i.e the view in front may hide the effect of the view in behind. How to overcome this?
For the first problem you can change the shadowRadius of the shadow, for example:
//You must include QuartzCore framework (#import <QuartzCore/QuartzCore.h>)
view.layer.cornerRadius = 5;
view.layer.shadowRadius = 0; //The shadow should be rendered as a solid shape
view.layer.shadowOffset = CGSizeMake(0, 2);
view.layer.shadowOpacity = 0.5;
view.layer.shadowColor = [UIColor blackColor].CGColor;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:view.bounds];
view.layer.shadowPath = path.CGPath; //This is very important!
Remember to always set the shadowPath! If you don't the performance of rendering the shadow will decrease a lot.
For the second problem, sorry but I don't think there's a way to let the shadow of an object appear over another view that is over the original one.

how can I animate a CAlayer's bounds to progressively reveal an image?

For certain reasons, I'm trying to avoid using a CAScrollLayer to do this. The effect I'm going after is to progressively reveal (from bottom to top) a CALayer's content (a png I previously loaded in). So I thought about doing this:
layer.anchorPoint = CGPointMake(.5, 1);
CABasicAnimation* a = [CABasicAnimation animationWithKeyPath:#"bounds.size.height"];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
a.fillMode = kCAFillModeBoth;
a.removedOnCompletion = NO;
a.duration = 1;
a.fromValue = [NSNumber numberWithFloat:0.];
a.toValue = [NSNumber numberWithFloat:layer.bounds.size.height];
[layer addAnimation:a forKey:nil];
The problem with this is you can tell the layer's content is scaled with the bounds. I was trying for the bounds to change but the content to stay always the original size, so that effectively the bounds clip the image and as I increase bounds.height, the image "Reveals" itself.
Any ideas as to how to pull it off or what might I be missing?
Ok I got it to work, but I basically had to update the layer's frame too, to reflect the change in anchor point:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
layer.contentsGravity = kCAGravityTop;
layer.masksToBounds = YES;
layer.anchorPoint = CGPointMake(.5, 1);
CGRect newFrame = layer.frame;
newFrame.origin.y += newFrame.size.height / 2;
layer.frame = newFrame;
[CATransaction setValue:(id)kCFBooleanFalse forKey:kCATransactionDisableActions];
a.toValue = [NSNumber numberWithFloat:layer.bounds.size.height];
[layer addAnimation:a forKey:nil];
"Dad" has the right answer.
You want to create a CAShapeLayer, and install that as the mask on your layer.
You create a CGPath that is just a rectangle and install that path into the shape layer. The contents of the path determine what areas of the masked layer show up. If the path is a triangle in the middle of the layer, then only the triangle appears.
You then create an animation that animates the path.
To reveal your image from the bottom, you'd set up a path that was a 0 height rectangle at the bottom of the layer, and then you'd create a CAAnimation where the toValue is the same rectangle with a hight of the full layer you want to reveal. The system would generate an animation that reveals the image in a sweep.
You can use this same technique to achieve all kinds of cool effects, like barn doors, venetian blinds, "iris wipes", etc.
What if you changed the clipping mask instead? (or use a mask layer).
You could put another image over the target image and move it up like a stage curtain.

Smoothly rotate and change size of UIView with shadow

I have a UIView with a shadow and a UIImageView subview.
I want to resize the view when the iPad is rotated and I'm trying to do this in the willRotateToInterfaceOrientation callback.
If I set the shadow on the UIView in the basic way the rotation is very choppy; so I'd like some suggestions from others on how to set the shadow setting layer.shadowPath.
I have tried animating the frame size change using [UIView animateWithDuration:animations] and setting the new shadowPath in the same block, but the shadow path snaps to the new size.
And if I don't change the layer's shadowPath in the animations block, it doesn't change.
From a few searches I've done, animating changes to layer properties need to be done with a CABasicAnimation.
So I think the question may be "how do I animate a UIView's frame size and layer change simultaneously?"
There's a bit more code than one would hope but something like this should work.
CGFloat animationDuration = 5.0;
// Create the CABasicAnimation for the shadow
CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:#"shadowPath"];
shadowAnimation.duration = animationDuration;
shadowAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; // Match the easing of the UIView block animation
shadowAnimation.fromValue = (id)self.shadowedView.layer.shadowPath;
// Animate the frame change on the view
[UIView animateWithDuration:animationDuration
delay:0.0f
options:UIViewAnimationCurveEaseInOut
animations:^{
self.shadowedView.frame = CGRectMake(self.shadowedView.frame.origin.x,
self.shadowedView.frame.origin.y,
self.shadowedView.frame.size.width * 2.,
self.shadowedView.frame.size.height * 2);
} completion:nil];
// Set the toValue for the animation to the new frame of the view
shadowAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:self.shadowedView.bounds].CGPath;
// Add the shadow path animation
[self.shadowedView.layer addAnimation:shadowAnimation forKey:#"shadowPath"];
// Set the new shadow path
self.shadowedView.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.shadowedView.bounds].CGPath;

Making a CALayer transparent

Consider the following...
Say I have two CALayer's, one on top of the other. Each layer is the size of the entire iPad screen, the top layer obscures the bottom layer.
Is there some way to mark a portion of the top layer as being "transparent", so that the same section of the bottom layer shows through the transparent portion? In other words, is there a way to "cut out" a portion of the top layer to reveal the bottom layer underneath?
The CALayer mask property. You'll need to subclass CALayer to drawToContext: opaque black over the entire bounds, then do a CGContextClear(ctx, <your see-through box>);
Then create an instance of the layer, give it the same frame as your top-layer bounds, and set it to the mask property.
Yes, you can do it by making different alpha values for each layer,
basically the inner layer(super) one should have at least an alpha value of 0.7 and the outer layer(subLayer) should have less alpha value than its parent, lets say 0.3
Then the outer layer should reveal the inner layer.
But if you wanna do some better revelation, you could draw the outer layer, by setting radial gradient on it.
This is my sample code, but it I haven't drawn the radial gradient for the outer layer.
//
CALayer *innnerLayer = [CALayer layer];
innnerLayer.borderColor = [UIColor greenColor].CGColor;
innnerLayer.borderWidth = 0.8f;
innnerLayer.backgroundColor = [UIColor colorWithWhite:0. alpha:0.5].CGColor;
innnerLayer.frame = CGRectMake(70.0, 150.0f, 100.0f, 100.0f);
CALayer *outLayer = [CALayer layer];
outLayer.frame = innnerLayer.bounds;
outLayer.backgroundColor = [UIColor colorWithWhite:0.f alpha:0.3f].CGColor;
// add outer layer to inner layer
[innnerLayer addSublayer:outLayer];
// add the inner layer to main view
[self.view.layer addSublayer:innnerLayer];
// Experiment with different alpha values, but the outerAlpha
Are you using CALayer as a sublayer of your UIView? You have to set the backgroundColor of your UIView to clear like this:
self.backgroundColor = [UIColor clearColor];
Just setting backgroundColor property to NULL helped me.

Resources