I've got a UIScrollView in my app and I have seen in some other apps that when the user scrolls, the top section fades out on scroll rather than just dissapearing out.
I really love this effect and want to achieve it. Any ideas how it can be done?
Simple 2020 solution:
import UIKit
class FadeTail: UIIView {
private lazy var gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
l.startPoint = CGPoint(x: 0.5, y: 0)
l.endPoint = CGPoint(x: 0.5, y: 1)
let baseColor = UIColor.white // for example
l.colors = [
baseColor.withAlphaComponent(0),
baseColor.withAlphaComponent(1),
].map{$0.cgColor}
layer.addSublayer(l)
return l
}()
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
Simply
autolayout that view in storyboard,
any shape or size you wish
on top of the view (text, image, webview, anything) you wish to be faded.
Easy.
Tip - gradients, circles, etc
If you need crazy circular/banded/etc fades, use the techniques here: https://stackoverflow.com/a/61174086/294884
EDIT: I've put this code up on github, see here.
See my answer to a similar question.
My solution is to subclass UIScrollView, and create a mask layer in the layoutSubviews method.
#import "FadingScrollView.h"
#import <QuartzCore/QuartzCore.h>
static float const fadePercentage = 0.2;
#implementation FadingScrollView
// ...
- (void)layoutSubviews
{
[super layoutSubviews];
NSObject * transparent = (NSObject *) [[UIColor colorWithWhite:0 alpha:0] CGColor];
NSObject * opaque = (NSObject *) [[UIColor colorWithWhite:0 alpha:1] CGColor];
CALayer * maskLayer = [CALayer layer];
maskLayer.frame = self.bounds;
CAGradientLayer * gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(self.bounds.origin.x, 0,
self.bounds.size.width, self.bounds.size.height);
gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque,
opaque, transparent, nil];
// Set percentage of scrollview that fades at top & bottom
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage],
[NSNumber numberWithFloat:1.0 - fadePercentage],
[NSNumber numberWithFloat:1], nil];
[maskLayer addSublayer:gradientLayer];
self.layer.mask = maskLayer;
}
#end
The code above fades the top and bottom of the UIScrollView from the background colour to transparent, but this can be easily changed to fade the top only (or fade to any colour you want).
Change this line to fade the top only:
// Fade top of scrollview only
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage],
[NSNumber numberWithFloat:1],
[NSNumber numberWithFloat:1], nil];
EDIT 2:
Or fade the top only by changing these two lines:
// Fade top of scrollview only
gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque, nil];
gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage], nil];
Or, fade the bottom only:
// Fade bottom of scrollview only
gradientLayer.colors = [NSArray arrayWithObjects: opaque, transparent, nil];
gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:1.0 - fadePercentage],
[NSNumber numberWithFloat:1], nil];
You can use a CAGradientLayer by
Adding the QuartzCore.framework to your project (see Linking to Library or Framework).
Add #import of the QuartzCore headers:
#import <QuartzCore/QuartzCore.h>
And then use CAGradientLayer:
- (void)addGradientMaskToView:(UIView *)view
{
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = #[(id)[[UIColor clearColor] CGColor], (id)[[UIColor whiteColor] CGColor]];
gradient.startPoint = CGPointMake(0.5, 0.0); // this is the default value, so this line is not needed
gradient.endPoint = CGPointMake(0.5, 0.20);
[view.layer setMask:gradient];
}
Note, this CAGradientLayer is a gradient from a color with alpha of 0.0 (e.g. clearColor) to a color to a color with alpha of 1.0 (e.g. whiteColor), not just from black to white. You can adjust the startPoint (the default value is probably fine) and the endPoint to adjust where you want the gradient to be applied.
And generally, when doing this with a UIScrollView, unless you want the gradient to scroll with you, you make the UIScrollView a subview of some other UIView and apply this gradient to that container view, not the scroll view itself.
Thanks to Fattie's answer I created the following UIView extension, in Swift, that takes care of the gradient fading and provides more styles (bottom, top, left right, vertical and horizontal) as well as fade percentage.
For any comments/recommendations, please let me know at the gist I've created, I try to keep both that and this answer up to date with any changes I add.
The Extension:
extension UIView {
enum UIViewFadeStyle {
case bottom
case top
case left
case right
case vertical
case horizontal
}
func fadeView(style: UIViewFadeStyle = .bottom, percentage: Double = 0.07) {
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
let startLocation = percentage
let endLocation = 1 - percentage
switch style {
case .bottom:
gradient.startPoint = CGPoint(x: 0.5, y: endLocation)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
case .top:
gradient.startPoint = CGPoint(x: 0.5, y: startLocation)
gradient.endPoint = CGPoint(x: 0.5, y: 0.0)
case .vertical:
gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]
case .left:
gradient.startPoint = CGPoint(x: startLocation, y: 0.5)
gradient.endPoint = CGPoint(x: 0.0, y: 0.5)
case .right:
gradient.startPoint = CGPoint(x: endLocation, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
case .horizontal:
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]
}
layer.mask = gradient
}
}
You add an alpha mask layer to a view containing your scroll view like this:
CALayer *mask = [CALayer layer];
CGImageRef maskRef = [UIImage imageNamed:#"scrollMask"].CGImage;
CGImageRef maskImage = CGImageMaskCreate(CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef), NULL, false);
mask.contents = (__bridge id)maskImage;
mask.frame = CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height);
view.layer.mask = mask;
view.layer.masksToBounds = YES;
CGImageRelease(maskImage);
where "scrollMask" is a grayscale image defining mask region: white == fully masked, black == not masked at all and gray == partially masked.
To create the effect you're looking for, the mask image would be black with a white gradient at the top like this:
For more details, take a look at the documentation for CGImageMaskCreate.
We built onto Steph Sharp's code to only fade when necessary. Our code is setup as a static utility method instead of a subclass so that we could reuse the method in our subclasses of UIScrollView and UITableView. Here is our static utility method:
#define kScrollViewFadeColorLight [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:0.0]
#define kScrollViewFadeColorDark [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:1.0]
#define kScrollViewScrollBarWidth 7.0
#define kScrollViewFadingEdgeLength 40.0
+ (void) applyFadeToScrollView:(UIScrollView*) scrollView {
CGFloat topOffset = -scrollView.contentInset.top;
CGFloat bottomOffset = scrollView.contentSize.height + scrollView.contentInset.bottom - scrollView.bounds.size.height;
CGFloat distanceFromTop = scrollView.contentOffset.y - topOffset;
CGFloat distanceFromBottom = bottomOffset - scrollView.contentOffset.y;
BOOL isAtTop = distanceFromTop < 1.0;
BOOL isAtBottom = distanceFromBottom < 1.0;
if (isAtTop && isAtBottom) {
// There is no scrolling to be done here, so don't fade anything!
scrollView.layer.mask = nil;
return;
}
NSObject* transparent = (NSObject*)[kScrollViewFadeColorLight CGColor];
NSObject* opaque = (NSObject*)[kScrollViewFadeColorDark CGColor];
CALayer* maskLayer = [CALayer layer];
maskLayer.frame = scrollView.bounds;
CALayer* scrollGutterLayer = [CALayer layer];
scrollGutterLayer.frame = CGRectMake(scrollView.bounds.size.width - kScrollViewScrollBarWidth, 0.0,
kScrollViewScrollBarWidth, scrollView.bounds.size.height);
scrollGutterLayer.backgroundColor = (__bridge CGColorRef)(opaque);
[maskLayer addSublayer:scrollGutterLayer];
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(0.0, 0.0, scrollView.bounds.size.width, scrollView.bounds.size.height);
CGFloat fadePercentage = kScrollViewFadingEdgeLength / scrollView.bounds.size.height;
NSMutableArray* colors = [[NSMutableArray alloc] init];
NSMutableArray* locations = [[NSMutableArray alloc] init];
if (!isAtTop) {
[colors addObjectsFromArray:#[transparent, opaque]];
[locations addObjectsFromArray:#[#0.0, [NSNumber numberWithFloat:fadePercentage]]];
}
if (!isAtBottom) {
[colors addObjectsFromArray:#[opaque, transparent]];
[locations addObjectsFromArray:#[[NSNumber numberWithFloat:1.0 - fadePercentage], #1.0]];
}
gradientLayer.colors = colors;
gradientLayer.locations = locations;
[maskLayer addSublayer:gradientLayer];
scrollView.layer.mask = maskLayer;
}
Here is our usage of that method.
FadingScrollView.h
#interface FadingScrollView : UIScrollView
#end
FadingScrollView.m
#implementation FadingScrollView
- (void) layoutSubviews {
[super layoutSubviews];
[Utils applyFadeToScrollView:self];
}
#end
FadingTableView.h
#interface FadingTableView : UITableView
#end
FadingTableView.m
#implementation FadingTableView
- (void) layoutSubviews {
[super layoutSubviews];
[Utils applyFadeToScrollView:self];
}
#end
Related
I found this code from tutorial and tried to use it:
CAGradientLayer *btnGradient = [CAGradientLayer layer];
btnGradient.frame = button.bounds;
btnGradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:102.0f / 255.0f green:102.0f / 255.0f blue:102.0f / 255.0f alpha:1.0f] CGColor],
(id)[[UIColor colorWithRed:51.0f / 255.0f green:51.0f / 255.0f blue:51.0f / 255.0f alpha:1.0f] CGColor],
nil];
[button.layer insertSublayer:btnGradient atIndex:0];
The code is in viewDidLoad method.
button is defined like this in .h file: #property (nonatomic, strong) IBOutlet UIButton *button;
and it's #synthesized in .m file and it's connected in interface builder
I am able to do other customization to the button like changing its background color (solid color) and changing the color of text. But when I try to use gradient color the background is just transparent.
I appreciate your help!
You need to set the gradient layer's frame to cover the entire button. Chance is that at viewDidLoad the button's size is zero, which makes the gradient layer's frame zero... And later when the button's size changes, the gradient layer's frame doesn't changed accordingly.
Subclassing UIButton and overriding layoutSubviews is a good idea
#interface MyButton : UIButton {
}
#implementation MyButton {
CAGradientLayer* _gradient;
}
-(id)init {
self = [super init];
_gradient = [CAGradientLayer layer];
_gradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:102.0f / 255.0f green:102.0f / 255.0f blue:102.0f / 255.0f alpha:1.0f] CGColor],
(id)[[UIColor colorWithRed:51.0f / 255.0f green:51.0f / 255.0f blue:51.0f / 255.0f alpha:1.0f] CGColor],
nil];
[self.layer insertSublayer:_gradient atIndex:0];
}
-(void)layoutSubviews {
[super layoutSubviews];
_gradient.frame = self.bounds;
}
#end
Another option is to use observeValueForKeyPath... to detect changes in the button's frame, and resize the layer accordingly. Not a very reusable solution thought.
I had this problem where the gradient wasn't appearing. Like #khanhnguyen said the problem was I setting it in viewDidLoad and because of that the button's bounds was unknown so nothing was appearing.
When I moved the code to viewDidLayoutSubviews it appeared. Here is the answer in Swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let purpleColor = UIColor(red: 129/255, green: 52/255, blue: 175/255, alpha: 1)
let pinkColor = UIColor(red: 221/255, green: 42/255, blue: 123/255, alpha: 1)
let yellowColor = UIColor(red: 254/255, green: 218/255, blue: 119/255, alpha: 1)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = yourButton.bounds
gradientLayer.colors = [purpleColor.cgColor, pinkColor.cgColor, yellowColor.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
yourButton.layer.insertSublayer(gradientLayer, at: 0)
}
If using a collection view cell add it to:
override func layoutSubviews() {
super.layoutSubviews()
}
Probably first you need to set the frame button. Try this.
+ (UIButton*) buttonWithGradient:(CGSize)size beginColor:(UIColor*)beginColor endColor:(UIColor*)endColor
{
CGRect frame = { CGPointZero, size };
UIButton* button = [[UIButton alloc] initWithFrame:frame];
CAGradientLayer* gradient = [CAGradientLayer layer];
gradient.frame = frame;
gradient.colors = #[(id)beginColor.CGColor, (id)endColor.CGColor];
[button.layer insertSublayer:gradient atIndex:0];
return [button autorelease];
}
I know how to create and animate a view like the one in the Share sub view of the new app store app that comes with iOS 6+ (see attached screenshot), but I don't know how to add that nice coloring effect with transparency on this view.
anyone can provide a code sample to make a UIView looks exactly the one in the screenshot?
P.S. the alpha property alone of UIView does not do such thing.
You can add this method to a UIView category and reuse as needed.
It applies a linear black gradient from "theColor" to transparent to the given view.
You should have QuartzCore.framework in your project in order to use the CAGradientLayer object.
+ (void)addLinearGradientToView:(UIView *)theView withColor:(UIColor *)theColor transparentToOpaque:(BOOL)transparentToOpaque
{
CAGradientLayer *gradient = [CAGradientLayer layer];
//the gradient layer must be positioned at the origin of the view
CGRect gradientFrame = theView.frame;
gradientFrame.origin.x = 0;
gradientFrame.origin.y = 0;
gradient.frame = gradientFrame;
//build the colors array for the gradient
NSArray *colors = [NSArray arrayWithObjects:
(id)[theColor CGColor],
(id)[[theColor colorWithAlphaComponent:0.9f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.6f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.4f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.3f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.1f] CGColor],
(id)[[UIColor clearColor] CGColor],
nil];
//reverse the color array if needed
if(transparentToOpaque)
{
colors = [[colors reverseObjectEnumerator] allObjects];
}
//apply the colors and the gradient to the view
gradient.colors = colors;
[theView.layer insertSublayer:gradient atIndex:0];
}
Please note that you should have the backgroundColor of theView set to clearColor so that it doesn't interfere with the gradient.
Also, for the results shown in the screenshot, the transparentToOpaque flag should be YES.
Here is PedroSilva's solution translated to Swift if anyone needs it (added vertical option as well)
class func addLinearGradientToView(view: UIView, colour: UIColor, transparntToOpaque: Bool, vertical: Bool)
{
let gradient = CAGradientLayer()
let gradientFrame = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height)
gradient.frame = gradientFrame
var colours = [
colour.CGColor,
colour.colorWithAlphaComponent(0.9).CGColor,
colour.colorWithAlphaComponent(0.8).CGColor,
colour.colorWithAlphaComponent(0.7).CGColor,
colour.colorWithAlphaComponent(0.6).CGColor,
colour.colorWithAlphaComponent(0.5).CGColor,
colour.colorWithAlphaComponent(0.4).CGColor,
colour.colorWithAlphaComponent(0.3).CGColor,
colour.colorWithAlphaComponent(0.2).CGColor,
colour.colorWithAlphaComponent(0.1).CGColor,
UIColor.clearColor().CGColor
]
if transparntToOpaque == true
{
colours = colours.reverse()
}
if vertical == true
{
gradient.startPoint = CGPointMake(0, 0.5)
gradient.endPoint = CGPointMake(1, 0.5)
}
gradient.colors = colours
view.layer.insertSublayer(gradient, atIndex: 0)
}
CAGradientLayer *layer = [CAGradientLayer layer];
layer.frame = yourView.bounds;
UIColor *blackColor = [UIColor colorWithWhite:0.42f alpha:1.0f];
UIColor *clearColor = [UIColor colorWithWhite:0.42f alpha:0.0f];
layer.colors = [NSArray arrayWithObjects:(id)clearColor.CGColor, (id)blackColor.CGColor, nil];
[myView.layer insertSublayer:layer atIndex:0];
Where layer is the layer of your view.
You can simply use a semi transparent png as a background to get the effect like this. Make sure you set the UIView's opaque to NO and you will be fine.
There is no magic needed for this effect to be achieved.
You should do it like Apple does it. UIImageViews with small, stretched images.
This view (the bottom-half square that includes the icons) uses an image that stretches and produces this effect.
You should follow this method too. Your views will be very light and it will just cost you a few tests in Photoshop to achieve the correct effect.
As I have noticed, Apple never uses gradient layers on the iPhone. It must be significantly more GPU consuming because it would have saved them a lot of images...
Here's a similar image to test with.
Hope it helps.
You just need to set the view's background color's alpha value to your desired value.
See attached zip code. Go to AlphaViewController's nib file and see it there
I have only set the alpha value for the subview to 70%. You can do it according to your requirements.
In Case if anyone need Swift 3.X
func addLinearGradientToView(view: UIView, colour: UIColor, transparntToOpaque: Bool, vertical: Bool)
{
let gradient = CAGradientLayer()
let gradientFrame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
gradient.frame = gradientFrame
var colours = [
colour.cgColor,
colour.withAlphaComponent(0.9).cgColor,
colour.withAlphaComponent(0.8).cgColor,
colour.withAlphaComponent(0.7).cgColor,
colour.withAlphaComponent(0.6).cgColor,
colour.withAlphaComponent(0.5).cgColor,
colour.withAlphaComponent(0.4).cgColor,
colour.withAlphaComponent(0.3).cgColor,
colour.withAlphaComponent(0.2).cgColor,
colour.withAlphaComponent(0.1).cgColor,
UIColor.clear.cgColor
]
if transparntToOpaque == true
{
colours = colours.reversed()
}
if vertical == true
{
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 0, y: 0.5)
}
gradient.colors = colours
view.layer.insertSublayer(gradient, at: 0)
}
Other way in Swift 3.x
let gradient = CAGradientLayer()
gradient.frame = view.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
view.layer.insertSublayer(gradient, at: 0)
I'm working on creating fancy looking UIbuttons by adding linear gradient to the button. However I'm not sure at which index I need to add the gradient. The code that I have currently places the gradient over the image/text.
How can I insert a sublayer to a UIButton under the text/image sublayer? It is important for me to keep the text and the image of a button visible!
+(void)addLinearGradientToView:(UIView*)view TopColor:(UIColor*)topColor BottomColor:(UIColor*)bottomColor
{
for(CALayer* layer in view.layer.sublayers)
{
if ([layer isKindOfClass:[CAGradientLayer class]])
{
[layer removeFromSuperlayer];
}
}
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5, 0);
gradientLayer.endPoint = CGPointMake(0.5,1);
gradientLayer.frame = view.bounds;
gradientLayer.colors = [NSArray arrayWithObjects:(id)[topColor CGColor], (id)[bottomColor CGColor], nil];
// [view.layer addSublayer:gradientLayer];
if(view.layer.sublayers.count>0)
{
[view.layer insertSublayer:gradientLayer atIndex:view.layer.sublayers.count-2];
}else {
[view.layer addSublayer:gradientLayer];
}
}
Add it to the layer of your custom button:
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = customButton.layer.bounds;
gradientLayer.colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithWhite:1.0f alpha:0.1f].CGColor,
(id)[UIColor colorWithWhite:0.4f alpha:0.5f].CGColor,
nil];
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f],
nil];
gradientLayer.cornerRadius = customButton.layer.cornerRadius;
[customButton.layer addSublayer:gradientLayer];
where customButton is your custom UIButton.
For what you are trying to achieve, always insert at index 0.
I wrote an article about this sort of thing recently that you may find interesting.
Swift 4
#IBOutlet weak var gButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let topGradientColor = UIColor.red
let bottomGradientColor = UIColor.yellow
let gradientLayer = CAGradientLayer()
gradientLayer.frame = gButton.bounds
gradientLayer.colors = [topGradientColor.cgColor, bottomGradientColor.cgColor]
//Vertical
//gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
//gradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
//Horizontal
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gButton.layer.insertSublayer(gradientLayer, at: 0)
}
Here is the code to achieve this
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = <#View Variable Name#>.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)([UIColor colorWithRed:0.337 green:0.596 blue:1.000 alpha:1.000].CGColor),(id)([UIColor colorWithRed:0.757 green:0.459 blue:1.000 alpha:1.000].CGColor),(id)([UIColor colorWithRed:0.310 green:0.835 blue:1.000 alpha:1.000].CGColor),nil];
gradient.startPoint = CGPointMake(0.5,0.0);
gradient.endPoint = CGPointMake(0.5,1.0);
[<#View Variable name#>.layer insertSublayer:gradient atIndex:0];
I got this tool that makes it easy to select your colors and automatically output the code for the gradient. Very handy I use it everyday
itunes.apple.com/gb/app/gradient-creator/id1031070259?mt=12
I'm trying to get my view to do a nice shining animation to catch the user's eyes. Any ideas how to implement this?
Here's what I have so far:
[UIView beginAnimations:#"viewShine" context:self.view];
[UIView setAnimationRepeatAutoreverses:NO];
[UIView setAnimationRepeatCount:0];
[UIView setAnimationDuration:2];
[UIView setAnimationDelegate:self];
//Do nice shining animation here
[UIView commitAnimations];
By shine I mean something like what happens to the "slide to unlock" text when you open the iPhone, or anything that's easy to do and looks nice.
I wrote a method that can be called on any UIView to give it the shine you are looking for without needing to include an image:
-(void)AddShineAnimationToView:(UIView*)aView
{
CAGradientLayer *gradient = [CAGradientLayer layer];
[gradient setStartPoint:CGPointMake(0, 0)];
[gradient setEndPoint:CGPointMake(1, 0)];
gradient.frame = CGRectMake(0, 0, aView.bounds.size.width*3, aView.bounds.size.height);
float lowerAlpha = 0.78;
gradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithWhite:1 alpha:lowerAlpha] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:lowerAlpha] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1.0] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1.0] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1.0] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:lowerAlpha] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:lowerAlpha] CGColor],
nil];
gradient.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.4],
[NSNumber numberWithFloat:0.45],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.55],
[NSNumber numberWithFloat:0.6],
[NSNumber numberWithFloat:1.0],
nil];
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration = 2;
theAnimation.repeatCount = INFINITY;
theAnimation.autoreverses = NO;
theAnimation.removedOnCompletion = NO;
theAnimation.fillMode = kCAFillModeForwards;
theAnimation.fromValue=[NSNumber numberWithFloat:-aView.frame.size.width*2];
theAnimation.toValue=[NSNumber numberWithFloat:0];
[gradient addAnimation:theAnimation forKey:#"animateLayer"];
aView.layer.mask = gradient;
}
Figured it out.
Here's my code in case you want to do something similar:
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[whiteView setBackgroundColor:[UIColor whiteColor]];
[whiteView setUserInteractionEnabled:NO];
[self.view addSubview:whiteView];
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:1.0f green:1.0f blue:1.0f alpha:0.0f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"ShineMask.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(-whiteView.frame.size.width,
0.0f,
whiteView.frame.size.width * 2,
whiteView.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:self.view.frame.size.width * 9];
maskAnim.repeatCount = HUGE_VALF;
maskAnim.duration = 3.0f;
[maskLayer addAnimation:maskAnim forKey:#"shineAnim"];
whiteView.layer.mask = maskLayer;
Using this image:
Here is the swift code for the shine effect
class Animate {
/// Add a persistent shimmer animation. Usage: `Animate.shimmer(myView)`
static func shimmer(view: UIView) {
let gradient = CAGradientLayer()
gradient.startPoint = CGPointMake(0, 0)
gradient.endPoint = CGPointMake(1, -0.02)
gradient.frame = CGRectMake(0, 0, view.bounds.size.width*3, view.bounds.size.height)
let lowerAlpha: CGFloat = 0.7
let solid = UIColor(white: 1, alpha: 1).CGColor
let clear = UIColor(white: 1, alpha: lowerAlpha).CGColor
gradient.colors = [ solid, solid, clear, clear, solid, solid ]
gradient.locations = [ 0, 0.3, 0.45, 0.55, 0.7, 1 ]
let theAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.translation.x")
theAnimation.duration = 4
theAnimation.repeatCount = Float.infinity
theAnimation.autoreverses = false
theAnimation.removedOnCompletion = false
theAnimation.fillMode = kCAFillModeForwards
theAnimation.fromValue = -view.frame.size.width * 2
theAnimation.toValue = 0
gradient.addAnimation(theAnimation, forKey: "animateLayer")
view.layer.mask = gradient
}
}
Swift 3.0
func shimmer(view: UIView) {
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: -0.02)
gradient.frame = CGRect(x: 0, y: 0, width: view.bounds.size.width*3, height: view.bounds.size.height)
let lowerAlpha: CGFloat = 0.7
let solid = UIColor(white: 1, alpha: 1).cgColor
let clear = UIColor(white: 1, alpha: lowerAlpha).cgColor
gradient.colors = [ solid, solid, clear, clear, solid, solid ]
gradient.locations = [ 0, 0.3, 0.45, 0.55, 0.7, 1 ]
let theAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.translation.x")
theAnimation.duration = 2
theAnimation.repeatCount = Float.infinity
theAnimation.autoreverses = false
theAnimation.isRemovedOnCompletion = false
theAnimation.fillMode = kCAFillModeForwards
theAnimation.fromValue = -view.frame.size.width * 2
theAnimation.toValue = 0
gradient.add(theAnimation, forKey: "animateLayer")
view.layer.mask = gradient
}
It might be worth to mention: in case of shining progress label, like in facebook paper app, or iOS lock screen, you might consider using https://github.com/facebook/Shimmer.
Swift 5.0
func shimmer(){
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: -0.02)
gradient.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width*3, height: self.bounds.size.height)
let lowerAlpha: CGFloat = 0.8
let solid = UIColor(white: 1, alpha: 1).cgColor
//let verdeVXM = UIColor(red:0.77, green:0.84, blue:0.00, alpha:1.0)
let clear = UIColor(white: 1, alpha: lowerAlpha).cgColor
gradient.colors = [ solid, solid, clear, clear, solid, solid ]
gradient.locations = [ 0, 0.3, 0.45, 0.55, 0.7, 1 ]
let theAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.translation.x")
theAnimation.duration = 2
theAnimation.repeatCount = Float.infinity
theAnimation.autoreverses = false
theAnimation.isRemovedOnCompletion = false
theAnimation.fillMode = CAMediaTimingFillMode.forwards
theAnimation.fromValue = -self.frame.size.width * 2
theAnimation.toValue = 0
gradient.add(theAnimation, forKey: "animateLayer")
self.layer.mask = gradient
}
Does anyone have any experience in applying a Gradient to a CAShapeLayer? CAShapeLayer is a fantastic layer class, but it appears to only support solid fill coloring, whereas I'd like it to have a gradient fill (actually an animatable gradient at that).
Everything else to do with CAShapeLayer (shadows, shapes, stroke color, animatable shape path) is fantastic.
I've tried placing a CAGradientLayer inside a CAShapeLayer, or indeed setting the CAShapeLayer as the mask of the GradientLayer and adding both to a container layer, but these don't have the right outcome.
Should I subclass CAShapeLayer, or is there a better way forward?
Thanks.
You could use the path of your shape to create a masking layer and apply that on the gradient layer, like this:
UIView *v = [[UIView alloc] initWithFrame:self.window.frame];
CAShapeLayer *gradientMask = [CAShapeLayer layer];
gradientMask.fillColor = [[UIColor clearColor] CGColor];
gradientMask.strokeColor = [[UIColor blackColor] CGColor];
gradientMask.lineWidth = 4;
gradientMask.frame = CGRectMake(0, 0, v.bounds.size.width, v.bounds.size.height);
CGMutablePathRef t = CGPathCreateMutable();
CGPathMoveToPoint(t, NULL, 0, 0);
CGPathAddLineToPoint(t, NULL, v.bounds.size.width, v.bounds.size.height);
gradientMask.path = t;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = CGRectMake(0, 0, v.bounds.size.width, v.bounds.size.height);
NSMutableArray *colors = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[colors addObject:(id)[[UIColor colorWithHue:(0.1 * i) saturation:1 brightness:.8 alpha:1] CGColor]];
}
gradientLayer.colors = colors;
[gradientLayer setMask:gradientMask];
[v.layer addSublayer:gradientLayer];
If you want to also use the shadows, you would have to place a "duplicate" of the shape layer under the gradient layer, recycling the same path reference.
Many thanks to #Palimondo for a great answer!
In case someone is looking for Swift 4 + filling animation code of this solution:
let myView = UIView(frame: .init(x: 0, y: 0, width: 200, height: 150))
view.addSubview(myView)
myView.center = view.center
// Start and finish point
let startPoint = CGPoint(x: myView.bounds.minX, y: myView.bounds.midY)
let finishPoint = CGPoint(x: myView.bounds.maxX, y: myView.bounds.midY)
// Path
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: finishPoint)
// Gradient Mask
let gradientMask = CAShapeLayer()
let lineHeight = myView.frame.height
gradientMask.fillColor = UIColor.clear.cgColor
gradientMask.strokeColor = UIColor.black.cgColor
gradientMask.lineWidth = lineHeight
gradientMask.frame = myView.bounds
gradientMask.path = path.cgPath
// Gradient Layer
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
// make sure to use .cgColor
gradientLayer.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer.frame = myView.bounds
gradientLayer.mask = gradientMask
myView.layer.addSublayer(gradientLayer)
// Corner radius
myView.layer.cornerRadius = 10
myView.clipsToBounds = true
Extra. In case you also need a "filling animation", add this lines:
// Filling animation
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 10
gradientMask.add(animation, forKey: "LineAnimation")
This is a great solution, but you might encounter unexpected problems if you're creating a category on CAShapeLayer where you don't immediately have the view.
See Setting correct frame of a newly created CAShapeLayer
Bottom line, get the bounds of the path then set the gradient mask's frame using the path bounds and translate as necessary. Good thing here is that by using the path's bounds rather than any other frame, the gradient will only fit within the path bounds (assuming that's what you want).
// Category on CAShapeLayer
CGRect pathBounds = CGPathGetBoundingBox(self.path);
CAShapeLayer *gradientMask = [CAShapeLayer layer];
gradientMask.fillColor = [[UIColor blackColor] CGColor];
gradientMask.frame = CGRectMake(0, 0, pathBounds.size.width, pathBounds.size.height);
gradientMask.path = self.path;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = CGRectMake(0, 0, pathBounds.size.width, pathBounds.size.height);
NSMutableArray *colors = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[colors addObject:(id)[[UIColor colorWithHue:(0.1 * i) saturation:1 brightness:.8 alpha:1] CGColor]];
}
gradientLayer.colors = colors;
[gradientLayer setMask:gradientMask];
[self addSublayer:gradientLayer];