ifish/Ifish/views/FBShimmering/FBShimmeringLayer.m

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