How to scale transition UILabel without using a lot of memory?
Asked Answered
S

2

9

I'm trying to reproduce some of the 2D transitions in Impress.js's sample presentation in Objective C. Specifically the rotate, pan, and scaling - right now I'm focusing on the scaling.

I've tried scaling a UILabel to the point where it "passes the screen", as the "visualize your Big thoughts" -> "and tiny ideas" does in the sample presentation.

This is what I've tried so far:

UILabel *label = [[UILabel alloc] init];

label.text = @"Hello World!";
label.textColor = [UIColor blackColor];
label.font = [UIFont fontWithName:@"Arial" size:18.f];
[label sizeToFit];
label.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
[self.view addSubview:label];

label.contentScaleFactor *= 80;

[UIView animateWithDuration:5 animations:^{
    label.transform = CGAffineTransformScale(label.transform, 80, 80);
}];

Unfortunately this eats up about ~30-60 MB of RAM, depending on what the contentScaleFactor and initial font size is. If I don't increase the contentScaleFactor the text looks blurry. Increasing the font size also seems to eat just as much memory.

Here is what it looks in the profiler:

enter image description here

And this is just a single UILabel.

Is there any way to do this without eating up so much memory, without sacrificing quality of the text being rendered or the transitions?

Sulfur answered 4/3, 2013 at 20:43 Comment(2)
Anyway, have a look at CATransformLayer as a way of grouping layers and transforming them.Passus
Updated question to focus on the memory consumption after scaling a UILabel. The previous question was too broad (and closed as such). While the currently accepted answer is awesome, it doesn't address the issue in the updated question. While I would like to put another bounty on it, I have very little rep to drop another 500.Sulfur
I
4

Project download link

I don't believe it's necessary to leave Quartz to achieve this reproduction. Everything you've described as well as everything I've gathered from messing with Impress.js seems to be replicable through applying transforms (mostly 2D, some 3D) to a set of UILabels added to a container view that can be moved freely within the main view.

To do this, the project I created uses a subclass of UILabel titled "ImpressLabel" with an extra init function where instead of passing the label a frame, you pass it a size, a center point, and a CGFloat for the label's rotation on the Z axis. This transform is applied to the label upon instantiation so that when you set up the labels they will appear on screen already in the position and transformation you specify.

Then as far as configuring the text goes, you can pass the label an NSAttributedString instead of a NSString. This allows you to modify different parts of the string independently, so different words in the label can be different sizes, fonts, colors, background colors, etc. Here's an example of the above two paragraphs:

ImpressLabel *label1 = [[ImpressLabel alloc] initWithSize:CGSizeMake(260.0f, 80.0f) andCenterPointInSuperview:CGPointMake(500.0f, 500.0f) andRotationInSuperview:0.0f andEndingScaleFactor:1.3];

NSMutableAttributedString *firstLabelAttributes = [[NSMutableAttributedString alloc] initWithString:@"then you should try\nimpress.js*\n* no rhyme intended"];

[firstLabelAttributes addAttribute:NSFontAttributeName
                             value:[UIFont systemFontOfSize:label1.font.pointSize - 2]
                             range:NSMakeRange(0, 19)];

[firstLabelAttributes addAttribute:NSFontAttributeName
                             value:[UIFont systemFontOfSize:label1.font.pointSize - 8]
                             range:NSMakeRange(firstLabelAttributes.string.length - 19, 19)];

[firstLabelAttributes addAttribute:NSFontAttributeName
                             value:[UIFont boldSystemFontOfSize:label1.font.pointSize + 14]
                             range:NSMakeRange(23, 11)];


[label1 setNumberOfLines:3];
[label1 setAttributedText:firstLabelAttributes];
[label1 setTextAlignment:NSTextAlignmentCenter];
[containmentView addSubview:label1];

Now, on to more of the guts of the whole operation. As I mentioned above, this subclass adds a tap gesture to each label. When a tap is recognized a few things happen. The view containing the labels will pan/back/away by adjusting it's scale. It will also begin rotate and adjust its anchor point in the main view so that when the animation stops, the selected label will be centered on screen in the correct orientation. Then of course, while this is all going on the alpha of the selected label will be brought up to 1.0f while the alpha of the rest will be lowered to 0.25f.

