469 lines
14 KiB
Objective-C
Executable File
469 lines
14 KiB
Objective-C
Executable File
/**
|
|
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
|