blob: 172eb4db0cc089829a367d64a3eb02dc2f511963 [file] [log] [blame] [edit]
// Copyright 2015-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCInkView.h"
#import <CoreGraphics/CoreGraphics.h>
#import "private/MDCInkLayer.h"
#import "private/MDCLegacyInkLayer.h"
#import "MDCInkViewDelegate.h"
#import "MDCInkLayerDelegate.h"
#import "MDCLegacyInkLayerDelegate.h"
@interface MDCInkPendingAnimation : NSObject <CAAction>
@property(nonatomic, weak) CALayer *animationSourceLayer;
@property(nonatomic, strong) NSString *keyPath;
@property(nonatomic, strong) id fromValue;
@property(nonatomic, strong) id toValue;
@end
@interface MDCInkView () <CALayerDelegate, MDCInkLayerDelegate, MDCLegacyInkLayerDelegate>
@property(nonatomic, strong) CAShapeLayer *maskLayer;
@property(nonatomic, copy) MDCInkCompletionBlock startInkRippleCompletionBlock;
@property(nonatomic, copy) MDCInkCompletionBlock endInkRippleCompletionBlock;
@property(nonatomic, strong) MDCInkLayer *activeInkLayer;
// Legacy ink ripple
@property(nonatomic, readonly, weak) MDCLegacyInkLayer *inkLayer;
@end
@implementation MDCInkView {
BOOL _isActiveInkLayerAnimationRunning;
}
+ (Class)layerClass {
return [MDCLegacyInkLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonMDCInkViewInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCInkViewInit];
}
return self;
}
- (void)commonMDCInkViewInit {
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor clearColor];
self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.layer.delegate = self;
self.inkColor = self.defaultInkColor;
_usesLegacyInkRipple = YES;
// Use mask layer when the superview has a shadowPath.
_maskLayer = [CAShapeLayer layer];
_maskLayer.delegate = self;
self.inkLayer.animationDelegate = self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (self.inkStyle == MDCInkStyleUnbounded) {
self.layer.mask = nil;
} else if (self.superview.layer.shadowPath) {
// If the superview has a shadowPath make sure ink does not spread outside of the shadowPath.
self.maskLayer.path = self.superview.layer.shadowPath;
self.layer.mask = _maskLayer;
}
CGRect inkBounds = CGRectMake(0, 0, CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds));
self.layer.bounds = inkBounds;
// When bounds change ensure all ink layer bounds are changed too.
for (CALayer *layer in self.layer.sublayers) {
if ([layer isKindOfClass:[MDCInkLayer class]]) {
MDCInkLayer *inkLayer = (MDCInkLayer *)layer;
inkLayer.bounds = inkBounds;
inkLayer.fillColor = self.inkColor.CGColor;
}
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
- (void)setInkStyle:(MDCInkStyle)inkStyle {
_inkStyle = inkStyle;
switch (inkStyle) {
case MDCInkStyleBounded:
self.inkLayer.masksToBounds = YES;
self.inkLayer.bounded = YES;
break;
case MDCInkStyleUnbounded:
self.inkLayer.masksToBounds = NO;
self.inkLayer.bounded = NO;
break;
}
}
- (UIColor *)inkColor {
return self.inkLayer.inkColor;
}
- (void)setInkColor:(UIColor *)inkColor {
self.inkLayer.inkColor = inkColor ?: self.defaultInkColor;
}
- (CGFloat)maxRippleRadius {
return [self shouldIgnoreMaxRippleRadius] ? 0 : self.inkLayer.maxRippleRadius;
}
- (void)setMaxRippleRadius:(CGFloat)radius {
self.inkLayer.maxRippleRadius = radius;
// This is required for legacy Ink so that the Ink bounds will be adjusted correctly
[self setNeedsLayout];
}
- (BOOL)shouldIgnoreMaxRippleRadius {
return !self.usesLegacyInkRipple && self.inkStyle == MDCInkStyleBounded;
}
- (BOOL)usesCustomInkCenter {
return self.inkLayer.useCustomInkCenter;
}
- (void)setUsesCustomInkCenter:(BOOL)usesCustomInkCenter {
self.inkLayer.useCustomInkCenter = usesCustomInkCenter;
}
- (CGPoint)customInkCenter {
return self.inkLayer.customInkCenter;
}
- (void)setCustomInkCenter:(CGPoint)customInkCenter {
self.inkLayer.customInkCenter = customInkCenter;
}
- (MDCLegacyInkLayer *)inkLayer {
return (MDCLegacyInkLayer *)self.layer;
}
- (void)startTouchBeganAnimationAtPoint:(CGPoint)point
completion:(MDCInkCompletionBlock)completionBlock {
[self startTouchBeganAtPoint:point animated:YES withCompletion:completionBlock];
}
- (void)startTouchBeganAtPoint:(CGPoint)point
animated:(BOOL)animated
withCompletion:(nullable MDCInkCompletionBlock)completionBlock {
if (self.usesLegacyInkRipple) {
[self.inkLayer spreadFromPoint:point completion:completionBlock];
} else {
@synchronized(self) {
if (animated && _isActiveInkLayerAnimationRunning) {
// Only one ink layer animation can be running at a time.
return;
}
_isActiveInkLayerAnimationRunning = YES;
}
self.startInkRippleCompletionBlock = completionBlock;
MDCInkLayer *inkLayer = [MDCInkLayer layer];
inkLayer.inkColor = self.inkColor;
inkLayer.maxRippleRadius = self.maxRippleRadius;
inkLayer.animationDelegate = self;
inkLayer.opacity = 0;
inkLayer.frame = self.bounds;
[self.layer addSublayer:inkLayer];
[inkLayer startInkAtPoint:point animated:animated];
self.activeInkLayer = inkLayer;
}
}
- (void)startTouchEndAtPoint:(CGPoint)point
animated:(BOOL)animated
withCompletion:(nullable MDCInkCompletionBlock)completionBlock {
if (self.usesLegacyInkRipple) {
[self.inkLayer evaporateWithCompletion:completionBlock];
} else {
self.endInkRippleCompletionBlock = completionBlock;
[self.activeInkLayer endInkAtPoint:point animated:animated];
}
}
- (void)startTouchEndedAnimationAtPoint:(CGPoint)point
completion:(MDCInkCompletionBlock)completionBlock {
[self startTouchEndAtPoint:point animated:YES withCompletion:completionBlock];
}
- (void)cancelAllAnimationsAnimated:(BOOL)animated {
if (self.usesLegacyInkRipple) {
[self.inkLayer resetAllInk:animated];
} else {
NSArray<CALayer *> *sublayers = [self.layer.sublayers copy];
for (CALayer *layer in sublayers) {
if ([layer isKindOfClass:[MDCInkLayer class]]) {
MDCInkLayer *inkLayer = (MDCInkLayer *)layer;
if (animated) {
[inkLayer endAnimationAtPoint:CGPointZero];
} else {
[inkLayer removeFromSuperlayer];
}
}
}
}
}
- (UIColor *)defaultInkColor {
static UIColor *defaultInkColor;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultInkColor = [[UIColor alloc] initWithWhite:0 alpha:(CGFloat)0.14];
});
return defaultInkColor;
}
+ (MDCInkView *)injectedInkViewForView:(UIView *)view {
MDCInkView *foundInkView = nil;
for (MDCInkView *subview in view.subviews) {
if ([subview isKindOfClass:[MDCInkView class]]) {
foundInkView = subview;
break;
}
}
if (!foundInkView) {
foundInkView = [[MDCInkView alloc] initWithFrame:view.bounds];
foundInkView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[view addSubview:foundInkView];
}
return foundInkView;
}
#pragma mark - MDCLegacyInkLayerDelegate
- (void)legacyInkLayerAnimationDidStart:(MDCLegacyInkLayer *)inkLayer {
if ([self.animationDelegate respondsToSelector:@selector(inkAnimationDidStart:)]) {
[self.animationDelegate inkAnimationDidStart:self];
}
}
- (void)legacyInkLayerAnimationDidEnd:(MDCLegacyInkLayer *)inkLayer {
if ([self.animationDelegate respondsToSelector:@selector(inkAnimationDidEnd:)]) {
[self.animationDelegate inkAnimationDidEnd:self];
}
}
#pragma mark - MDCInkLayerDelegate
- (void)inkLayerAnimationDidStart:(MDCInkLayer *)inkLayer {
if (self.activeInkLayer == inkLayer && self.startInkRippleCompletionBlock) {
self.startInkRippleCompletionBlock();
}
if ([self.animationDelegate respondsToSelector:@selector(inkAnimationDidStart:)]) {
[self.animationDelegate inkAnimationDidStart:self];
}
}
- (void)inkLayerStartAnimationDidFinish:(MDCInkLayer *)inkLayer {
@synchronized(self) {
if (self.activeInkLayer == inkLayer && _isActiveInkLayerAnimationRunning) {
_isActiveInkLayerAnimationRunning = NO;
}
}
}
- (void)inkLayerAnimationDidEnd:(MDCInkLayer *)inkLayer {
if (self.activeInkLayer == inkLayer && self.endInkRippleCompletionBlock) {
self.endInkRippleCompletionBlock();
}
if ([self.animationDelegate respondsToSelector:@selector(inkAnimationDidEnd:)]) {
[self.animationDelegate inkAnimationDidEnd:self];
}
}
#pragma mark - CALayerDelegate
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if ([event isEqualToString:@"path"] || [event isEqualToString:@"shadowPath"]) {
// We have to create a pending animation because if we are inside a UIKit animation block we
// won't know any properties of the animation block until it is commited.
MDCInkPendingAnimation *pendingAnim = [[MDCInkPendingAnimation alloc] init];
pendingAnim.animationSourceLayer = self.superview.layer;
pendingAnim.fromValue = [layer.presentationLayer valueForKey:event];
pendingAnim.toValue = nil;
pendingAnim.keyPath = event;
return pendingAnim;
}
return nil;
}
@end
@implementation MDCInkPendingAnimation
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict {
if ([anObject isKindOfClass:[CAShapeLayer class]]) {
CAShapeLayer *layer = (CAShapeLayer *)anObject;
// In order to synchronize our animation with UIKit animations we have to fetch the resizing
// animation created by UIKit and copy the configuration to our custom animation.
CAAnimation *boundsAction = [self.animationSourceLayer animationForKey:@"bounds.size"];
if ([boundsAction isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *animation = (CABasicAnimation *)[boundsAction copy];
animation.keyPath = self.keyPath;
animation.fromValue = self.fromValue;
animation.toValue = self.toValue;
[layer addAnimation:animation forKey:event];
}
}
}
@end