Initial commit
This commit is contained in:
Executable
+468
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
Copyright (c) 2014-present, Facebook, Inc.
|
||||
All rights reserved.
|
||||
|
||||
This source code is licensed under the BSD-style license found in the
|
||||
LICENSE file in the root directory of this source tree. An additional grant
|
||||
of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "FBShimmeringLayer.h"
|
||||
|
||||
#import <QuartzCore/CAAnimation.h>
|
||||
#import <QuartzCore/CAGradientLayer.h>
|
||||
#import <QuartzCore/CATransaction.h>
|
||||
|
||||
#import <UIKit/UIGeometry.h>
|
||||
#import <UIKit/UIColor.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
|
||||
#endif
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
UIKIT_EXTERN float UIAnimationDragCoefficient(void); // UIKit private drag coeffient, use judiciously
|
||||
#endif
|
||||
|
||||
static CGFloat FBShimmeringLayerDragCoefficient(void)
|
||||
{
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
return UIAnimationDragCoefficient();
|
||||
#else
|
||||
return 1.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void FBShimmeringLayerAnimationApplyDragCoefficient(CAAnimation *animation)
|
||||
{
|
||||
CGFloat k = FBShimmeringLayerDragCoefficient();
|
||||
|
||||
if (k != 0 && k != 1) {
|
||||
animation.speed = 1 / k;
|
||||
}
|
||||
}
|
||||
|
||||
// animations keys
|
||||
static NSString *const kFBShimmerSlideAnimationKey = @"slide";
|
||||
static NSString *const kFBFadeAnimationKey = @"fade";
|
||||
static NSString *const kFBEndFadeAnimationKey = @"fade-end";
|
||||
|
||||
static CABasicAnimation *fade_animation(id delegate, CALayer *layer, CGFloat opacity, CFTimeInterval duration)
|
||||
{
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
||||
animation.delegate = delegate;
|
||||
animation.fromValue = @([(layer.presentationLayer ?: layer) opacity]);
|
||||
animation.toValue = @(opacity);
|
||||
animation.fillMode = kCAFillModeBoth;
|
||||
animation.removedOnCompletion = NO;
|
||||
animation.duration = duration;
|
||||
FBShimmeringLayerAnimationApplyDragCoefficient(animation);
|
||||
return animation;
|
||||
}
|
||||
|
||||
static CABasicAnimation *shimmer_begin_fade_animation(id delegate, CALayer *layer, CGFloat opacity, CGFloat duration)
|
||||
{
|
||||
return fade_animation(delegate, layer, opacity, duration);
|
||||
}
|
||||
|
||||
static CABasicAnimation *shimmer_end_fade_animation(id delegate, CALayer *layer, CGFloat opacity, CGFloat duration)
|
||||
{
|
||||
CABasicAnimation *animation = fade_animation(delegate, layer, opacity, duration);
|
||||
[animation setValue:@YES forKey:kFBEndFadeAnimationKey];
|
||||
return animation;
|
||||
}
|
||||
|
||||
static CABasicAnimation *shimmer_slide_animation(id delegate, CFTimeInterval duration, FBShimmerDirection direction)
|
||||
{
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
|
||||
animation.delegate = delegate;
|
||||
animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
|
||||
animation.duration = duration;
|
||||
animation.repeatCount = HUGE_VALF;
|
||||
FBShimmeringLayerAnimationApplyDragCoefficient(animation);
|
||||
if (direction == FBShimmerDirectionLeft ||
|
||||
direction == FBShimmerDirectionUp) {
|
||||
animation.speed = -fabsf(animation.speed);
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
// take a shimmer slide animation and turns into repeating
|
||||
static CAAnimation *shimmer_slide_repeat(CAAnimation *a, CFTimeInterval duration, FBShimmerDirection direction)
|
||||
{
|
||||
CAAnimation *anim = [a copy];
|
||||
anim.repeatCount = HUGE_VALF;
|
||||
anim.duration = duration;
|
||||
anim.speed = (direction == FBShimmerDirectionRight || direction == FBShimmerDirectionDown) ? fabsf(anim.speed) : -fabsf(anim.speed);
|
||||
return anim;
|
||||
}
|
||||
|
||||
// take a shimmer slide animation and turns into finish
|
||||
static CAAnimation *shimmer_slide_finish(CAAnimation *a)
|
||||
{
|
||||
CAAnimation *anim = [a copy];
|
||||
anim.repeatCount = 0;
|
||||
return anim;
|
||||
}
|
||||
|
||||
@interface FBShimmeringMaskLayer : CAGradientLayer
|
||||
@property (readonly, nonatomic) CALayer *fadeLayer;
|
||||
@end
|
||||
|
||||
@implementation FBShimmeringMaskLayer
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (nil != self) {
|
||||
_fadeLayer = [[CALayer alloc] init];
|
||||
_fadeLayer.backgroundColor = [UIColor whiteColor].CGColor;
|
||||
[self addSublayer:_fadeLayer];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSublayers
|
||||
{
|
||||
[super layoutSublayers];
|
||||
CGRect r = self.bounds;
|
||||
_fadeLayer.bounds = r;
|
||||
_fadeLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FBShimmeringLayer ()
|
||||
@property (strong, nonatomic) FBShimmeringMaskLayer *maskLayer;
|
||||
@end
|
||||
|
||||
@implementation FBShimmeringLayer
|
||||
{
|
||||
CALayer *_contentLayer;
|
||||
FBShimmeringMaskLayer *_maskLayer;
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
@synthesize shimmering = _shimmering;
|
||||
@synthesize shimmeringPauseDuration = _shimmeringPauseDuration;
|
||||
@synthesize shimmeringAnimationOpacity = _shimmeringAnimationOpacity;
|
||||
@synthesize shimmeringOpacity = _shimmeringOpacity;
|
||||
@synthesize shimmeringSpeed = _shimmeringSpeed;
|
||||
@synthesize shimmeringHighlightLength = _shimmeringHighlightLength;
|
||||
@synthesize shimmeringDirection = _shimmeringDirection;
|
||||
@synthesize shimmeringFadeTime = _shimmeringFadeTime;
|
||||
@synthesize shimmeringBeginFadeDuration = _shimmeringBeginFadeDuration;
|
||||
@synthesize shimmeringEndFadeDuration = _shimmeringEndFadeDuration;
|
||||
@dynamic shimmeringHighlightWidth;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (nil != self) {
|
||||
// default configuration
|
||||
_shimmeringPauseDuration = 0.4;
|
||||
_shimmeringSpeed = 230.0;
|
||||
_shimmeringHighlightLength = 1.0;
|
||||
_shimmeringAnimationOpacity = 0.5;
|
||||
_shimmeringOpacity = 1.0;
|
||||
_shimmeringDirection = FBShimmerDirectionRight;
|
||||
_shimmeringBeginFadeDuration = 0.1;
|
||||
_shimmeringEndFadeDuration = 0.3;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setContentLayer:(CALayer *)contentLayer
|
||||
{
|
||||
// reset mask
|
||||
self.maskLayer = nil;
|
||||
|
||||
// note content layer and add for display
|
||||
_contentLayer = contentLayer;
|
||||
self.sublayers = contentLayer ? @[contentLayer] : nil;
|
||||
|
||||
// update shimmering animation
|
||||
[self _updateShimmering];
|
||||
}
|
||||
|
||||
- (void)setShimmering:(BOOL)shimmering
|
||||
{
|
||||
if (shimmering != _shimmering) {
|
||||
_shimmering = shimmering;
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringSpeed:(CGFloat)speed
|
||||
{
|
||||
if (speed != _shimmeringSpeed) {
|
||||
_shimmeringSpeed = speed;
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringHighlightLength:(CGFloat)length
|
||||
{
|
||||
if (length != _shimmeringHighlightLength) {
|
||||
_shimmeringHighlightLength = length;
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringDirection:(FBShimmerDirection)direction
|
||||
{
|
||||
if (direction != _shimmeringDirection) {
|
||||
_shimmeringDirection = direction;
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringPauseDuration:(CFTimeInterval)duration
|
||||
{
|
||||
if (duration != _shimmeringPauseDuration) {
|
||||
_shimmeringPauseDuration = duration;
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringAnimationOpacity:(CGFloat)shimmeringAnimationOpacity
|
||||
{
|
||||
if (shimmeringAnimationOpacity != _shimmeringAnimationOpacity) {
|
||||
_shimmeringAnimationOpacity = shimmeringAnimationOpacity;
|
||||
[self _updateMaskColors];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShimmeringOpacity:(CGFloat)shimmeringOpacity
|
||||
{
|
||||
if (shimmeringOpacity != _shimmeringOpacity) {
|
||||
_shimmeringOpacity = shimmeringOpacity;
|
||||
[self _updateMaskColors];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSublayers
|
||||
{
|
||||
[super layoutSublayers];
|
||||
CGRect r = self.bounds;
|
||||
_contentLayer.anchorPoint = CGPointMake(0.5, 0.5);
|
||||
_contentLayer.bounds = r;
|
||||
_contentLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
|
||||
|
||||
if (nil != _maskLayer) {
|
||||
[self _updateMaskLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
if (!CGRectEqualToRect(self.bounds, bounds)) {
|
||||
[super setBounds:bounds];
|
||||
|
||||
[self _updateShimmering];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Internal
|
||||
|
||||
- (void)_clearMask
|
||||
{
|
||||
if (nil == _maskLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL disableActions = [CATransaction disableActions];
|
||||
[CATransaction setDisableActions:YES];
|
||||
|
||||
self.maskLayer = nil;
|
||||
_contentLayer.mask = nil;
|
||||
|
||||
[CATransaction setDisableActions:disableActions];
|
||||
}
|
||||
|
||||
- (void)_createMaskIfNeeded
|
||||
{
|
||||
if (_shimmering && !_maskLayer) {
|
||||
_maskLayer = [FBShimmeringMaskLayer layer];
|
||||
_maskLayer.delegate = self;
|
||||
_contentLayer.mask = _maskLayer;
|
||||
[self _updateMaskColors];
|
||||
[self _updateMaskLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_updateMaskColors
|
||||
{
|
||||
if (nil == _maskLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We create a gradient to be used as a mask.
|
||||
// In a mask, the colors do not matter, it's the alpha that decides the degree of masking.
|
||||
UIColor *maskedColor = [UIColor colorWithWhite:1.0 alpha:_shimmeringOpacity];
|
||||
UIColor *unmaskedColor = [UIColor colorWithWhite:1.0 alpha:_shimmeringAnimationOpacity];
|
||||
|
||||
// Create a gradient from masked to unmasked to masked.
|
||||
_maskLayer.colors = @[(__bridge id)maskedColor.CGColor, (__bridge id)unmaskedColor.CGColor, (__bridge id)maskedColor.CGColor];
|
||||
}
|
||||
|
||||
- (void)_updateMaskLayout
|
||||
{
|
||||
// Everything outside the mask layer is hidden, so we need to create a mask long enough for the shimmered layer to be always covered by the mask.
|
||||
CGFloat length = 0.0f;
|
||||
if (_shimmeringDirection == FBShimmerDirectionDown ||
|
||||
_shimmeringDirection == FBShimmerDirectionUp) {
|
||||
length = CGRectGetHeight(_contentLayer.bounds);
|
||||
} else {
|
||||
length = CGRectGetWidth(_contentLayer.bounds);
|
||||
}
|
||||
if (0 == length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// extra distance for the gradient to travel during the pause.
|
||||
CGFloat extraDistance = length + _shimmeringSpeed * _shimmeringPauseDuration;
|
||||
|
||||
// compute how far the shimmering goes
|
||||
CGFloat fullShimmerLength = length * 3.0f + extraDistance;
|
||||
CGFloat travelDistance = length * 2.0f + extraDistance;
|
||||
|
||||
// position the gradient for the desired width
|
||||
CGFloat highlightOutsideLength = (1.0 - _shimmeringHighlightLength) / 2.0;
|
||||
_maskLayer.locations = @[@(highlightOutsideLength),
|
||||
@(0.5),
|
||||
@(1.0 - highlightOutsideLength)];
|
||||
|
||||
CGFloat startPoint = (length + extraDistance) / fullShimmerLength;
|
||||
CGFloat endPoint = travelDistance / fullShimmerLength;
|
||||
|
||||
// position for the start of the animation
|
||||
_maskLayer.anchorPoint = CGPointZero;
|
||||
if (_shimmeringDirection == FBShimmerDirectionDown ||
|
||||
_shimmeringDirection == FBShimmerDirectionUp) {
|
||||
_maskLayer.startPoint = CGPointMake(0.0, startPoint);
|
||||
_maskLayer.endPoint = CGPointMake(0.0, endPoint);
|
||||
_maskLayer.position = CGPointMake(0.0, -travelDistance);
|
||||
_maskLayer.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(_contentLayer.bounds), fullShimmerLength);
|
||||
} else {
|
||||
_maskLayer.startPoint = CGPointMake(startPoint, 0.0);
|
||||
_maskLayer.endPoint = CGPointMake(endPoint, 0.0);
|
||||
_maskLayer.position = CGPointMake(-travelDistance, 0.0);
|
||||
_maskLayer.bounds = CGRectMake(0.0, 0.0, fullShimmerLength, CGRectGetHeight(_contentLayer.bounds));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_updateShimmering
|
||||
{
|
||||
// create mask if needed
|
||||
[self _createMaskIfNeeded];
|
||||
|
||||
// if not shimmering and no mask, noop
|
||||
if (!_shimmering && !_maskLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure layed out
|
||||
[self layoutIfNeeded];
|
||||
|
||||
BOOL disableActions = [CATransaction disableActions];
|
||||
if (!_shimmering) {
|
||||
if (disableActions) {
|
||||
// simply remove mask
|
||||
[self _clearMask];
|
||||
} else {
|
||||
// end slide
|
||||
CFTimeInterval slideEndTime = 0;
|
||||
|
||||
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
|
||||
if (slideAnimation != nil) {
|
||||
// determing total time sliding
|
||||
CFTimeInterval now = CACurrentMediaTime();
|
||||
CFTimeInterval slideTotalDuration = now - slideAnimation.beginTime;
|
||||
|
||||
// determine time offset into current slide
|
||||
CFTimeInterval slideTimeOffset = fmod(slideTotalDuration, slideAnimation.duration);
|
||||
|
||||
// transition to non-repeating slide
|
||||
CAAnimation *finishAnimation = shimmer_slide_finish(slideAnimation);
|
||||
|
||||
// adjust begin time to now - offset
|
||||
finishAnimation.beginTime = now - slideTimeOffset;
|
||||
|
||||
// note slide end time and begin
|
||||
slideEndTime = finishAnimation.beginTime + slideAnimation.duration;
|
||||
[_maskLayer addAnimation:finishAnimation forKey:kFBShimmerSlideAnimationKey];
|
||||
}
|
||||
|
||||
// fade in text at slideEndTime
|
||||
CABasicAnimation *fadeInAnimation = shimmer_end_fade_animation(self, _maskLayer.fadeLayer, 1.0, _shimmeringEndFadeDuration);
|
||||
fadeInAnimation.beginTime = slideEndTime;
|
||||
[_maskLayer.fadeLayer addAnimation:fadeInAnimation forKey:kFBFadeAnimationKey];
|
||||
|
||||
// expose end time for synchronization
|
||||
_shimmeringFadeTime = slideEndTime;
|
||||
}
|
||||
} else {
|
||||
// fade out text, optionally animated
|
||||
CABasicAnimation *fadeOutAnimation = nil;
|
||||
if (_shimmeringBeginFadeDuration > 0.0 && !disableActions) {
|
||||
fadeOutAnimation = shimmer_begin_fade_animation(self, _maskLayer.fadeLayer, 0.0, _shimmeringBeginFadeDuration);
|
||||
[_maskLayer.fadeLayer addAnimation:fadeOutAnimation forKey:kFBFadeAnimationKey];
|
||||
} else {
|
||||
BOOL innerDisableActions = [CATransaction disableActions];
|
||||
[CATransaction setDisableActions:YES];
|
||||
|
||||
_maskLayer.fadeLayer.opacity = 0.0;
|
||||
[_maskLayer.fadeLayer removeAllAnimations];
|
||||
|
||||
[CATransaction setDisableActions:innerDisableActions];
|
||||
}
|
||||
|
||||
// begin slide animation
|
||||
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
|
||||
|
||||
// compute shimmer duration
|
||||
CGFloat length = 0.0f;
|
||||
if (_shimmeringDirection == FBShimmerDirectionDown ||
|
||||
_shimmeringDirection == FBShimmerDirectionUp) {
|
||||
length = CGRectGetHeight(_contentLayer.bounds);
|
||||
} else {
|
||||
length = CGRectGetWidth(_contentLayer.bounds);
|
||||
}
|
||||
CFTimeInterval animationDuration = (length / _shimmeringSpeed) + _shimmeringPauseDuration;
|
||||
|
||||
if (slideAnimation != nil) {
|
||||
// ensure existing slide animation repeats
|
||||
[_maskLayer addAnimation:shimmer_slide_repeat(slideAnimation, animationDuration, _shimmeringDirection) forKey:kFBShimmerSlideAnimationKey];
|
||||
} else {
|
||||
// add slide animation
|
||||
slideAnimation = shimmer_slide_animation(self, animationDuration, _shimmeringDirection);
|
||||
slideAnimation.fillMode = kCAFillModeForwards;
|
||||
slideAnimation.removedOnCompletion = NO;
|
||||
slideAnimation.beginTime = CACurrentMediaTime() + fadeOutAnimation.duration;
|
||||
[_maskLayer addAnimation:slideAnimation forKey:kFBShimmerSlideAnimationKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - CALayerDelegate
|
||||
|
||||
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
|
||||
{
|
||||
// no associated actions
|
||||
return (id)kCFNull;
|
||||
}
|
||||
|
||||
#pragma mark - CAAnimationDelegate
|
||||
|
||||
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
|
||||
{
|
||||
if (flag && [[anim valueForKey:kFBEndFadeAnimationKey] boolValue]) {
|
||||
[self _clearMask];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user