for example:
label.text = #"I \n love \n stackoverflow ";
how to show those three lines just like first animation fade show "I" , then after animation during time animation fade show "love" finally after animation during time animation fade show "stackoverflow"?
I have created a function for your request. Make sure your label have dynamic height, numberOfLines = 0 and try it ;)
- (void)animateLabel:(UILabel*)label withString:(NSString*)string duration:(NSTimeInterval)duration {
NSUInteger length = [string length];
NSUInteger paraStart = 0, paraEnd = 0, contentsEnd = 0;
NSMutableArray *displayedStringArray = [NSMutableArray array];
NSRange currentRange;
while (paraEnd < length) {
[string getParagraphStart:¶Start end:¶End
contentsEnd:&contentsEnd forRange:NSMakeRange(paraEnd, 0)];
currentRange = NSMakeRange(0, contentsEnd);
[displayedStringArray addObject:[string substringWithRange:currentRange]];
}
CATransition *animation = [CATransition animation];
animation.duration = duration;
animation.type = kCATransitionFade;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
__block void (^animationBlock) (int lineNumber);
__block void (^weakAnimationBlock) (int lineNumber);
animationBlock = ^void(int lineNumber){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[label.layer addAnimation:animation forKey:#"changeTextTransition"];
label.text = displayedStringArray[lineNumber];
weakAnimationBlock = animationBlock;
__block int nextLine = lineNumber + 1;
if (nextLine < displayedStringArray.count) {
weakAnimationBlock(nextLine);
}
});
};
animationBlock(0);
}
Simply call
[YOUR_CLASS animateLabel:label withString:#"I\nLove\nStackOverFlow" duration:YOUR_DURATION];]
I'm trying to create an infinitely tracing path that draws and the back of it disappears. However, the code I have makes the path disappear after a single iteration. In fact, the layer disappears before the path's disappear animation completes. It seems like, instead, it disappears when the drawing animation completes.
Furthermore, once it disappears, it doesn't show up again for another few seconds (perhaps however long it would take for the animation to repeat) before starting again.
Two questions:
Why is the layer disappearing before the disappearing animation completes?
How can I make the animation play continuously without disappearing?
Thank you in advance! Here's my code:
CGRect boundingFrame = CGRectMake(CGRectGetMidX(self.bounds) - 48.0, CGRectGetMidY(self.bounds) - 48.0, 96.0, 96.0);
_pathPoints = #[
[NSValue valueWithCGPoint:CGPointMake(CGRectGetMidX(boundingFrame), CGRectGetMinY(boundingFrame) + squareSideWidth/2.0)],
[NSValue valueWithCGPoint:CGPointMake(CGRectGetMinX(boundingFrame) + squareSideWidth/2.0, CGRectGetMidY(boundingFrame))],
[NSValue valueWithCGPoint:CGPointMake(CGRectGetMidX(boundingFrame), CGRectGetMaxY(boundingFrame) - squareSideWidth/2.0)],
[NSValue valueWithCGPoint:CGPointMake(CGRectGetMaxX(boundingFrame) - squareSideWidth/2.0, CGRectGetMidY(boundingFrame))]
];
CAShapeLayer *line = [CAShapeLayer layer];
line.path = pathForPoints(_pathPoints).CGPath;
line.lineCap = kCALineCapRound;
line.strokeColor = color.CGColor;
line.fillColor = [UIColor clearColor].CGColor;
line.strokeEnd = 0.0;
[self.layer addSublayer:line];
NSMutableArray<CABasicAnimation *> *animations = [NSMutableArray new];
for (int i = 0; i < [_pathPoints count]; i++) {
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.beginTime = index;
drawAnimation.duration = 1.0;
drawAnimation.fromValue = #(strokeEndForIndex(points, index - 1));
drawAnimation.toValue = #(strokeEndForIndex(points, index));
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
drawAnimation.additive = YES;
drawAnimation.removedOnCompletion = NO;
CABasicAnimation *eraseAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
eraseAnimation.beginTime = index + 0.75;
eraseAnimation.duration = 1.0;
eraseAnimation.fromValue = #(strokeEndForIndex(points, index - 1));
eraseAnimation.toValue = #(strokeEndForIndex(points, index));
eraseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
eraseAnimation.additive = YES;
eraseAnimation.removedOnCompletion = NO;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = #[drawAnimation, eraseAnimation];
group.duration = 2.0 * [animations count];
group.repeatCount = INFINITY;
group.removedOnCompletion = NO;
[line addAnimation:group forKey:#"line"];
}
Helper functions, for reference:
static CGFloat lengthOfPathWithPoints(NSArray<NSValue *> *points)
{
CGFloat totalDist = 0;
for (int i = 0; i < [points count]; i++) {
CGFloat xDist = [points[i % [points count]] CGPointValue].x - [points[(i + 1) % [points count]] CGPointValue].x;
CGFloat yDist = [points[i % [points count]] CGPointValue].y - [points[(i + 1) % [points count]] CGPointValue].y;
totalDist += sqrt(pow(xDist, 2) + pow(yDist, 2));
}
return totalDist;
}
static CGFloat strokeEndForIndex(NSArray<NSValue *> *points, int index)
{
// If it's the last index, just return 1 early
if (index == [points count] - 1) {
return 1.0;
}
// In the case where index = -1 (as it does for the initial erase animation fromValue), just return 0
if (index < 0) {
return 0.0;
}
CGFloat totalDist = 0;
for (int i = 0; i < index + 1; i++) {
CGFloat xDist = [points[i % [points count]] CGPointValue].x - [points[(i + 1) % [points count]] CGPointValue].x;
CGFloat yDist = [points[i % [points count]] CGPointValue].y - [points[(i + 1) % [points count]] CGPointValue].y;
totalDist += sqrt(pow(xDist, 2) + pow(yDist, 2));
}
return totalDist/lengthOfPathWithPoints(points);
}
static UIBezierPath *pathForPoints(NSArray<NSValue *> *points)
{
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:[[points firstObject] CGPointValue]];
for (int i = 0; i < [points count]; i++) {
[path addLineToPoint:[points[(i + 1) % 4] CGPointValue]];
}
return path;
}
Increase your duration property value and change your repeatCount = Float(Int.max) to something like this.
I'm using this class called SHLineGraphView and it's working good, but I have a problem that they didn't implemented a methods for removing a plot. Now I have to do it, but I can't quite figure the right way to do.
This is how they drawing the plot:
- (void)addPlot:(SHPlot *)newPlot;
{
if(nil == newPlot) {
return;
}
if(_plots == nil){
_plots = [NSMutableArray array];
}
[_plots addObject:newPlot];
}
- (void)drawPlot:(SHPlot *)plot {
NSDictionary *theme = plot.plotThemeAttributes;
//
CAShapeLayer *backgroundLayer = [CAShapeLayer layer];
backgroundLayer.frame = self.bounds;
backgroundLayer.fillColor = ((UIColor *)theme[kPlotFillColorKey]).CGColor;
backgroundLayer.backgroundColor = [UIColor clearColor].CGColor;
[backgroundLayer setStrokeColor:[UIColor clearColor].CGColor];
[backgroundLayer setLineWidth:((NSNumber *)theme[kPlotStrokeWidthKey]).intValue];
CGMutablePathRef backgroundPath = CGPathCreateMutable();
//
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.frame = self.bounds;
circleLayer.fillColor = ((UIColor *)theme[kPlotPointFillColorKey]).CGColor;
circleLayer.backgroundColor = [UIColor clearColor].CGColor;
[circleLayer setStrokeColor:((UIColor *)theme[kPlotPointFillColorKey]).CGColor];
[circleLayer setLineWidth:((NSNumber *)theme[kPlotStrokeWidthKey]).intValue];
CGMutablePathRef circlePath = CGPathCreateMutable();
//
CAShapeLayer *graphLayer = [CAShapeLayer layer];
graphLayer.frame = self.bounds;
graphLayer.fillColor = [UIColor clearColor].CGColor;
graphLayer.backgroundColor = [UIColor clearColor].CGColor;
[graphLayer setStrokeColor:((UIColor *)theme[kPlotStrokeColorKey]).CGColor];
[graphLayer setLineWidth:((NSNumber *)theme[kPlotStrokeWidthKey]).intValue];
CGMutablePathRef graphPath = CGPathCreateMutable();
double yRange = [_yAxisRange doubleValue]; // this value will be in dollars
double yIntervalValue = yRange / INTERVAL_COUNT;
//logic to fill the graph path, ciricle path, background path.
[plot.plottingValues enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *dic = (NSDictionary *)obj;
__block NSNumber *_key = nil;
__block NSNumber *_value = nil;
[dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
_key = (NSNumber *)key;
_value = (NSNumber *)obj;
}];
int xIndex = [self getIndexForValue:_key forPlot:plot];
//x value
double height = self.bounds.size.height - BOTTOM_MARGIN_TO_LEAVE;
double y = height - ((height / ([_yAxisRange doubleValue] + yIntervalValue)) * [_value doubleValue]);
(plot.xPoints[xIndex]).x = ceil((plot.xPoints[xIndex]).x);
(plot.xPoints[xIndex]).y = ceil(y);
}];
//move to initial point for path and background.
CGPathMoveToPoint(graphPath, NULL, _leftMarginToLeave, plot.xPoints[0].y);
CGPathMoveToPoint(backgroundPath, NULL, _leftMarginToLeave, plot.xPoints[0].y);
int count = _xAxisValues.count;
for(int i=0; i< count; i++){
CGPoint point = plot.xPoints[i];
CGPathAddLineToPoint(graphPath, NULL, point.x, point.y);
CGPathAddLineToPoint(backgroundPath, NULL, point.x, point.y);
CGPathAddEllipseInRect(circlePath, NULL, CGRectMake(point.x - 2.5, point.y - 2.5, 5, 5));
}
//move to initial point for path and background.
CGPathAddLineToPoint(graphPath, NULL, _leftMarginToLeave + PLOT_WIDTH, plot.xPoints[count -1].y);
CGPathAddLineToPoint(backgroundPath, NULL, _leftMarginToLeave + PLOT_WIDTH, plot.xPoints[count - 1].y);
//additional points for background.
CGPathAddLineToPoint(backgroundPath, NULL, _leftMarginToLeave + PLOT_WIDTH, self.bounds.size.height - BOTTOM_MARGIN_TO_LEAVE);
CGPathAddLineToPoint(backgroundPath, NULL, _leftMarginToLeave, self.bounds.size.height - BOTTOM_MARGIN_TO_LEAVE);
CGPathCloseSubpath(backgroundPath);
backgroundLayer.path = backgroundPath;
graphLayer.path = graphPath;
circleLayer.path = circlePath;
//animation
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1;
animation.fromValue = #(0.0);
animation.toValue = #(1.0);
[graphLayer addAnimation:animation forKey:#"strokeEnd"];
backgroundLayer.zPosition = 0;
graphLayer.zPosition = 1;
circleLayer.zPosition = 2;
[self.layer addSublayer:graphLayer];
[self.layer addSublayer:circleLayer];
[self.layer addSublayer:backgroundLayer];
NSUInteger count2 = _xAxisValues.count;
for(int i=0; i< count2; i++){
CGPoint point = plot.xPoints[i];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.backgroundColor = [UIColor clearColor];
btn.tag = i;
btn.frame = CGRectMake(point.x - 20, point.y - 20, 40, 40);
[btn addTarget:self action:#selector(clicked:) forControlEvents:UIControlEventTouchUpInside];
objc_setAssociatedObject(btn, kAssociatedPlotObject, plot, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self addSubview:btn];
}
}
And this is what I've been added, but just can't figure out how to actually remove the plot:
- (void)removePlot:(SHPlot *)plot
{
if (nil == plot) {
return;
}
if (_plots == nil) {
return;
}
[_plots removeObject:plot];
[self removePlotFromView:plot];
}
This is what you need to add in your SHLineGraphView.m file.
This method will remove all the plots added on your graphview.
- (void)removeAllPlots {
for (UIView *view in self.subviews) {
view.layer.sublayers = nil;
[view removeFromSuperview];
}
self.layer.sublayers = nil;
}
For removing a specific plot, first you need to give unique tag to your graphLayer, circleLayer and backgroundLayer.
You can do that by adding following code in your drawPlot: method.
[graphLayer setValue:[NSNumber numberWithInteger:[_plots indexOfObject:plot]] forKey:#"PLTag"];
[circleLayer.frame setValue:[NSNumber numberWithInteger:[_plots indexOfObject:plot]] forKey:#"PLTag"];
[backgroundLayer setValue:[NSNumber numberWithInteger:[_plots indexOfObject:plot]] forKey:#"PLTag"];
Then, add this method to remove specific plot:
- (void)removePlot:(SHPlot *)plot
{
if (plot == nil)
return;
else if (_plots == nil)
return;
for (CALayer *layer in self.layer.sublayers) {
if ([[layer valueForKey:#"PLTag"] isEqualToNumber:[NSNumber numberWithInteger:[_plots indexOfObject:plot]]]) {
[layer removeFromSuperlayer];
}
}
}
Hope it helps you.
So, by #Tejvansh Singh Chhabra's answer I managed to find a solution:
First of all, I've added these 2 new methods to SHGraphLineView.h:
/**
* this methods will remove a plot from the graph.
*
* #param plot the Plot that you want to remove from the Graph.
*/
- (void)removePlot:(SHPlot *)plot;
/**
* this methods will draw a new plot on the graph.
*
* #param plot the Plot that you want to draw on the Graph.
*/
- (void)drawNewPlot:(SHPlot*)plot;
And implement them on the main file:
- (void)removePlot:(SHPlot *)plot
{
if (plot == nil) {
return;
}
else if (_plots == nil) {
return;
}
for (CALayer *layer in [self.layer.sublayers copy])
{
if ([layer valueForKey:#"PLTag"] != nil)
{
NSString *tag = [layer valueForKey:#"PLTag"];
NSString *plotIndex = [NSString stringWithFormat:#"%ld", (long)plot.plotIndex];
if ([tag isEqualToString:plotIndex])
{
[layer removeFromSuperlayer];
}
}
}
[_plots removeObject:plot];
}
- (void)drawNewPlot:(SHPlot*)plot
{
if (nil == plot) {
return;
}
if (_plots == nil) {
_plots = [NSMutableArray array];
}
[_plots addObject:plot];
[self xPointWithoutDrawingForPlot:plot];
[self drawPlot:plot];
}
Now there is another method that create the x point for the plot, but without the labels and all the other this that are drawing with it when you first build the graph:
- (void)xPointWithoutDrawingForPlot:(SHPlot*)plot
{
int xIntervalCount = _xAxisValues.count;
double xIntervalInPx = PLOT_WIDTH / _xAxisValues.count;
//initialize actual x points values where the circle will be
plot.xPoints = calloc(sizeof(CGPoint), xIntervalCount);
for(int i=0; i < xIntervalCount; i++)
{
CGPoint currentLabelPoint = CGPointMake((xIntervalInPx * i) + _leftMarginToLeave, self.bounds.size.height - BOTTOM_MARGIN_TO_LEAVE);
CGRect xLabelFrame = CGRectMake(currentLabelPoint.x , currentLabelPoint.y, xIntervalInPx, BOTTOM_MARGIN_TO_LEAVE);
plot.xPoints[i] = CGPointMake((int) xLabelFrame.origin.x + (xLabelFrame.size.width /2) , (int) 0);
}
}
Another important thing is to add a valueForKey to the CAShapeLayer objects of the plot at first draw, we need to add to this method:
- (void)drawPlot:(SHPlot *)plot;
These 3 lines:
[graphLayer setValue:[NSString stringWithFormat:#"%ld", (long)plot.plotIndex] forKey:#"PLTag"];
[circleLayer setValue:[NSString stringWithFormat:#"%ld", (long)plot.plotIndex] forKey:#"PLTag"];
[backgroundLayer setValue:[NSString stringWithFormat:#"%ld", (long)plot.plotIndex] forKey:#"PLTag"];
One last thing, to SHPlot class we need to add an NSInteger that will indicate the index of this specific plot, and you need to set it when you first build the plot
SHPlot.h
/**
* index for plot for identification
*/
#property (assign, nonatomic) NSInteger plotIndex;
Enjoy! :)
I have a method which translates a view and optionally scales it while translating. I am using animation groups and adding the required animations as needed. Here are the animation cases working perfectly.
Translation (x or y axis or both) (OK)
Scaling (OK)
Translation while scaling (MOSTLY)
However, doing a translation while scaling works only when the starting scalefactor is 1. When the view is already scaled, I notice most of the transition jumping instead of animating, although the end result of scaling and translation is correct.
I am posting part of the code for clarification. The problem occurs only when previousZoomScale is not 1! I appreciate any directions you can point me to.
-(void)performAnimationForView: (AnimationType)animationType
WithDX: (CGFloat)dx
AndDY: (CGFloat)dy
Scale: (CGFloat)scale
Duration: (CGFloat)duration
{
CABasicAnimation *translateXAnimation = nil, *translateYAnimation = nil, *scaleAnimation = nil;
NSString* animationGroupName;
CGPoint newDxDy;
switch (animationType)
{
case kAnimationTranslateAndScale:
// set animation key
animationGroupName = #"translatescale";
translateXAnimation = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
translateXAnimation.repeatCount = 0;
translateXAnimation.autoreverses = NO;
translateXAnimation.removedOnCompletion = NO;
translateXAnimation.fillMode = kCAFillModeForwards;
translateXAnimation.fromValue = [NSNumber numberWithFloat: self.previousDxDy.x]; // previous point we're tranlsating from
translateXAnimation.toValue = [NSNumber numberWithFloat:dx]; // destination point
translateYAnimation = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
translateYAnimation.repeatCount = 0;
translateYAnimation.autoreverses = NO;
translateYAnimation.removedOnCompletion = NO;
translateYAnimation.fillMode = kCAFillModeForwards;
translateYAnimation.fromValue = [NSNumber numberWithFloat: self.previousDxDy.y];
translateYAnimation.toValue = [NSNumber numberWithFloat:dy];
newDxDy = CGPointMake(dx, dy);
self.previousDxDy = newDxDy;
// scaling animation
scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.autoreverses = NO;
scaleAnimation.repeatCount = 0;
scaleAnimation.removedOnCompletion = NO;
scaleAnimation.fillMode = kCAFillModeForwards;
scaleAnimation.fromValue = [NSNumber numberWithDouble: self.previousZoomScale]; // initial zoom scale
scaleAnimation.toValue = [NSNumber numberWithDouble:scale]; // target scale
self.previousZoomScale = scale;
break;
}
NSMutableArray* animationArray = [[NSMutableArray alloc] initWithObjects: nil];
if (translateXAnimation != nil)
[animationArray addObject: translateXAnimation];
if (translateYAnimation != nil)
[animationArray addObject: translateYAnimation];
if (scaleAnimation != nil)
[animationArray addObject: scaleAnimation];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theGroup.duration = duration;
theGroup.removedOnCompletion = NO;
theGroup.fillMode = kCAFillModeForwards;
theGroup.repeatCount = 0;
theGroup.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
theGroup.animations = animationArray;
theGroup.delegate = self;
[theGroup setValue: animationGroupName forKey: #"animationtype"];
[self.backgroundImageView.layer addAnimation: theGroup forKey: animationGroupName];
}
I am trying to animate several views (layers) using CAKeyframeAnimation and CAAnimationGroup. Each view to be animated is contained in an array. My idea is to loop through the array where CAKeyframeAnimation instance of each view would be added to an array of animations, which then would be added to the CAAnimationGroup. My goal is to animate each view one after another. As far as I understand I need to use the group in order to have a reference for the beginTime.
I am probably missing something obvious, but my code doesn't work
CAAnimationGroup *cardAnimationGroup = [CAAnimationGroup animation];
cardAnimationGroup.delegate = self;
cardAnimationGroup.removedOnCompletion = NO;
cardAnimationGroup.beginTime = 0.0f;
[cardAnimationGroup setValue:#"animation0" forKey:#"id"];
cardAnimationGroup.duration = 1.2f * [_myViewsArray count];
NSMutableArray *animationsArray = [[NSMutableArray alloc] init];
// Loop through views
for (UIView *cardView in _myViewsArray)
{
NSUInteger index = [_myViewsArray indexOfObject:cardView];
// my target origin
CGFloat yOffset = (-(cardView.frame.origin.y - _middleCardView.frame.origin.y) - index * 2);
CAKeyframeAnimation *translateCardPositionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.translation.y"];
translateCardPositionAnimation.removedOnCompletion = YES;
//translateCardPositionAnimation.fillMode = kCAFillModeForwards;
translateCardPositionAnimation.beginTime = 1.2f * index;
translateCardPositionAnimation.duration = 1.2f;
translateCardPositionAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:yOffset],
[NSNumber numberWithFloat:yOffset],nil];
translateCardPositionAnimation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f],
[NSNumber numberWithFloat:1.2f], nil];
[_containerCardView addSubview:cardView];
[_containerCardView sendSubviewToBack:cardView];
[animationsArray addObject:translateCardPositionAnimation];
[cardView.layer addObject:translateCardPositionAnimation forKey:#"id"];
if (index == [_myViewsArray count]-1) {
[cardAnimationGroup setAnimations:animationsArray];
}
}