I have a grid of 30 UIButtons and potentially even more, subclassed to be rendered using layers: a base CALayer, a CAShapeLayer, a CAGradientLayer and a CATextLayer. I am trying to minimize the overall time required to render/display the buttons when the corresponding xib file is loaded. If I simply setup each button in turn in viewDidLoad, the time required for the view to appear is about 5-6 seconds, which is clearly too much.
In order to speed-up the buttons setup, I am using Grand Central Dispatch as follows. In viewDidLoad, I setup each button layers using dispatch_async on the global queue (adding to the base layer the shape and the gradient layers), so that the buttons can be rendered in a different thread. A the end of the block, the CATextLayer is added to the gradient layer.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
CGRect base_bounds = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height - self.layer.bounds.size.height * 0.10f);
CGPoint move_point = CGPointMake(0.0f, base_bounds.size.height * 0.10f);
self.layer.masksToBounds = NO;
baseLayer = [CALayer layer];
baseLayer.cornerRadius = 10.0;
baseLayer.shadowOffset = CGSizeMake(0.0f, 2.0f);
baseLayer.shadowOpacity = 1.5f;
baseLayer.shadowColor = [UIColor blackColor].CGColor;
baseLayer.shadowRadius = 2.5f;
baseLayer.anchorPoint = CGPointMake(0.5f, 0.5f);
baseLayer.position = move_point;
CAShapeLayer *shape = [CALayer layer];
shape.bounds = base_bounds;
shape.cornerRadius = 10.0;
shape.anchorPoint = CGPointMake(0.0f, 0.0f);
shape.position = move_point;
shape.backgroundColor = [UIColor darkGrayColor].CGColor;
gradient = [CAGradientLayer layer];
gradient.anchorPoint = CGPointMake(0.0f, 0.0f);
gradient.position = CGPointMake(0.0f, 0.0f);
gradient.bounds = base_bounds;
gradient.cornerRadius = 10.0;
gradient.borderColor = [UIColor colorWithRed:0.72f
green:0.72f
blue:0.72f
alpha:1.0].CGColor;
gradient.borderWidth = 0.73;
gradient.colors = [NSArray arrayWithObjects:
(id)[UIColor whiteColor].CGColor,
(id)[UIColor whiteColor].CGColor,
nil];
[baseLayer addSublayer:shape];
[baseLayer addSublayer:gradient];
[self.layer addSublayer:baseLayer];
[textLayer setBounds:gradient.bounds];
[textLayer setPosition:CGPointMake(CGRectGetMidX(textLayer.bounds), CGRectGetMaxY(textLayer.bounds) - 6)];
[textLayer setString:self.titleLabel.text];
[textLayer setForegroundColor:[UIColor blackColor].CGColor];
[gradient addSublayer:textLayer];
});
This approach reduce the overall time to about 2-3 seconds. I am wondering if anyone can suggest a faster way to render the buttons. Please note that I am not interested to any solution which discards the use of layers.
Thank you in advance.
Perhaps I'm missing the point but wouldn't you be better off overriding the UIButton drawRect: method and doing your drawing in CoreGraphics (CG) things would be drawn A LOT faster than seconds, you can easily do gradients, text, images with the CG API. If I understand correctly you have 4 layers per button and 30+ buttons in the same view (120+ layers)? If so, I don't think you are meant to draw so many layers (rendering/blending all of them individually would explain the huge render time). Another possibility would be to have 4 big layers, for all of the buttons.
Here's my idea,
Separate your CALayers from UIButton - UIKit won't allow anything on the background thread.
When you have an opportunity in a screen preceding the button grid, use [buttonGridViewController performSelectorInBackground:renderCALayers] to render your CALayers in the background.
When you load your button grid in viewDidLoad, overlay UIButtons with type UIButtonTypeCustom, and backgroundColor = [UIColor clearColor] over the top of your 'button' CALayers (make sure to call bringSubviewToFront on the UIButtons so they get the touch events).
If you don't know how many buttons you are rendering, you might want to pre-render the maximum number you might display.
If you have any questions please comment.
EDIT:
A few related items,
What to replace a UIButton to improve frame rate?
UI design - Best way to deal with many UIButtons
How to get touch event on a CALayer?
I believe the only way you'll get quicker load from this point is to either replace or remove UIButtons altogether and intercept touch events with another approach. That way all rendering can be done in a background thread before the view is presented.
Have you considered using NSOperation to generate your button's layers? In your code, you'd instantiate an operation queue and then iterate through each button, firing an operation (passing the respective parameters required to generate that button's layer) into the queue as each button is initiated.
Within your NSOperation, generate your layer using CoreGraphics, then pass the layer back to the button that initiated the operation (in this case, the destination button) via your completion handler (be sure to call that on the main thread). This should give you pretty good performance as your buttons layer's will be rendered off the main thread using CoreGraphics, then passed back to UIKit for presentation only after the layer has been rendered.
NSOperation sits atop GCD, has a marginal overhead but provides benefits in your case, such as the ability to set dependent prerequisites and prioritization.
Also, I'm pretty sure you shouldn't call UIColor in the GCD block you shared.
Hope this is helpful!
Related
I have some iOS sample code that if I tap on the main view and the tap handler changes the CALayer object's frame property, the layer object will animate to its position and size.
But if I put that animation inside ViewController's viewDidLoad:
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
ship1.frame = CGRectMake(0, 0, 300, 200);
then the animation won't happen. It will just use the (0, 0, 300, 200) with no animation. Why is that and how to make it work?
Several things.
First of all, the frame property is not animatable. Instead, you should be animating bounds (to change the size) and position (to move the layer).
To quote the docs:
Note: The frame property is not directly animatable. Instead you
should animate the appropriate combination of the bounds, anchorPoint
and position properties to achieve the desired result.
Second, you have to return to the event loop after adding a layer before changing an animatable property.
When you return and the system visits the event loop, the next time you change an animatable property, it creates an animation transaction to animate that change.
Since you created a layer, set it's frame, and then changed it, all before returning, your code simply adds the layer at it's final frame.
What you should do instead is to set up the layer, install it into it's parent layer, and return. You can use performSelector:withObject:afterDelay: to invoke the animation after the layer has been added:
- (void)viewDidLoad
{
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
[self performSelector: #selector(changeLayer)
withObject: nil
afterDelay: 0.0];
}
- (void) changeLayer
{
//Animate changes to bounds and position, not frame.
ship1.bounds = CGRectMake(0, 0, 300, 200);
//Note that position is based on the center of the layer by default, so you'll
//need to adjust your desired position.
ship1.position = CGPointMake(0,0);
}
You have specified no time delay or animation time. So the object will be moved before it is displayed, and you won't see it in the earlier position. You also need to return to the run loop for the display to show any animation.
There are 3 ways to trigger animations on iOS:
1. UIView Animation
You use a UIView.animateWithDuration method or other variant to directly specify that you want an animation. You can specify parameters such as duration, delay, a completion block and others.
2. Core Animation - Implicit Animation (DESIRED APPROACH)
As the name implies, this animation is triggered without an explicit command. In order to work though, you need to set the CALayer delegate, like this:
ship1.delegate = self;
All the parameters have default values until you say otherwise. For modifying these parameters, you need to call CATransaction:
[CATransition setAnimationDuration:0.2];
IMPORTANT: These CATransaction calls must be placed BEFORE the modifications of the layer's properties, because all animations happen asynchronous (in different runtime threads) and it will be too late for you if you put these calls after.
3. Core Animation - Explicit Animation
You create an CAAnimation (there are different CAAnimation subclass to choose from) and attach it to the layer:
CABasicAnimation *a = [CABasicAnimation alloc] init....
[ship1 addAnimation:a];
I hope this clarifies some doubts about the subject.
I have a UIButton of the custom button type, where I just set a background and border color on the underlying CALayer of the button. I then applied a mask to this underlying layer composed of a CAShapeLayer (to get two rounded corners) as described in this post.
So far so good. I animate the UIButton to grow in size. So far so good. See below (everything in the image is the _fusedBar, save the x-axis line snippet you see below it and the dollar amount text).
When I go to animate the UIButton to shrink using the same animation routine but different parameter values (affine translation with simultaneous frame height change), none of the UIButton (save for a sliver) is visible as the shrinking is taking place.
I've tried to have the CAShapeLayer that is the mask remove itself from its super layer, I've tried to set the mask property on the UIButton's layer to nil, etc., but none of these remove the mask such that I can see the UIButton animate (shrinking with affine translation and frame height change).
Here's the relevant animation code:
// Stackoverflow readers: Assume 'duration', 'scaledHeight' and 'delay'
// already defined.
void (^fusedBarChange)(void) = ^{
_fusedBar.transform = CGAffineTransformMakeTranslation(0, -scaledHeight);
[_fusedBar nd_setNewSizeHeight:scaledHeight];
};
[UIView animateWithDuration:duration
delay:delay
options:UIViewAnimationCurveEaseOut
animations:^(void) {
fusedBarChange();
}
completion:^(BOOL finished) {
// Other stuff not relevant to _fusedBar animation
}];
Before this animation gets called, I have the mask setup per the post cited above. That code is below. Note that '_fusedBar' is the UIButton at the heart of this discussion.
- (void)configureRoundedTop {
// YES: Start by creating the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_fusedBar.bounds
byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
cornerRadii:CGSizeMake(4.0, 4.0)];
// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = _fusedBar.bounds;
maskLayer.path = maskPath.CGPath;
// Set the newly created shape layer as the mask for the image view's layer
_fusedBar.layer.mask = maskLayer;
}
When growing the _fusedBar from a sliver to a few hundred points, the growth is animated, and you can see it.
Prior to using this CAShapeLayer mask, when shrinking the _fusedBar back to a sliver, I could see the animation (same code above, just a different value for 'scaledHeight'). Now, I cannot see the shrinking animation.
So, I know I need to somehow shake-off the effects of the masking layer before animating the shrinking operation, but the obvious things (to me) haven't worked. Here's an example of various things I've tried:
// None of these do the trick. I call them just before
// the shrinking animation is invoked:
[_fusedBar setNeedsLayout];
[_fusedBar setNeedsDisplay];
[_fusedBar.layer.mask removeFromSuperlayer];
_fusedBar.layer.mask = nil;
Clearly, I'm missing some nugget of CALayers, masking and perhaps even presentationLayer semantics.
Any guidance much appreciated!
Coming back to it after a couple of days, I saw my mistake (not visible from the extracts posted above).
Essentially, after the animation to grow or shrink the "fusedBar", I was reapplying a mask based on the view's frame. Since this would effectively execute in concert with the animation block, it was going to have a mask that the view was shrinking into.
I needed to put that re-apply mask code in my animation completion block or suppress it if the fusedBar was shrinking.
Thanks to all those who took a look to help out!
I'd like to draw a simple inset line in Interface Builder to separate some items in a list and make the UI a bit more tidy. I don't see any "line" or similar objects in the objects library and can't seem to find any drawing commands in Interface builder.
I use a very narrow UIView with backgroundColor set to the appropriate color.
There are no lines in the iPhone UI library. This functionality on Max OS X was supplied by NSBox, but on the iPhone there is no corresponding UI element.
If you're afraid a UIView might affect performance, you can draw a line in code using CoreGraphics' CAShapeLayers.
Every UIView has a CALayer, so draw the line and add it to the views CALayer.
You can do this either in a custom UIView's drawRect or in your view controller:
(Make sure to add Quartz framework to your project)
UIBezierPath *linePath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0,self.view.frame.size.width, 1)];
//shape layer for the line
CAShapeLayer *line = [CAShapeLayer layer];
line.path = [linePath CGPath];
line.fillColor = [[UIColor blackColor] CGColor];
line.frame = CGRectMake(xPosition, yPosition, self.view.frame.size.width,1);
[self.view.layer addSublayer:line];
In place of view, why not just use a label and set the appropriate background color?
I used a view and made it narrow & changed the color to make it look like a line. But my issue is I am using the line on a vertical scrollview and the lines also get scrolled.
Interface Builder does not allow you to draw the shadows essential for an inset line. If you work with Photoshop you know this.
The following code will draw an "inset" if the line is in front of a white/beige Background.
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 1.0f)];
line.backgroundColor = [UIColor colorWithRed:(200.0f/255.0f) green:(200.0f/255.0f) blue:(200.0f/255.0f) alpha:1.0f];
line.layer.shadowColor = [UIColor darkGrayColor].CGColor;
line.layer.shadowOffset = CGSizeMake(0.0f, 1.0f);
line.layer.shadowRadius = 0.5f;
line.layer.shadowOpacity = 0.4f;
line.layer.masksToBounds =NO;
[Background addSubview:line];
Remember to import < QuartzCore/QuartzCore.h>.
As an alliterative - in Interface Builder, add a view who's height is set to 2 points. Wire up a IBOutlet to that view and set its layer border color and width:
self.mySeparatorLine.layer.borderWidth = 1.0f;
self.mySeparatorLine.layer.borderColor = [UIColor lightGrayColor].CGColor;
You can use Progress View as an option for a horizontal line. Just change the tint color and progress to 1 which is 100%.
I have a view with has another view (called swipeView) inside it. I'm trying to gradient color it using the CAGradientLayer - the code is below
CAGradientLayer *layer = [CAGradientLayer layer];
layer.colors = [NSArray arrayWithObjects:(id)[UIColor darkGrayColor].CGColor,
[UIColor lightGrayColor].CGColor, nil];
layer.frame = swipeView.bounds;
[swipeView.layer insertSublayer:layer atIndex:0];
I'm calling this in the ViewDidLoad method of the main view controller. I see no effect - the view I'm trying to color remains just the same. What am I doing wrong? Are there any other settings/ordering etc. I should know about?
I don't know if this is the problem you're having, but I was just having the exact same symptom, and after a few hours of bafflement, I discovered my problem: I was creating a button, adding the gradient layer, and only later positioning the button. Initially, the button dimensions are all 0, and when setting layer.frame = swipeView.bounds; I was setting the gradient layer to have 0 dimensions too. I added code so that when sizing the button, I also sized the sublayers to match, and this fixed the problem. I'm actually using MonoTouch, so the code looks like this:
btn.Frame = new RectangleF([the dimensions you need]);
foreach(var s in btn.Layer.Sublayers)
{
s.Frame = btn.Bounds;
}
Maybe you're creating a view in code, and the view hasn't been sized or positioned, and so when you add the gradient it also has no size, and that's why you can't see it. When the view changes size (in ViewDidLayoutSubviews maybe), resize the gradient layer and see if that solves it.
That should work. Are you sure you hooked up your swipeView outlet correctly? Debug your -viewDidLoad and verify that swipeView is not nil. Also double check that the bounds (layer.frame) is something sensible.
It shouldn't be necessary, but you can try throwing in a [layer setNeedsDisplay] just to force the layer to render again, but I don't think that is your issue here.
CAGradientLayer *layer = [CAGradientLayer layer];
layer.colors = [NSArray arrayWithObjects:(id)[UIColor darkGrayColor].CGColor,
(id)[UIColor lightGrayColor].CGColor, nil];
layer.frame = swipeView.bounds;
[swipeView.layer insertSublayer:layer atIndex:0];
Make sure that swipeView is not nil and try swipeView.frame.
You don't need the nil terminator at the end of the colors array.
Also, I found insertSublayer did not work, with index either 0 or 1. Changing to simply addSublayer did the trick.
I am trying to rotate my object like shakes dice.
please suggest simplest way to implement it in my iphone application.
Any kind of sample code or documentation.
http://www.amazon.com/OpenGL-SuperBible-Comprehensive-Tutorial-Reference/dp/0321498828
If you want to do this without using OpenGL you can use a UIImageView and the set a series of animation images. By creating a series of images you can create a "rolling dice" effect. Here is an example of how to do this:
// create a UIImageView
UIImageView *rollDiceImageMainTemp = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"rollDiceAnimationImage1.png"]];
// position and size the UIImageView
rollDiceImageMainTemp.frame = CGRectMake(0, 0, 100, 100);
// create an array of images that will represent your animation (in this case the array contains 2 images but you will want more)
NSArray *savingHighScoreAnimationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"rollDiceAnimationImage1.png"],
[UIImage imageNamed:#"rollDiceAnimationImage2.png"],
nil];
// set the new UIImageView to a property in your view controller
self.viewController.rollDiceImage = rollDiceImageMainTemp;
// release the UIImageView that you created with alloc and init to avoid memory leak
[rollDiceImageMainTemp release];
// set the animation images and duration, and repeat count on your UIImageView
[self.viewController.rollDiceImageMain setAnimationImages:savingHighScoreAnimationImages];
[self.viewController.rollDiceImageMain setAnimationDuration:2.0];
[self.viewController.rollDiceImageMain.animationRepeatCount:3];
// start the animation
[self.viewController.rollDiceImageMain startAnimating];
// show the new UIImageView
[self.viewController.view addSubview:self.rollDiceImageMain];
You can modify the creation and setup including the size, position, number of images, the duration, repeat count, etc as needed. I've used this feature of UIImageView to create simple animation effects and it works great!
Please let me know if you try this and if it works for your needs. Also, post any follow up questions.
Bart
nice your example, but do you think it's possible to rotate / control the rotation with touch gesture ?