From e2f4fd39ee036bef0411b59b35080642b4a23483 Mon Sep 17 00:00:00 2001 From: Yarden Eitan Date: Sun, 11 Apr 2021 06:30:47 -0700 Subject: [PATCH] [Shadows] Code improvements for the new Shadow component. PiperOrigin-RevId: 367871950 --- .../examples/ShadowTypicalUseExample.swift | 17 +- components/Shadow/src/MDCShadow.h | 25 ++- components/Shadow/src/MDCShadow.m | 75 +------- components/Shadow/src/MDCShadowConfiguring.h | 87 --------- components/Shadow/src/MDCShadowConfiguring.m | 52 ------ components/Shadow/src/MDCShadowsCollection.h | 146 +++++++++++++++ components/Shadow/src/MDCShadowsCollection.m | 170 ++++++++++++++++++ components/Shadow/src/MaterialShadow.h | 2 +- .../Shadow/src/private/MDCShadow+Internal.h | 25 --- .../tests/snapshot/MDCShadowSnapshotTests.m | 29 +-- components/Shadow/tests/unit/MDCShadowTests.m | 92 ++++++++-- 11 files changed, 439 insertions(+), 281 deletions(-) delete mode 100644 components/Shadow/src/MDCShadowConfiguring.h delete mode 100644 components/Shadow/src/MDCShadowConfiguring.m create mode 100644 components/Shadow/src/MDCShadowsCollection.h create mode 100644 components/Shadow/src/MDCShadowsCollection.m delete mode 100644 components/Shadow/src/private/MDCShadow+Internal.h diff --git a/components/Shadow/examples/ShadowTypicalUseExample.swift b/components/Shadow/examples/ShadowTypicalUseExample.swift index 369162920b2..0761d5a85a4 100644 --- a/components/Shadow/examples/ShadowTypicalUseExample.swift +++ b/components/Shadow/examples/ShadowTypicalUseExample.swift @@ -29,14 +29,15 @@ final class ShadowedView: UIView { override func layoutSubviews() { super.layoutSubviews() - MDCConfigureShadow(for: self, color: MDCShadowColor(), elevation: 2) + MDCConfigureShadow( + for: self, shadow: MDCShadowForElevation(1, MDCShadowsCollectionDefault()), + color: MDCShadowColor()) } } /// Typical use-case for a shaped view with Material Shadows. final class ShapedView: UIView { let shapeLayer = CAShapeLayer() - let shadow = MDCShadowForElevation(2) override init(frame: CGRect) { super.init(frame: frame) @@ -61,14 +62,17 @@ final class ShapedView: UIView { super.layoutSubviews() guard let path = polygonPath(bounds: self.bounds, numSides: 3, numPoints: 3) else { return } shapeLayer.path = path - MDCConfigureShadow(for: self, color: MDCShadowColor(), shadow: shadow, path: path) + + MDCConfigureShadow( + for: self, shadow: MDCShadowForElevation(1, MDCShadowsCollectionDefault()), + color: MDCShadowColor(), + path: path) } } /// More complex use-case for a view with a custom shape which animates. final class AnimatedShapedView: UIView { let shapeLayer = CAShapeLayer() - let shadow = MDCShadowForElevation(2) let firstNumSides = 3 let lastNumSides = 12 let animationStepDuration: CFTimeInterval = 0.6 @@ -104,7 +108,10 @@ final class AnimatedShapedView: UIView { bounds: bounds, numSides: firstNumSides, numPoints: lastNumSides) else { return } shapeLayer.path = startPath - MDCConfigureShadow(for: self, color: MDCShadowColor(), shadow: shadow, path: startPath) + MDCConfigureShadow( + for: self, shadow: MDCShadowForElevation(1, MDCShadowsCollectionDefault()), + color: MDCShadowColor(), + path: startPath) var polygonPaths = (firstNumSides...lastNumSides).map { polygonPath(bounds: bounds, numSides: $0, numPoints: lastNumSides) diff --git a/components/Shadow/src/MDCShadow.h b/components/Shadow/src/MDCShadow.h index d4a7fb3aa7f..deaa46c40cf 100644 --- a/components/Shadow/src/MDCShadow.h +++ b/components/Shadow/src/MDCShadow.h @@ -15,11 +15,14 @@ #import /** - Immutable value type holding shadow metrics to apply to a view's layer. Use - `MDCShadowForElevation()` or `MDCShadowBuilder` to create this object. + An immutable shadow object that consists of shadow properties (opacity, radius, offset). + + To generate a shadow instance, please use the MDCShadowBuilder APIs. */ __attribute__((objc_subclassing_restricted)) @interface MDCShadow : NSObject +- (nonnull instancetype)init NS_UNAVAILABLE; + /** CALayer.shadowOpacity */ @property(nonatomic, readonly) CGFloat opacity; @@ -29,8 +32,6 @@ __attribute__((objc_subclassing_restricted)) @interface MDCShadow : NSObject /** CALayer.shadowOffset */ @property(nonatomic, readonly) CGSize offset; -- (nonnull instancetype)init NS_UNAVAILABLE; - @end /** @@ -50,15 +51,9 @@ __attribute__((objc_subclassing_restricted)) @interface MDCShadowBuilder : NSObj /** Returns an immutable value type containing a snapshot of the values in this object. */ - (nonnull MDCShadow *)build; -@end - -/** - Default color for a Material shadow. On iOS >= 13, this is a dynamic color. - */ -FOUNDATION_EXTERN UIColor *_Nonnull MDCShadowColor(void); +/** Returns a builder with the provided opacity, radius, and offset properties. */ ++ (nonnull MDCShadowBuilder *)builderWithOpacity:(CGFloat)opacity + radius:(CGFloat)radius + offset:(CGSize)offset; -/** - Returns an `MDCShadow` representing the Material shadow metrics for the given elevation (in - points). - */ -FOUNDATION_EXTERN MDCShadow *_Nonnull MDCShadowForElevation(CGFloat elevation); +@end diff --git a/components/Shadow/src/MDCShadow.m b/components/Shadow/src/MDCShadow.m index 897165f1ee0..94a61441f6c 100644 --- a/components/Shadow/src/MDCShadow.m +++ b/components/Shadow/src/MDCShadow.m @@ -14,9 +14,6 @@ #import "MDCShadow.h" -#import "MaterialAvailability.h" -#import "MDCShadow+Internal.h" - @implementation MDCShadow - (instancetype)initWithOpacity:(CGFloat)opacity radius:(CGFloat)radius offset:(CGSize)offset { @@ -59,68 +56,14 @@ - (MDCShadow *)build { return [[MDCShadow alloc] initWithOpacity:self.opacity radius:self.radius offset:self.offset]; } -@end - -static UIColor *LightStyleShadowColor(void) { - static UIColor *lightStyleShadowColor; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - lightStyleShadowColor = [UIColor colorWithRed:0.235 green:0.251 blue:0.263 alpha:1]; - }); - return lightStyleShadowColor; -} - -UIColor *MDCShadowColor(void) { -#if MDC_AVAILABLE_SDK_IOS(13_0) - if (@available(iOS 13.0, *)) { - return [UIColor colorWithDynamicProvider:^(UITraitCollection *traitCollection) { - switch (traitCollection.userInterfaceStyle) { - case UIUserInterfaceStyleUnspecified: - __attribute__((fallthrough)); - case UIUserInterfaceStyleLight: - return LightStyleShadowColor(); - case UIUserInterfaceStyleDark: - return UIColor.blackColor; - } - __builtin_unreachable(); - }]; - } -#endif // MDC_AVAILABLE_SDK_IOS(13_0) - return LightStyleShadowColor(); ++ (MDCShadowBuilder *)builderWithOpacity:(CGFloat)opacity + radius:(CGFloat)radius + offset:(CGSize)offset { + MDCShadowBuilder *builder = [[MDCShadowBuilder alloc] init]; + builder.opacity = opacity; + builder.radius = radius; + builder.offset = offset; + return builder; } -static int ShadowElevationToLevel(CGFloat elevation) { - if (elevation < 1) { - return 0; - } - if (elevation < 3) { - return 1; - } - if (elevation < 6) { - return 2; - } - if (elevation < 8) { - return 3; - } - if (elevation < 12) { - return 4; - } - return 5; -} - -MDCShadow *MDCShadowForElevation(CGFloat elevation) { - static NSArray *shadowLevels; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - shadowLevels = @[ - [[MDCShadow alloc] initWithOpacity:0 radius:0 offset:CGSizeMake(0, 0)], - [[MDCShadow alloc] initWithOpacity:0.43 radius:2.5 offset:CGSizeMake(0, 1)], - [[MDCShadow alloc] initWithOpacity:0.4 radius:3.25 offset:CGSizeMake(0, 1.25)], - [[MDCShadow alloc] initWithOpacity:0.34 radius:4.75 offset:CGSizeMake(0, 2.25)], - [[MDCShadow alloc] initWithOpacity:0.42 radius:6 offset:CGSizeMake(0, 3)], - [[MDCShadow alloc] initWithOpacity:0.4 radius:7.25 offset:CGSizeMake(0, 5)], - ]; - }); - int shadowLevel = ShadowElevationToLevel(elevation); - return shadowLevels[shadowLevel]; -} +@end diff --git a/components/Shadow/src/MDCShadowConfiguring.h b/components/Shadow/src/MDCShadowConfiguring.h deleted file mode 100644 index e1910ee38f3..00000000000 --- a/components/Shadow/src/MDCShadowConfiguring.h +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2021-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 - -@class MDCShadow; - -/** - Given a view and a shadow color (e.g. `MDCShadowColor()`) along with an elevation, updates - the shadow properties of `view.layer`: - - * shadowColor - * shadowOpacity - * shadowRadius - * shadowOffset - * shadowPath - - `shadowPath` will be set to the current bounds of the given view (including rounded - corners if set on view.layer). - - TODO(b/182581383): maskedCorners, cornerCurve, and cornerCurveExpansionFactor are not - yet supported. - - If `elevation` is < 1, disables the view's shadow. Otherwise, enables the shadow. - - Call this function from your `UIView` subclass's `-layoutSubviews` to update `shadowPath` - whenever the view's bounds change. - */ -FOUNDATION_EXTERN void MDCConfigureShadowForViewWithElevation(UIView *_Nonnull view, - UIColor *_Nonnull shadowColor, - CGFloat elevation) - NS_SWIFT_NAME(MDCConfigureShadow(for:color:elevation:)); - -/** - Given a view and a shadow color (e.g. `MDCShadowColor()`) along with an `MDCShadow` value, updates - the shadow properties of `view.layer`: - - * shadowColor - * shadowOpacity - * shadowRadius - * shadowOffset - * shadowPath - - `shadowPath` will be set to the current bounds of the given view (including rounded - corners if set on view.layer). - - TODO(b/182581383): maskedCorners, cornerCurve, and cornerCurveExpansionFactor are not - yet supported. - - Call this function from your `UIView` subclass's `-layoutSubviews` to update `shadowPath` - whenever the view's bounds change. - */ -FOUNDATION_EXTERN void MDCConfigureShadowForViewWithShadow(UIView *_Nonnull view, - UIColor *_Nonnull shadowColor, - MDCShadow *_Nonnull shadow) - NS_SWIFT_NAME(MDCConfigureShadow(for:color:shadow:)); - -/** - Given a view, a shadow color (e.g. `MDCShadowColor()`), an `MDCShadow` value, and a `path` in the - view's coordinate space representing the shape of the view, updates the shadow properties of - `view.layer`: - - * shadowColor - * shadowOpacity - * shadowRadius - * shadowOffset - * shadowPath - - Call this function from your `UIView` subclass's `-layoutSubviews` to update `shadowPath` - whenever the view's bounds or shape changes. - */ -FOUNDATION_EXTERN void MDCConfigureShadowForViewWithShadowAndPath(UIView *_Nonnull view, - UIColor *_Nonnull shadowColor, - MDCShadow *_Nonnull shadow, - CGPathRef _Nonnull path) - NS_SWIFT_NAME(MDCConfigureShadow(for:color:shadow:path:)); diff --git a/components/Shadow/src/MDCShadowConfiguring.m b/components/Shadow/src/MDCShadowConfiguring.m deleted file mode 100644 index 2e74820946e..00000000000 --- a/components/Shadow/src/MDCShadowConfiguring.m +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021-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 "MDCShadowConfiguring.h" - -#import "MaterialAvailability.h" -#import "MDCShadow.h" - -void MDCConfigureShadowForViewWithElevation(UIView *view, UIColor *shadowColor, CGFloat elevation) { - MDCShadow *shadow = MDCShadowForElevation(elevation); - if (!shadow) { - view.layer.shadowColor = nil; - view.layer.shadowOpacity = 0; - view.layer.shadowRadius = 0; - view.layer.shadowOffset = CGSizeZero; - view.layer.shadowPath = nil; - return; - } - MDCConfigureShadowForViewWithShadow(view, shadowColor, shadow); -} - -void MDCConfigureShadowForViewWithShadow(UIView *view, UIColor *shadowColor, MDCShadow *shadow) { - // Support views both with and without cornerRadius set. - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds - cornerRadius:view.layer.cornerRadius]; - MDCConfigureShadowForViewWithShadowAndPath(view, shadowColor, shadow, path.CGPath); -} - -void MDCConfigureShadowForViewWithShadowAndPath(UIView *view, UIColor *shadowColor, - MDCShadow *shadow, CGPathRef path) { -#if MDC_AVAILABLE_SDK_IOS(13_0) - if (@available(ios 13.0, *)) { - shadowColor = [shadowColor resolvedColorWithTraitCollection:view.traitCollection]; - } -#endif // MDC_AVAILABLE_SDK_IOS(13_0) - view.layer.shadowColor = shadowColor.CGColor; - view.layer.shadowOpacity = (float)shadow.opacity; - view.layer.shadowRadius = shadow.radius; - view.layer.shadowOffset = shadow.offset; - view.layer.shadowPath = path; -} diff --git a/components/Shadow/src/MDCShadowsCollection.h b/components/Shadow/src/MDCShadowsCollection.h new file mode 100644 index 00000000000..1831e3b8816 --- /dev/null +++ b/components/Shadow/src/MDCShadowsCollection.h @@ -0,0 +1,146 @@ +// Copyright 2021-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 + +#import "MDCShadow.h" + +/** + An immutable shadows collection object that encapsulates a storage of shadows correlating to + elevation values. + + To apply a shadow on a view, please see and use the C methods @c MDCConfigureShadowForView and + @c MDCConfigureShadowForViewWithPath. + */ +__attribute__((objc_subclassing_restricted)) @interface MDCShadowsCollection : NSObject + +- (nonnull instancetype)init NS_UNAVAILABLE; + +@end + +/** + A shadows collection object builder that generates an MDCShadowsCollection instance using the @c + build method. The object allows to add shadows for a given elevation. + + To ensure no nullability situations, please instantiate the builder using @c + builderWithShadow:forElevation and provide an initial MDCShadow instance. + */ +__attribute__((objc_subclassing_restricted)) @interface MDCShadowsCollectionBuilder : NSObject + +- (nonnull instancetype)init NS_UNAVAILABLE; + +/** + Adds a shadow for the given elevation. + + @param shadow An MDCShadow object consisting of shadow properties such as radius, opacity, and + offset. + @param elevation The elevation provided in points (dp). + */ +- (void)addShadow:(MDCShadow *_Nonnull)shadow forElevation:(CGFloat)elevation; + +/** + Adds a dictionary of shadows with their correlating elevations to the shadows collection. + + @param shadowsForElevations A dictionary holding NSNumber elevation value keys, and their values + are MDCShadow objects correlating to their elevation key. + */ +- (void)addShadowsForElevations: + (NSDictionary *_Nonnull)shadowsForElevations; + +/** + Creates an initial MDCShadowsCollectionBuilder by providing it an initial MDCShadow object and its + correlating elevation. + + @param shadow An MDCShadow object consisting of shadow properties such as radius, opacity, and + offset. + @param elevation The elevation provided in points (dp). + */ ++ (MDCShadowsCollectionBuilder *_Nonnull)builderWithShadow:(MDCShadow *_Nonnull)shadow + forElevation:(CGFloat)elevation; + +/** + Builds and returns an MDCShadowsCollection instance given the provided shadows using the + @c addShadow:forElevation and @c addShadowsForElevations: APIs. + */ +- (MDCShadowsCollection *_Nonnull)build; + +@end + +/** + Given a view, MDCShadow instance, and a shadow color (e.g. `MDCShadowColor()`), updates the shadow + properties of `view.layer`: + + * shadowColor + * shadowOpacity + * shadowRadius + * shadowOffset + * shadowPath + + `shadowPath` will be set to the current bounds of the given view (including rounded + corners if set on view.layer). + + TODO(b/182581383): maskedCorners, cornerCurve, and cornerCurveExpansionFactor are not + yet supported. + + Call this function from your `UIView` subclass's `-layoutSubviews` to update `shadowPath` + whenever the view's bounds change. + */ +FOUNDATION_EXTERN void MDCConfigureShadowForView(UIView *_Nonnull view, MDCShadow *_Nonnull shadow, + UIColor *_Nonnull shadowColor) + NS_SWIFT_NAME(MDCConfigureShadow(for:shadow:color:)); + +/** + Given a view, MDCShadow instance, a shadow color (e.g. `MDCShadowColor()`), and a `path` in the + view's coordinate space representing the shape of the view, updates the shadow properties of + `view.layer`: + + * shadowColor + * shadowOpacity + * shadowRadius + * shadowOffset + * shadowPath + + Call this function from your `UIView` subclass's `-layoutSubviews` to update `shadowPath` + whenever the view's bounds or shape changes. + */ +FOUNDATION_EXTERN void MDCConfigureShadowForViewWithPath(UIView *_Nonnull view, + MDCShadow *_Nonnull shadow, + UIColor *_Nonnull shadowColor, + CGPathRef _Nonnull path) + NS_SWIFT_NAME(MDCConfigureShadow(for:shadow:color:path:)); + +/** + Default color for a Material shadow. On iOS >= 13, this is a dynamic color. + */ +FOUNDATION_EXTERN UIColor *_Nonnull MDCShadowColor(void); + +/** + Returns an MDCShadowsCollection instance with predefined defaults of shadow properties (opacity, + radius, offset) for elevations. + */ +FOUNDATION_EXTERN MDCShadowsCollection *_Nonnull MDCShadowsCollectionDefault(void); + +/** + Returns an MDCShadow instance representing the shadow properties for the given elevation (in + points) by using the @c shadowsCollection container holding elevation to MDCShadow instance + bindings. + + Note: If the provided elevation is not stored, the shadow properties that are set are of the + nearest elevation above the provided elevation. i.e. if elevations 1 and 3 are set, and an + elevation of 2 is provided as input, the shadow properties set will be of elevation 3. If the + provided elevation is above the highest elevation value that is set, then the shadow properties set + will be of the highest elevation. + */ +FOUNDATION_EXTERN MDCShadow *_Nonnull MDCShadowForElevation( + CGFloat elevation, MDCShadowsCollection *_Nonnull shadowsCollection); diff --git a/components/Shadow/src/MDCShadowsCollection.m b/components/Shadow/src/MDCShadowsCollection.m new file mode 100644 index 00000000000..ed3c6b6f891 --- /dev/null +++ b/components/Shadow/src/MDCShadowsCollection.m @@ -0,0 +1,170 @@ +// Copyright 2021-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 "MDCShadowsCollection.h" + +#import "MaterialAvailability.h" +#import "MDCShadow.h" + +@implementation MDCShadowsCollection { + NSDictionary *_shadowValuesForElevation; + NSArray *_orderedKeys; +} + +- (instancetype)initWithShadowValuesForElevation: + (NSDictionary *)shadowValuesForElevation { + self = [super init]; + if (self) { + _shadowValuesForElevation = [shadowValuesForElevation copy]; + _orderedKeys = [shadowValuesForElevation.allKeys sortedArrayUsingSelector:@selector(compare:)]; + } + return self; +} + +- (MDCShadow *)shadowForElevation:(CGFloat)elevation { + NSUInteger lookupIndex = [self indexInOrderedKeysOfGivenElevation:elevation]; + // If the value is larger than the largest value in the array, we will return the highest value in + // the array. + if (lookupIndex >= _orderedKeys.count) { + lookupIndex = _orderedKeys.count - 1; + } + + NSNumber *key = _orderedKeys[lookupIndex]; + return [_shadowValuesForElevation objectForKey:key]; +} + +- (NSUInteger)indexInOrderedKeysOfGivenElevation:(CGFloat)elevation { + NSNumber *num = @(elevation); + NSUInteger index = + [_orderedKeys indexOfObject:num + inSortedRange:NSMakeRange(0, _orderedKeys.count) + options:NSBinarySearchingInsertionIndex + usingComparator:^NSComparisonResult(NSNumber *num1, NSNumber *num2) { + return [num1 compare:num2]; + }]; + return index; +} + +@end + +@implementation MDCShadowsCollectionBuilder { + NSMutableDictionary *_shadowValuesForElevation; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _shadowValuesForElevation = [[NSMutableDictionary alloc] init]; + } + return self; +} + ++ (MDCShadowsCollectionBuilder *)builderWithShadow:(MDCShadow *)shadow + forElevation:(CGFloat)elevation { + MDCShadowsCollectionBuilder *builder = [[self alloc] init]; + [builder addShadow:shadow forElevation:elevation]; + return builder; +} + +- (void)addShadow:(MDCShadow *)shadow forElevation:(CGFloat)elevation { + [_shadowValuesForElevation setObject:shadow forKey:@(elevation)]; +} + +- (void)addShadowsForElevations:(NSDictionary *)shadowsForElevations { + [_shadowValuesForElevation addEntriesFromDictionary:shadowsForElevations]; +} + +- (MDCShadowsCollection *)build { + return [[MDCShadowsCollection alloc] initWithShadowValuesForElevation:_shadowValuesForElevation]; +} + +@end + +static UIColor *LightStyleShadowColor(void) { + static UIColor *lightStyleShadowColor; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lightStyleShadowColor = [UIColor colorWithRed:0.235 green:0.251 blue:0.263 alpha:1]; + }); + return lightStyleShadowColor; +} + +UIColor *MDCShadowColor(void) { +#if MDC_AVAILABLE_SDK_IOS(13_0) + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^(UITraitCollection *traitCollection) { + switch (traitCollection.userInterfaceStyle) { + case UIUserInterfaceStyleUnspecified: + __attribute__((fallthrough)); + case UIUserInterfaceStyleLight: + return LightStyleShadowColor(); + case UIUserInterfaceStyleDark: + return UIColor.blackColor; + } + __builtin_unreachable(); + }]; + } +#endif // MDC_AVAILABLE_SDK_IOS(13_0) + return LightStyleShadowColor(); +} + +MDCShadowsCollection *MDCShadowsCollectionDefault(void) { + static MDCShadowsCollection *shadowsCollection; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + MDCShadow *shadow = [[MDCShadowBuilder builderWithOpacity:0 radius:0 + offset:CGSizeMake(0, 0)] build]; + MDCShadowsCollectionBuilder *shadowsBuilder = + [MDCShadowsCollectionBuilder builderWithShadow:shadow forElevation:0]; + NSDictionary *shadowValuesForElevation = @{ + @1 : [[MDCShadowBuilder builderWithOpacity:0.43 radius:2.5 offset:CGSizeMake(0, 1)] build], + @3 : [[MDCShadowBuilder builderWithOpacity:0.4 radius:3.25 offset:CGSizeMake(0, 1.25)] build], + @6 : [[MDCShadowBuilder builderWithOpacity:0.34 radius:4.75 + offset:CGSizeMake(0, 2.25)] build], + @8 : [[MDCShadowBuilder builderWithOpacity:0.42 radius:6 offset:CGSizeMake(0, 3)] build], + @12 : [[MDCShadowBuilder builderWithOpacity:0.4 radius:7.25 offset:CGSizeMake(0, 5)] build], + }; + [shadowsBuilder addShadowsForElevations:shadowValuesForElevation]; + shadowsCollection = [shadowsBuilder build]; + }); + return shadowsCollection; +} + +void MDCConfigureShadowForView(UIView *view, MDCShadow *shadow, UIColor *shadowColor) { + // The bezierPathWithRoundedRect API supports both a cornerRadius of 0 (created just a square + // path) and also rounded corners where the cornerRadius is >0. + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds + cornerRadius:view.layer.cornerRadius]; + + MDCConfigureShadowForViewWithPath(view, shadow, shadowColor, path.CGPath); +} + +void MDCConfigureShadowForViewWithPath(UIView *view, MDCShadow *shadow, UIColor *shadowColor, + CGPathRef path) { +#if MDC_AVAILABLE_SDK_IOS(13_0) + if (@available(ios 13.0, *)) { + shadowColor = [shadowColor resolvedColorWithTraitCollection:view.traitCollection]; + } +#endif // MDC_AVAILABLE_SDK_IOS(13_0) + view.layer.shadowColor = shadowColor.CGColor; + view.layer.shadowOpacity = (float)shadow.opacity; + view.layer.shadowRadius = shadow.radius; + view.layer.shadowOffset = shadow.offset; + view.layer.shadowPath = path; +} + +MDCShadow *MDCShadowForElevation(CGFloat elevation, + MDCShadowsCollection *shadowValuesForElevation) { + return [shadowValuesForElevation shadowForElevation:elevation]; +} diff --git a/components/Shadow/src/MaterialShadow.h b/components/Shadow/src/MaterialShadow.h index c4fb1074199..c66cffe4daa 100644 --- a/components/Shadow/src/MaterialShadow.h +++ b/components/Shadow/src/MaterialShadow.h @@ -14,4 +14,4 @@ // limitations under the License. #import "MDCShadow.h" -#import "MDCShadowConfiguring.h" +#import "MDCShadowsCollection.h" diff --git a/components/Shadow/src/private/MDCShadow+Internal.h b/components/Shadow/src/private/MDCShadow+Internal.h deleted file mode 100644 index 8c7225d5d2f..00000000000 --- a/components/Shadow/src/private/MDCShadow+Internal.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021-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 - -#import "MDCShadow.h" - -@interface MDCShadow () - -- (instancetype)initWithOpacity:(CGFloat)opacity - radius:(CGFloat)radius - offset:(CGSize)offset NS_DESIGNATED_INITIALIZER; - -@end diff --git a/components/Shadow/tests/snapshot/MDCShadowSnapshotTests.m b/components/Shadow/tests/snapshot/MDCShadowSnapshotTests.m index 13c705e5d5b..d806ae95d52 100644 --- a/components/Shadow/tests/snapshot/MDCShadowSnapshotTests.m +++ b/components/Shadow/tests/snapshot/MDCShadowSnapshotTests.m @@ -48,6 +48,7 @@ @interface MDCShadowTestView : UIView @property(nonatomic, strong, nullable) CAShapeLayer *shapeLayer; @property(nonatomic, nullable) CGPathRef shapePath; @property(nonatomic, strong, nullable) UITraitCollection *traitCollectionOverride; + @end @implementation MDCShadowTestView @@ -68,13 +69,17 @@ - (UITraitCollection *)traitCollection { - (void)layoutSubviews { [super layoutSubviews]; - if (self.shapePath) { + if (self.shapePath != nil) { + _Nonnull CGPathRef shapePath = self.shapePath; self.backgroundColor = nil; - MDCShadow *shadow = MDCShadowForElevation(self.shadowElevation); - MDCConfigureShadowForViewWithShadowAndPath(self, self.shadowColor, shadow, self.shapePath); + MDCConfigureShadowForViewWithPath( + self, MDCShadowForElevation(self.shadowElevation, MDCShadowsCollectionDefault()), + self.shadowColor, shapePath); } else { self.backgroundColor = UIColor.whiteColor; - MDCConfigureShadowForViewWithElevation(self, self.shadowColor, self.shadowElevation); + MDCConfigureShadowForView( + self, MDCShadowForElevation(self.shadowElevation, MDCShadowsCollectionDefault()), + self.shadowColor); } } @@ -135,7 +140,7 @@ - (void)testShadowWithZeroElevationShouldNotRenderShadow { - (void)testShadowWithLowElevationShouldRenderShadow { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; // Then [self generateSnapshotAndVerifyForView:self.view]; @@ -143,7 +148,7 @@ - (void)testShadowWithLowElevationShouldRenderShadow { - (void)testShadowWithHighElevationShouldRenderShadow { // Given - self.view.shadowElevation = 10; + self.view.shadowElevation = 8; // Then [self generateSnapshotAndVerifyForView:self.view]; @@ -151,7 +156,7 @@ - (void)testShadowWithHighElevationShouldRenderShadow { - (void)testShadowWithLowElevationShouldUpdateShadowOnBoundsChange { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; // When [self.view layoutIfNeeded]; @@ -163,7 +168,7 @@ - (void)testShadowWithLowElevationShouldUpdateShadowOnBoundsChange { - (void)testShadowWithLowElevationAndCornerRadiusShouldRenderRoundedShadow { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; self.view.layer.cornerRadius = 3; // Then @@ -172,7 +177,7 @@ - (void)testShadowWithLowElevationAndCornerRadiusShouldRenderRoundedShadow { - (void)testShadowWithLowElevationAndCornerRadiusShouldUpdateShadowOnBoundsChange { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; self.view.layer.cornerRadius = 3; // When @@ -185,7 +190,7 @@ - (void)testShadowWithLowElevationAndCornerRadiusShouldUpdateShadowOnBoundsChang - (void)testShadowWithLowElevationAndShapeLayerShouldRenderShapedShadow { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; UIBezierPath *triangleBezierPath = UIBezierPath.bezierPath; [triangleBezierPath moveToPoint:CGPointMake(40, 0)]; [triangleBezierPath addLineToPoint:CGPointMake(80, 80)]; @@ -201,7 +206,7 @@ - (void)testCustomShadowColorInLightModeShouldBeGreen { #if MDC_AVAILABLE_SDK_IOS(13_0) if (@available(iOS 13.0, *)) { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; self.view.shadowColor = MDCTestDynamicShadowColor(); // When @@ -218,7 +223,7 @@ - (void)testCustomShadowColorInDarkModeShouldBeRed { #if MDC_AVAILABLE_SDK_IOS(13_0) if (@available(iOS 13.0, *)) { // Given - self.view.shadowElevation = 2; + self.view.shadowElevation = 1; self.view.shadowColor = MDCTestDynamicShadowColor(); // When diff --git a/components/Shadow/tests/unit/MDCShadowTests.m b/components/Shadow/tests/unit/MDCShadowTests.m index 68b715250f1..759b3bdbeee 100644 --- a/components/Shadow/tests/unit/MDCShadowTests.m +++ b/components/Shadow/tests/unit/MDCShadowTests.m @@ -14,7 +14,11 @@ #import -#import "MDCShadow.h" +#import "MaterialShadow.h" + +@interface MDCShadowsCollection (Testing) +- (MDCShadow *)shadowForElevation:(CGFloat)elevation; +@end @interface MDCShadowTests : XCTestCase @end @@ -23,7 +27,7 @@ @implementation MDCShadowTests - (void)testZeroElevationShouldReturnEmptyShadow { // Given - MDCShadow *shadow = MDCShadowForElevation(0); + MDCShadow *shadow = MDCShadowForElevation(0, MDCShadowsCollectionDefault()); // Then XCTAssertEqual(shadow.opacity, 0); @@ -33,7 +37,7 @@ - (void)testZeroElevationShouldReturnEmptyShadow { - (void)testLowElevationShouldReturnShadow { // Given - MDCShadow *shadow = MDCShadowForElevation(2); + MDCShadow *shadow = MDCShadowForElevation(2, MDCShadowsCollectionDefault()); // Then XCTAssertGreaterThan(shadow.opacity, 0); @@ -43,7 +47,7 @@ - (void)testLowElevationShouldReturnShadow { - (void)testHighElevationShouldReturnShadow { // Given - MDCShadow *shadow = MDCShadowForElevation(24); + MDCShadow *shadow = MDCShadowForElevation(24, MDCShadowsCollectionDefault()); // Then XCTAssertGreaterThan(shadow.opacity, 0); @@ -53,8 +57,8 @@ - (void)testHighElevationShouldReturnShadow { - (void)testSameElevationShouldBeEqualToSelf { // Given - MDCShadow *lowElevationShadowA = MDCShadowForElevation(2); - MDCShadow *lowElevationShadowB = MDCShadowForElevation(2); + MDCShadow *lowElevationShadowA = MDCShadowForElevation(2, MDCShadowsCollectionDefault()); + MDCShadow *lowElevationShadowB = MDCShadowForElevation(2, MDCShadowsCollectionDefault()); // Then XCTAssertEqualObjects(lowElevationShadowA, lowElevationShadowB); @@ -62,29 +66,81 @@ - (void)testSameElevationShouldBeEqualToSelf { - (void)testLowElevationShouldReturnDifferentShadowFromHighElevation { // Given - MDCShadow *lowElevationShadow = MDCShadowForElevation(2); - MDCShadow *highElevationShadow = MDCShadowForElevation(24); + MDCShadow *lowElevationShadow = MDCShadowForElevation(2, MDCShadowsCollectionDefault()); + MDCShadow *highElevationShadow = MDCShadowForElevation(24, MDCShadowsCollectionDefault()); // Then XCTAssertNotEqualObjects(lowElevationShadow, highElevationShadow); } -- (void)testBuilderReturnsExpectedValues { +- (void)testDefaultShadowElevationValuesAreReturnedCorrectlyForGivenElevation { + // Given + MDCShadowsCollection *shadowsCollection = MDCShadowsCollectionDefault(); + + // When + MDCShadow *zeroShadow = MDCShadowForElevation(0, shadowsCollection); + MDCShadow *oneShadow = MDCShadowForElevation(1, shadowsCollection); + MDCShadow *threeShadow = MDCShadowForElevation(3, shadowsCollection); + MDCShadow *sixShadow = MDCShadowForElevation(6, shadowsCollection); + MDCShadow *eightShadow = MDCShadowForElevation(8, shadowsCollection); + MDCShadow *twelveShadow = MDCShadowForElevation(12, shadowsCollection); + + // Then + XCTAssertEqualObjects(zeroShadow, [shadowsCollection shadowForElevation:0]); + XCTAssertEqualObjects(oneShadow, [shadowsCollection shadowForElevation:1]); + XCTAssertEqualObjects(threeShadow, [shadowsCollection shadowForElevation:3]); + XCTAssertEqualObjects(sixShadow, [shadowsCollection shadowForElevation:6]); + XCTAssertEqualObjects(eightShadow, [shadowsCollection shadowForElevation:8]); + XCTAssertEqualObjects(twelveShadow, [shadowsCollection shadowForElevation:12]); +} + +- (void)testDefaultShadowElevationValuesAreReturnedCorrectlyForOutOfBoundsElevations { + // Given + MDCShadowsCollection *shadowsCollection = MDCShadowsCollectionDefault(); + + // When + MDCShadow *negativeShadow = MDCShadowForElevation(-4, shadowsCollection); + MDCShadow *bigShadow = MDCShadowForElevation(13, shadowsCollection); + MDCShadow *hugeShadow = MDCShadowForElevation(3000, shadowsCollection); + + // Then + XCTAssertEqualObjects(negativeShadow, [shadowsCollection shadowForElevation:0]); + XCTAssertEqualObjects(bigShadow, [shadowsCollection shadowForElevation:12]); + XCTAssertEqualObjects(hugeShadow, [shadowsCollection shadowForElevation:12]); +} + +- (void)testDefaultShadowElevationValuesAreReturnedCorrectlyForInBetweenValueElevations { // Given - MDCShadowBuilder *shadowBuilder = [[MDCShadowBuilder alloc] init]; + MDCShadowsCollection *shadowsCollection = MDCShadowsCollectionDefault(); // When - shadowBuilder.opacity = 42; - shadowBuilder.radius = 23; - shadowBuilder.offset = CGSizeMake(1, 2); + MDCShadow *shadow1 = MDCShadowForElevation(0.1, shadowsCollection); + MDCShadow *shadow2 = MDCShadowForElevation(2.314, shadowsCollection); + MDCShadow *shadow3 = MDCShadowForElevation(4, shadowsCollection); + MDCShadow *shadow4 = MDCShadowForElevation(6.01, shadowsCollection); + MDCShadow *shadow5 = MDCShadowForElevation(9.11, shadowsCollection); - MDCShadow *shadow = [shadowBuilder build]; + // Then + XCTAssertEqualObjects(shadow1, [shadowsCollection shadowForElevation:1]); + XCTAssertEqualObjects(shadow2, [shadowsCollection shadowForElevation:3]); + XCTAssertEqualObjects(shadow3, [shadowsCollection shadowForElevation:6]); + XCTAssertEqualObjects(shadow4, [shadowsCollection shadowForElevation:8]); + XCTAssertEqualObjects(shadow5, [shadowsCollection shadowForElevation:12]); +} + +- (void)testSettingNewShadowInstanceUsingBuilder { + // Given + MDCShadow *shadow = [[MDCShadowBuilder builderWithOpacity:0.1 + radius:0.2 + offset:CGSizeMake(0.3, 0.4)] build]; + + // When + MDCShadowsCollection *shadowsCollection = + [[MDCShadowsCollectionBuilder builderWithShadow:shadow forElevation:4] build]; // Then - XCTAssertEqualWithAccuracy(shadow.opacity, 42, 1e-6); - XCTAssertEqualWithAccuracy(shadow.radius, 23, 1e-6); - XCTAssertEqualWithAccuracy(shadow.offset.width, 1, 1e-6); - XCTAssertEqualWithAccuracy(shadow.offset.height, 2, 1e-6); + MDCShadow *fetchedShadow = MDCShadowForElevation(4, shadowsCollection); + XCTAssertEqualObjects(shadow, fetchedShadow); } @end