Jason Jardim asked (4 Months ago):
This is just a screen shot I found with someone posting a similar question. I am trying to fade out he top/ bottom cells in a tableview. How do I achieve this effect?
First of all, Jason, I am sorry it took so long. I was extremely busy during the past few months but I kept your e-mail at the bottom of my inbox as something that I am really interested in to give a good answer to.
Let me make it up for you by proposing several solutions to your question as well as show one that I find the coolest.
First of all by looking at the sample screenshot we see that there is a border around the table view. This tells us that the table view does not fill the entire screen but there is obviously an imageView responsible for the ornamentals surrounding the table view.
If we go with this imageView then the next question is whether the image is below or above the table view itself. The first instinct might be “below!” but that can be treacherous and cause you more work than is actually needed to achieve this effect. You could just as well have the part for the table view “cut out” in the image, i.e. have the center portion be Alpha 0 and the top and bottom would be an alpha gradient from 100% to 0%.
A designer versed with Photoshop could easily create something like this for you. Then you put this imageView on top of your table view, size the table view to fit inside the border. Finally you will want to make sure that userInteraction is disabled on the imageView, because otherwise no touches would be reaching the table view.
While this solution is simple it does not satisfy me personally. As developer I want to achieve such an effect entirely in code because then it has the added advantage of being independent or resolution or interface orientation.
The first thing we’ll try is to mask the layer of the table view. On iOS each UIView has a CALayer that takes care of the actual drawing. And each CALayer has a mask property where you can set another layer to mask out parts of the host layer.
For masking layers it does not matter which colors you use because only the alpha value of each pixel is considered for the composition. 100% alpha means that a layer pixel shows fully, 0% alpha causes a layer pixel to be transparent.
For this tutorial I created a new navigation-based app without CoreData. We also need the QuartzCore.framework for the advanced layer handling methods. A useful layer type for this purpose is CAGradientLayer which exists since iOS version 3.0.
RootViewController.h
#import <UIKit/UIKit.h> #import <QuartzCore/QuartzCore.h> @interface RootViewController : UITableViewController { CAGradientLayer *maskLayer; } @end |
For the sake of simplicity we create the mask in viewWillAppear because at this point the table view has already been resized to the proper size. If we would create it earlier (or if you plan to support different sizes) then you would also need code to adjust the layer bounds to fit.
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (!maskLayer) { maskLayer = [CAGradientLayer layer]; CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor; CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor; maskLayer.colors = [NSArray arrayWithObjects:(id)outerColor, (id)innerColor, (id)innerColor, (id)outerColor, nil]; maskLayer.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:0.2], [NSNumber numberWithFloat:0.8], [NSNumber numberWithFloat:1.0], nil]; maskLayer.bounds = CGRectMake(0, 0, self.tableView.frame.size.width, self.tableView.frame.size.height); maskLayer.anchorPoint = CGPointZero; self.view.layer.mask = maskLayer; } } |
We only create the layer once. Since there can be only one mask layer at a time we combine the top and the bottom gradient into one where the outer areas will be 20% of the entire height. The default anchor point is the center of the layer, so we change that to the top left.
If we stopped here then you would get the gradients, but they would move together with the contents of the table view. Luckily table views are UIScrollView child classes and thus you can also implement the UIScrollViewDelegate methods. We are using the one that fires on each movement to adjust the position of the masking layer.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { [CATransaction begin]; [CATransaction setDisableActions:YES]; maskLayer.position = CGPointMake(0, scrollView.contentOffset.y); [CATransaction commit]; } |
Note that we also need to disable actions because position is an animatable property on CALayer. If you set that this triggers an implicit animation which would cause the masking layer to lag behind. With actions disabled the layer position is set right away.
There’s another problem that only becomes apparent if you start scrolling and the vertical scroll bar shows: it is also affected by the mask which you probably don’t want because it looks weird.
So instead of actually use the mask for the regular layer masking we add it as a sublayer. Because now the colors are just composited on top of the table view we have to reverse them or else the inside would be whitened out and only the outside would show.
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (!maskLayer) { maskLayer = [CAGradientLayer layer]; CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor; CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor; maskLayer.colors = [NSArray arrayWithObjects:(id)outerColor, (id)innerColor, (id)innerColor, (id)outerColor, nil]; maskLayer.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:0.2], [NSNumber numberWithFloat:0.8], [NSNumber numberWithFloat:1.0], nil]; maskLayer.bounds = CGRectMake(0, 0, self.tableView.frame.size.width, self.tableView.frame.size.height); maskLayer.anchorPoint = CGPointZero; [self.view.layer addSublayer:maskLayer]; } } |
This change brought us full circle to essentially adding an imageView on top of the table view to mask out the edge gradients. In this example the rootViewController’s view is a UITableView and so we have no other place to add this sublayer to. But if the table View were itself a subview of some larger view we could add the gradient layer there and not having to deal with the scrolling.
Actually it’s not entirely true that there’s no other layer besides the table view one. You can always move to the view’s superview and add it there, but this is considered bad form, a view should only fiddle with it’s own descendants.
This tutorial has shown how you can employ a CAGradientLayer to block out parts of another view. With the same technique you could for example use a CAShapeLayer to mask out an irregular portion of any view. Or you could use a plain image as contents of a plain CALayer for such an effect.
Yet another possibility – if you want the gradients to be built into the table view itself – would be to subclass UITableView and do the gradient management there. Either use one large gradient or have two UIViews that draw the gradients individually. There you would put the repositioning of the gradients into the layoutSubviews property instead of the scrollview delegate method.
Happy Masking!
Categories: Q&A
Please tell me what’s bad in making a PNG and putting it’s above a table?
It’s absolutely easy.
If you only do what’s easy you never become a better developer. 😉
I’ve used this example to demonstrate layer masking which many people might not have heard about before.
Yes, i’m agree. Thanks for sample, because I am one of them who never heard 🙂
The outer color always be white. How to change it to another color?
It’s awesome! I’ve spent 2 hours browsing all those “transparent png” solutions until i found this simple and perfect one. Thanks a lot.
I implemented it slightly differently, in that I made a tableView a subview of a UIView. I then use the gradient layer as the layer mask of the UIView. Because I do it this way, I only needed to follow this tutorial up to the point where one implements the scrollview delegate methods. I didn’t need to do that. And the faded edges don’t move with the content of the table view.
It’s perhaps not ideal, because you wrap your TableView is inside a UIView, so you need to create a readonly property to your tableview.
i am implementing a button on a scrollbar and i want to fade the edges….can i implement this on tat too or is it meant only for tables?
This works for any scroll view since UITableView is just a sub-class of UIScrollView.
Hi, I tried using this code on my UITableView but instead of getting the gradient I get that no cells are displaying, any ideas on that?
Thank you for the great post!
I used some of the ideas in your code to make a transparent view between the section header and the actual cells for that section (I did not want the cells to be visible underneath the transparent part when scrolling).
Especially this line saved my day: [CATransaction setDisableActions:YES];
Thank you for this great code! Just a quick newbie question… if I wanted to implement this with the gradients on the left/right sides rather than the top/bottom, how would I go about doing that?
Thanks a lot, great post! I have one question though: how do you change the distance of the inner gradients? (if that makes sense..)
A somewhat more “comprehensive” solution based on this one: http://stackoverflow.com/a/21262188/2242359
Mostly it handles tableViews inside bigger view, handle every background or other views behind the table, and handling scrolling to the top and the bottom of the table view.