I have created 2 CALayers of same size and am passing these to the method below. However, the two layers runs together. How can I seperate these so that both are visible?
- (void) myAnimation : (CALayer *) sublayer {
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 100, 270, 270)];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = aPath.CGPath;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =35.0;
[sublayer addAnimation:anim forKey:#"race"];
}
Your path begins and ends at the same point. Assuming both beginning times are the same and duration is the same, your layers will precisely overlap. You can either shift the beginning time or rotate your UIBezierPath* aPath Here's an example of rotating your UIBezierPath* aPath and altering the duration. It should give you an idea how you could alter duration, begin time, rotation, etc.
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor blackColor]];
CALayer * layer1 = [CALayer layer];
CALayer * layer2 = [CALayer layer];
[layer1 setFrame:CGRectMake(0, 0, 5, 50)];
[layer2 setFrame:CGRectMake(0, 0, 5, 100)];
layer1.backgroundColor = [[UIColor colorWithRed:.1 green:.5 blue:1 alpha:.35] CGColor];
layer2.backgroundColor = [[UIColor colorWithRed:.9 green:.2 blue:.5 alpha:.35] CGColor];
[self.view.layer addSublayer:layer1];
[self.view.layer addSublayer:layer2];
[self myAnimation:layer1 andRotation:0 andDuration:35.0];
[self myAnimation:layer2 andRotation:10 andDuration:10.0];
}
- (void) myAnimation:(CALayer *)sublayer andRotation:(CGFloat)rot andDuration:(CFTimeInterval)dur {
CGRect rect = CGRectMake(30, 100, 270, 270);
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:rect];
// Creating a center point around which we will transform the path
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, center.x, center.y);
transform = CGAffineTransformRotate(transform, rot);
// Recenter the transform
transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
// This is the new path we will use.
CGPathRef rotated = CGPathCreateCopyByTransformingPath(aPath.CGPath, &transform);
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = rotated;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =dur;
[sublayer addAnimation:anim forKey:#"race"];
}
I am trying to create a view animation similar to facebook's expanding X when removing a message in the iPhone version. The problem I am facing right now is, I am animating the parent view's bounds using core animation and the resulting subviews animations are animating with it. Ultimately what I want is the parent view to animate and the subview to remain unanimated, in the center of it's parent's view. Below is the code I am using.
#interface FCXView : UIView
#end
#implementation FCXView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.layer.opacity = 1.0f;
self.alpha = 1.0f;
self.autoresizingMask = UIViewAutoresizingNone;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat lineWidth2 = 1.0f;
CGContextSetLineWidth(context, lineWidth2);
CGFloat red[4] = {1.0f, 0.0f, 0.0f, 0.9f};
CGContextSetStrokeColor(context, red);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 17.0f, 15.0f);
CGContextAddLineToPoint(context, 28.0f, 30.0f);
CGContextStrokePath(context);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 28.0f, 15.0f);
CGContextAddLineToPoint(context, 17.0f, 30.0f);
CGContextStrokePath(context);
CGContextFillPath(context);
}
#end
#interface FCRoundView ()
#end
#implementation FCRoundView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
self.layer.opacity = 1.0f;
self.userInteractionEnabled = YES;
self.multipleTouchEnabled = NO;
self.autoresizesSubviews = NO;
[self setupXView];
}
return self;
}
- (void)setupXView
{
// Load X
FCXView *xView = [[FCXView alloc] initWithFrame:CGRectMake(0, 0, 45, 45)];
[self addSubview:xView];
}
- (void)drawRect:(CGRect)rect
{
CGFloat lineWidth = 5;
CGRect borderRect = CGRectInset(rect, lineWidth * 0.5, lineWidth * 0.5);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextSetRGBFillColor(context, 175.0/255.0, 175.0/255.0, 175.0/255.0, 0.6);
CGContextSetLineWidth(context, 5.0);
CGContextFillEllipseInRect (context, borderRect);
CGContextStrokeEllipseInRect(context, borderRect);
}
#pragma mark - Handle touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.bounds.size.height == 45) {
SKBounceAnimation *bounceAnimation = [SKBounceAnimation animationWithKeyPath:#"bounds"];
bounceAnimation.fromValue = [NSValue valueWithCGRect:self.frame];
bounceAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 75, 75)];
bounceAnimation.duration = 0.5f;
bounceAnimation.numberOfBounces = 4;
bounceAnimation.delegate = self;
bounceAnimation.removedOnCompletion = NO;
bounceAnimation.fillMode = kCAFillModeForwards;
bounceAnimation.shouldOvershoot = YES;
[self.layer addAnimation:bounceAnimation forKey:#"someKey"];
}
else {
SKBounceAnimation *bounceAnimation = [SKBounceAnimation animationWithKeyPath:#"bounds"];
bounceAnimation.fromValue = [NSValue valueWithCGRect:self.frame];
bounceAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 45, 45)];
bounceAnimation.duration = 0.5f;
bounceAnimation.numberOfBounces = 4;
bounceAnimation.delegate = self;
bounceAnimation.removedOnCompletion = NO;
bounceAnimation.fillMode = kCAFillModeForwards;
[self.layer addAnimation:bounceAnimation forKey:#"someKey1"];
}
}
- (void)animationDidStop:(SKBounceAnimation *)animation finished:(BOOL)flag
{
[self.layer setValue:animation.toValue forKeyPath:animation.keyPath];
[self.layer removeAnimationForKey:#"someKey"];
[self.layer removeAnimationForKey:#"someKey1"];
}
Any ideas as to how Apple implemented the "slide to unlock" (also, "slide to power off" is another identical example) animation?
I thought about some sort of animating mask - but masking is not available on the iPhone OS for performance reasons.
Is there some private API effect (like SuckEffect) that they might have used? A spotlight type of effect? Some Core Animation thing?
Edit:
It's definitely not a series of stills. I've seen examples of being edit a plist value or something and customize the string on jailbroken iphones.
It can be easilly done by using Core Animation, animating a mask layer on the layer displaying the text.
Try this in any plain UIViewController (you can start with a new Xcode project based on the View-based application project template), or grab my Xcode project here:
Note that the CALayer.mask property is only available in iPhone OS 3.0 and later.
- (void)viewDidLoad
{
self.view.layer.backgroundColor = [[UIColor blackColor] CGColor];
UIImage *textImage = [UIImage imageNamed:#"SlideToUnlock.png"];
CGFloat textWidth = textImage.size.width;
CGFloat textHeight = textImage.size.height;
CALayer *textLayer = [CALayer layer];
textLayer.contents = (id)[textImage CGImage];
textLayer.frame = CGRectMake(10.0f, 215.0f, textWidth, textHeight);
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.15f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"Mask.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(-textWidth, 0.0f, textWidth * 2, textHeight);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:textWidth];
maskAnim.repeatCount = HUGE_VALF;
maskAnim.duration = 1.0f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
textLayer.mask = maskLayer;
[self.view.layer addSublayer:textLayer];
[super viewDidLoad];
}
The images used by this code are:
Yet another solution using a layer mask, but instead draws the gradient by hand and does not require images. View is the view with the animation, transparency is a float from 0 - 1 defining the amount of transparency (1 = no transparency which is pointless), and gradientWidth is the desired width of the gradient.
CAGradientLayer *gradientMask = [CAGradientLayer layer];
gradientMask.frame = view.bounds;
CGFloat gradientSize = gradientWidth / view.frame.size.width;
UIColor *gradient = [UIColor colorWithWhite:1.0f alpha:transparency];
NSArray *startLocations = #[[NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:(gradientSize / 2)], [NSNumber numberWithFloat:gradientSize]];
NSArray *endLocations = #[[NSNumber numberWithFloat:(1.0f - gradientSize)], [NSNumber numberWithFloat:(1.0f -(gradientSize / 2))], [NSNumber numberWithFloat:1.0f]];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"locations"];
gradientMask.colors = #[(id)gradient.CGColor, (id)[UIColor whiteColor].CGColor, (id)gradient.CGColor];
gradientMask.locations = startLocations;
gradientMask.startPoint = CGPointMake(0 - (gradientSize * 2), .5);
gradientMask.endPoint = CGPointMake(1 + gradientSize, .5);
view.layer.mask = gradientMask;
animation.fromValue = startLocations;
animation.toValue = endLocations;
animation.repeatCount = HUGE_VALF;
animation.duration = 3.0f;
[gradientMask addAnimation:animation forKey:#"animateGradient"];
SWIFT VERSION:
let transparency:CGFloat = 0.5
let gradientWidth: CGFloat = 40
let gradientMask = CAGradientLayer()
gradientMask.frame = swipeView.bounds
let gradientSize = gradientWidth/swipeView.frame.size.width
let gradient = UIColor(white: 1, alpha: transparency)
let startLocations = [0, gradientSize/2, gradientSize]
let endLocations = [(1 - gradientSize), (1 - gradientSize/2), 1]
let animation = CABasicAnimation(keyPath: "locations")
gradientMask.colors = [gradient.CGColor, UIColor.whiteColor().CGColor, gradient.CGColor]
gradientMask.locations = startLocations
gradientMask.startPoint = CGPointMake(0 - (gradientSize*2), 0.5)
gradientMask.endPoint = CGPointMake(1 + gradientSize, 0.5)
swipeView.layer.mask = gradientMask
animation.fromValue = startLocations
animation.toValue = endLocations
animation.repeatCount = HUGE
animation.duration = 3
gradientMask.addAnimation(animation, forKey: "animateGradient")
Swift 3
fileprivate func addGradientMaskToView(view:UIView, transparency:CGFloat = 0.5, gradientWidth:CGFloat = 40.0) {
let gradientMask = CAGradientLayer()
gradientMask.frame = view.bounds
let gradientSize = gradientWidth/view.frame.size.width
let gradientColor = UIColor(white: 1, alpha: transparency)
let startLocations = [0, gradientSize/2, gradientSize]
let endLocations = [(1 - gradientSize), (1 - gradientSize/2), 1]
let animation = CABasicAnimation(keyPath: "locations")
gradientMask.colors = [gradientColor.cgColor, UIColor.white.cgColor, gradientColor.cgColor]
gradientMask.locations = startLocations as [NSNumber]?
gradientMask.startPoint = CGPoint(x:0 - (gradientSize * 2), y: 0.5)
gradientMask.endPoint = CGPoint(x:1 + gradientSize, y: 0.5)
view.layer.mask = gradientMask
animation.fromValue = startLocations
animation.toValue = endLocations
animation.repeatCount = HUGE
animation.duration = 3
gradientMask.add(animation, forKey: nil)
}
You can use the kCGTextClip drawing mode to set the clipping path and then fill with a gradient.
// Get Context
CGContextRef context = UIGraphicsGetCurrentContext();
// Set Font
CGContextSelectFont(context, "Helvetica", 24.0, kCGEncodingMacRoman);
// Set Text Matrix
CGAffineTransform xform = CGAffineTransformMake(1.0, 0.0,
0.0, -1.0,
0.0, 0.0);
CGContextSetTextMatrix(context, xform);
// Set Drawing Mode to set clipping path
CGContextSetTextDrawingMode (context, kCGTextClip);
// Draw Text
CGContextShowTextAtPoint (context, 0, 20, "Gradient", strlen("Gradient"));
// Calculate Text width
CGPoint textEnd = CGContextGetTextPosition(context);
// Generate Gradient locations & colors
size_t num_locations = 3;
CGFloat locations[3] = { 0.3, 0.5, 0.6 };
CGFloat components[12] = {
1.0, 1.0, 1.0, 0.5,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.5,
};
// Load Colorspace
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
// Create Gradient
CGGradientRef gradient = CGGradientCreateWithColorComponents (colorspace, components,
locations, num_locations);
// Draw Gradient (using clipping path
CGContextDrawLinearGradient (context, gradient, rect.origin, textEnd, 0);
// Cleanup (exercise for reader)
Setup an NSTimer and vary the values in locations, or use CoreAnimation to do the same.
I added the code provided above by Pascal as a category on UILabel so you can animate any UILabel in this fashion. Here's the code. Some params might need to be changed for your background colors, etc. It uses the same mask image that Pascal has embedded in his answer.
//UILabel+FSHighlightAnimationAdditions.m
#import "UILabel+FSHighlightAnimationAdditions.h"
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#implementation UILabel (FSHighlightAnimationAdditions)
- (void)setTextWithChangeAnimation:(NSString*)text
{
NSLog(#"value changing");
self.text = text;
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.15f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"Mask.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(self.frame.size.width * -1, 0.0f, self.frame.size.width * 2, self.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:self.frame.size.width];
maskAnim.repeatCount = 1e100f;
maskAnim.duration = 2.0f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
self.layer.mask = maskLayer;
}
#end
//UILabel+FSHighlightAnimationAdditions.h
#import <Foundation/Foundation.h>
#interface UILabel (FSHighlightAnimationAdditions)
- (void)setTextWithChangeAnimation:(NSString*)text;
#end
not so fresh...but maybe it'll be useful
#define MM_TEXT_TO_DISPLAY #"default"
#define MM_FONT [UIFont systemFontOfSize:MM_FONT_SIZE]
#define MM_FONT_SIZE 25
#define MM_FONT_COLOR [[UIColor darkGrayColor] colorWithAlphaComponent:0.75f];
#define MM_SHADOW_ENABLED NO
#define MM_SHADOW_COLOR [UIColor grayColor]
#define MM_SHADOW_OFFSET CGSizeMake(-1,-1)
#define MM_CONTENT_EDGE_INSETS_TOP 0
#define MM_CONTENT_EDGE_INSETS_LEFT 10
#define MM_CONTENT_EDGE_INSETS_BOTTON 0
#define MM_CONTENT_EDGE_INSETS_RIGHT 10
#define MM_CONTENT_EDGE_INSETS UIEdgeInsetsMake(MM_CONTENT_EDGE_INSETS_TOP, MM_CONTENT_EDGE_INSETS_LEFT, MM_CONTENT_EDGE_INSETS_BOTTON, MM_CONTENT_EDGE_INSETS_RIGHT)
#define MM_TEXT_ALIGNMENT UITextAlignmentCenter
#define MM_BACKGROUND_COLOR [UIColor clearColor]
#define MM_TIMER_INTERVAL 0.05f
#define MM_HORIZONTAL_SPAN 5
#interface MMAnimatedGradientLabel : UILabel {
NSString *textToDisplay;
int text_length;
CGGradientRef gradient;
int current_position_x;
NSTimer *timer;
CGPoint alignment;
CGGlyph *_glyphs;
}
- (id)initWithString:(NSString *)_string;
- (void)startAnimation;
- (void)toggle;
- (BOOL)isAnimating;
#end
#define RGB_COMPONENTS(r, g, b, a) (r) / 255.0f, (g) / 255.0f, (b) / 255.0f, (a)
#interface MMAnimatedGradientLabel (Private)
- (CGRect)calculateFrame;
#end
#implementation MMAnimatedGradientLabel
// Missing in standard headers.
extern void CGFontGetGlyphsForUnichars(CGFontRef, const UniChar[], const CGGlyph[], size_t);
- (id)init {
textToDisplay = MM_TEXT_TO_DISPLAY;
return [self initWithFrame:[self calculateFrame]];
}
- (id)initWithString:(NSString *)_string {
textToDisplay = _string;
return [self initWithFrame:[self calculateFrame]];
}
-(id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// set default values
//
self.textAlignment = MM_TEXT_ALIGNMENT;
self.backgroundColor = MM_BACKGROUND_COLOR;
self.font = MM_FONT;
self.text = textToDisplay;
self.textColor = MM_FONT_COLOR;
if (MM_SHADOW_ENABLED) {
self.shadowColor = MM_SHADOW_COLOR;
self.shadowOffset = MM_SHADOW_OFFSET;
}
text_length = -1;
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGFloat colors[] =
{
RGB_COMPONENTS(255.0, 255.0, 255.0, 0.00),
// RGB_COMPONENTS(255.0, 255.0, 255.0, 0.15),
RGB_COMPONENTS(255.0, 255.0, 255.0, 0.95),
// RGB_COMPONENTS(255.0, 255.0, 255.0, 0.15),
RGB_COMPONENTS(255.0, 255.0, 255.0, 0.00)
};
gradient = CGGradientCreateWithColorComponents(rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));
CGColorSpaceRelease(rgb);
current_position_x = -(frame.size.width/2);// - MM_CONTENT_EDGE_INSETS.left - MM_CONTENT_EDGE_INSETS.right);
}
return self;
}
- (CGRect)calculateFrame {
CGSize size = [textToDisplay sizeWithFont:MM_FONT];
NSLog(#"size: %f, %f", size.width, size.height);
return CGRectMake(0, 0, size.width + MM_CONTENT_EDGE_INSETS.left + MM_CONTENT_EDGE_INSETS.right, size.height + MM_CONTENT_EDGE_INSETS.top + MM_CONTENT_EDGE_INSETS.bottom);
}
- (void)tick:(NSTimer*)theTimer {
if (current_position_x < self.frame.size.width)
current_position_x = current_position_x + MM_HORIZONTAL_SPAN;
else
current_position_x = -(self.frame.size.width/2); // - MM_CONTENT_EDGE_INSETS.left - MM_CONTENT_EDGE_INSETS.right);
[self setNeedsDisplay];
}
- (void)startAnimation {
timer = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:MM_TIMER_INTERVAL
target:self
selector:#selector(tick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)toggle {
if (!timer) {
timer = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:MM_TIMER_INTERVAL
target:self
selector:#selector(tick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
} else {
[timer invalidate];
[timer release];
timer = nil;
current_position_x = -(self.frame.size.width/2);
[self setNeedsDisplay];
}
}
- (BOOL)isAnimating {
if (timer)
return YES;
else
return NO;
}
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// Get drawing font.
CGFontRef font = CGFontCreateWithFontName((CFStringRef)[[self font] fontName]);
CGContextSetFont(ctx, font);
CGContextSetFontSize(ctx, [[self font] pointSize]);
// Calculate text drawing point only first time
//
if (text_length == -1) {
// Transform text characters to unicode glyphs.
text_length = [[self text] length];
unichar chars[text_length];
[[self text] getCharacters:chars range:NSMakeRange(0, text_length)];
_glyphs = malloc(sizeof(CGGlyph) * text_length);
for (int i=0; i<text_length;i ++)
_glyphs[i] = chars[i] - 29;
// Measure text dimensions.
CGContextSetTextDrawingMode(ctx, kCGTextInvisible);
CGContextSetTextPosition(ctx, 0, 0);
CGContextShowGlyphs(ctx, _glyphs, text_length);
CGPoint textEnd = CGContextGetTextPosition(ctx);
// Calculate text drawing point.
CGPoint anchor = CGPointMake(textEnd.x * (-0.5), [[self font] pointSize] * (-0.25));
CGPoint p = CGPointApplyAffineTransform(anchor, CGAffineTransformMake(1, 0, 0, -1, 0, 1));
if ([self textAlignment] == UITextAlignmentCenter)
alignment.x = [self bounds].size.width * 0.5 + p.x;
else if ([self textAlignment] == UITextAlignmentLeft)
alignment.x = 0;
else
alignment.x = [self bounds].size.width - textEnd.x;
alignment.y = [self bounds].size.height * 0.5 + p.y;
}
// Flip back mirrored text.
CGContextSetTextMatrix(ctx, CGAffineTransformMakeScale(1, -1));
// Draw shadow.
CGContextSaveGState(ctx);
CGContextSetTextDrawingMode(ctx, kCGTextFill);
CGContextSetFillColorWithColor(ctx, [[self textColor] CGColor]);
CGContextSetShadowWithColor(ctx, [self shadowOffset], 0, [[self shadowColor] CGColor]);
CGContextShowGlyphsAtPoint(ctx, alignment.x, alignment.y, _glyphs, text_length);
CGContextRestoreGState(ctx);
// Draw text clipping path.
CGContextSetTextDrawingMode(ctx, kCGTextClip);
CGContextShowGlyphsAtPoint(ctx, alignment.x, alignment.y, _glyphs, text_length);
// Restore text mirroring.
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
if ([self isAnimating]) {
// Fill text clipping path with gradient.
CGPoint start = CGPointMake(rect.origin.x + current_position_x, rect.origin.y);
CGPoint end = CGPointMake(rect.size.width/3*2 + current_position_x, rect.origin.y);
CGContextDrawLinearGradient(ctx, gradient, start, end, 0);
}
}
- (void) dealloc {
free(_glyphs);
[timer invalidate];
[timer release];
CGGradientRelease(gradient);
[super dealloc];
}
Thanks to rpetrich for the clipping gradient recipe. I am a newbie iPhone and Cocoa developer, so I was real happy to find it.
I've implemented a decent-looking Slide to Cancel UIViewController using rpetrich's method. You can download the Xcode project of my implementation from here.
My implementation uses a repeating NSTimer. I was unable to figure out how use Core (or Gore) Animation to have the iPhone's graphics engine continuously move the highlighting. I think that could be done on OS X with CALayer mask layers, but mask layers are not supported on iPhone OS.
When I play with Apple's "Slide to Unlock" slider on my iPhone's home screen, I occasionally see the animation freeze. So I think Apple may be using a timer as well.
If anyone can figure out how to do a non-timer based implementation using CA or OpenGL, I would love to see it.
Thanks for the help!
I know, I am a bit late with the answer, but Facebook has great library Shimmer that implements exactly that effect.
I took the best from above solutions and created a neat method that does all for you:
- (void)createSlideToUnlockViewWithText:(NSString *)text
{
UILabel *label = [[UILabel alloc] init];
label.text = text;
[label sizeToFit];
label.textColor = [UIColor whiteColor];
//Create an image from the label
UIGraphicsBeginImageContextWithOptions(label.bounds.size, NO, 0.0);
[[label layer] renderInContext:UIGraphicsGetCurrentContext()];
UIImage *textImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGFloat textWidth = textImage.size.width;
CGFloat textHeight = textImage.size.height;
CALayer *textLayer = [CALayer layer];
textLayer.contents = (id)[textImage CGImage];
textLayer.frame = CGRectMake(self.view.frame.size.width / 2 - textWidth / 2, self.view.frame.size.height / 2 - textHeight / 2, textWidth, textHeight);
UIImage *maskImage = [UIImage imageNamed:#"Mask.png"];
CALayer *maskLayer = [CALayer layer];
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.15] CGColor];
maskLayer.contents = (id)maskImage.CGImage;
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(-textWidth - maskImage.size.width, 0.0, (textWidth * 2) + maskImage.size.width, textHeight);
CABasicAnimation *maskAnimation = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnimation.byValue = [NSNumber numberWithFloat:textWidth + maskImage.size.width];
maskAnimation.repeatCount = HUGE_VALF;
maskAnimation.duration = 2.0;
maskAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[maskLayer addAnimation:maskAnimation forKey:#"slideAnimation"];
textLayer.mask = maskLayer;
self.slideToUnlockLayer = textLayer;
[self.view.layer addSublayer:self.slideToUnlockLayer];
}
First, a HUGE thank you to marcio for his solution. This worked almost perfectly, saved me hours of effort, and made a huge splash in my app. My boss loved it. I owe you beer. Or several.
One small correction for iPhone 4 only. I mean the hardware itself, not just iOS 4. They changed the system font on the iPhone 4 from Helvetica (iPhone 3Gs and below) to Helvetic Neue. This caused the translation you're doing from character to glyphs to be off by exactly 4 spots. For example the string "fg" would appear as "bc". I fixed this by explicitly setting the font to "Helvetica" rather than using "systemFontofSize". Now it works like a charm.
Again...THANK YOU!
I uploaded to GitHub a mini project who helps with the “slide to unlock” animation.
https://github.com/GabrielMassana/GM_FSHighlightAnimationAdditions
The project have LTR, RTL, Up to Down and Down to Up animations and it is based in the posts:
Pascal Bourque: https://stackoverflow.com/a/2778232/1381708
cberkley: https://stackoverflow.com/a/5710097/1381708
Cheers
Here's a SwiftUI version:
public struct Shimmer: AnimatableModifier {
private let gradient: Gradient
#State private var position: CGFloat = 0
public var animatableData: CGFloat {
get { position }
set { position = newValue }
}
private let animation = Animation
.linear(duration: 2)
.delay(1)
.repeatForever(autoreverses: false)
init(sideColor: Color = Color(white: 0.25), middleColor: Color = .white) {
gradient = Gradient(colors: [sideColor, middleColor, sideColor])
}
public func body(content: Content) -> some View {
LinearGradient(
gradient: gradient,
startPoint: .init(x: position - 0.2 * (1 - position), y: 0.5),
endPoint: .init(x: position + 0.2 * position, y: 0.5)
)
.mask(content)
.onAppear {
withAnimation(animation) {
position = 1
}
}
}
}
Use it like this:
Text("slide to unlock")
.modifier(Shimmer())
Maybe it's just a rendered-out animation - you know, a series of stills played one after another. Not necessarily a dynamic effect.
Update: Never mind, the video DrJokepu posted proved it's dynamically generated.
Top: UILabel with opaque background and clear text
Clear text is rendered in drawRect: func through complicated masking process
Middle: Worker View that is performing a repeating animation moving an image behind the top label
Bottom: a UIView that you add the middle and top subview to in that order. Can be whatever color you want the text to be
An example can be seen here https://github.com/jhurray/AnimatedLabelExample