- (void)tapForRotationDetected:(UITapGestureRecognizer *)sender {
    CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    [scale setToValue:[NSNumber numberWithFloat:0.8]];
    [scale setAutoreverses:YES];
    [scale setDuration:0.3];

    //Create animation to adjust the container views anchorpoint.
    CABasicAnimation *adjustAnchor = [CABasicAnimation animationWithKeyPath:@"anchorPoint"];
    [adjustAnchor setFromValue:[NSValue valueWithCGPoint:self.superview.layer.anchorPoint]];
    [adjustAnchor setToValue:[NSValue valueWithCGPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)]];
    [adjustAnchor setRemovedOnCompletion:NO];
    [adjustAnchor setFillMode:kCAFillModeForwards];

    //Create animation to rotate the container view within its superview.
    CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];

    //Create the animation group to apply these transforms
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    [animationGroup setAnimations:@[adjustAnchor,rotation]];
    [animationGroup setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
    [animationGroup setDuration:0.6];

    //Apply the end results of the animations directly to the container views layer.

    [self.superview.layer setTransform:CATransform3DRotate(CATransform3DIdentity, DEGREES_TO_RADIANS(-self.rotationInSuperview), 0.0f, 0.0f, 1.0f)];
    [self.superview.layer setAnchorPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)];
    [self.superview.layer addAnimation:animationGroup forKey:@"animationGroup"];

    //Animate the alpha property of all ImpressLabels in the container view.
    [self.superview bringSubviewToFront:self];
    [UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        for (ImpressLabel *label in sender.view.superview.subviews) {
            if ([label isKindOfClass:[ImpressLabel class]]) {
                if (label != self) {
                    [label setAlpha:0.25f];
                }else{
                    [label setAlpha:1.0f];
                }

            }
        }
    } completion:nil];
}

Now, to address some of the concerns listed in your question.

  1. I've profiled this project in Instruments' allocations tool and it only consumes about 3.2 MB overall, so I'd say this approach is efficient enough.

  2. The sample I've provided animates most objects in 2D space, with the exception of the scaling animation which is an illusion at best. What I've done here is meant to serve as an example of how this can be done and isn't a 100% complete demonstration because as I stated above, animation really isn't my area of expertise. However, from looking over the docs, it seems like the key to having a label rotated in the third dimension and adjusting its superview to rotate all the labels and leave the selected label flat would be the use of CATransform3DInvert(). Although I haven't yet had time to fully figure out it works, it looks like it might be exactly what is needed in this scenario.

  3. As far as mirroring goes, I don't believe there will be any problems with properly scaling everything. Looking over Apple's Multiple Display Programming Guide, it looks like the object passed to the NSNotification UIScreenDidConnectNotification is a UIScreen object. Since this is the case, you can easily ask for this displays bounds, and adjust the frames of the labels and the container view accordingly.

Note: In this example, only 0, 90, 180, and -90 degree transforms animate 100% correctly, due to the anchor point's coordinates being incorrectly generated. It looks like the solution lies with CGPointApplyAffineTransform(<#CGPoint point#>, <#CGAffineTransform t#>), but again I haven't had as much time to play with it as I would have liked. Either way, this should be plenty to get you started with your reproduction.

This has definitely sparked my interest though and when I get a chance to work on this again, I'll gladly update this post with any new information. Hope this helps!

Inventory answered 11/3, 2013 at 20:53 Comment(3)
Awesome! The "one more thing... have you noticed it's in 3D" isn't critical. Rotation and panning are the biggest things. When there is a rotation and a pan, the pan seems to jerk at the last second. 2D scaling would be nice. You should put the project on Github instead of Dropbox so it's always available on the answer.Sulfur
@Sulfur Unfortunately I haven't been able to work that out 100%. The jumping only occurs when trying to animate to/from labels that aren't rotated at right angles. This is due to the containers anchor point coordinates being set incorrectly from the transform. As I stated in the answer CGPointApplyAffineTransform() looks like it could cure this, but I have yet to be able to make this work. And I'll keep this on Dropbox for now and when I iron out a few of the bugs I'll gladly host this on Github.Inventory
@Sulfur Sure thing! And I'll definitely keep you posted with any progress I make with this!Inventory
C
1

I think it would help if you draw the text on a CALayer. A library that could help with this is: https://github.com/ZaBlanc/CanvasKit Draw the next step of your animation on a new CALayer and transform to that. You could chain the required animations using this library: https://github.com/yangmeyer/CPAnimationSequence or this library: https://github.com/clayallsopp/Walt

Cassaundra answered 11/3, 2013 at 7:9 Comment(1)
The last link is Ruby. The CanvasKit seems to use [NSString drawAtPoint]. The memory consumption seems lower than UILabel though, so definitely worth checking out, thanks!Sulfur

© 2022 - 2024 — McMap. All rights reserved.