Skip to content

Commit c990441

Browse files
yarneomaterial-automation
authored andcommitted
[Button] Add opt-in new performant shadow support.
PiperOrigin-RevId: 369820180
1 parent e5a68ba commit c990441

File tree

8 files changed

+608
-24
lines changed

8 files changed

+608
-24
lines changed

components/Buttons/src/MDCButton.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import "MaterialElevation.h"
1919
#import "MaterialInk.h"
2020
#import "MaterialRipple.h"
21+
#import "MaterialShadow.h"
2122
#import "MaterialShadowElevations.h"
2223
#import "MaterialShapes.h"
2324

components/Buttons/src/MDCButton.m

Lines changed: 125 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import "MaterialElevation.h"
1919
#import "MaterialInk.h"
2020
#import "MaterialRipple.h"
21+
#import "MaterialShadow.h"
2122
#import "MaterialShadowElevations.h"
2223
#import "MaterialShapeLibrary.h"
2324
#import "MaterialShapes.h"
@@ -112,6 +113,9 @@ @interface MDCButton () {
112113
BOOL _mdc_adjustsFontForContentSizeCategory;
113114
BOOL _cornerRadiusObserverAdded;
114115
CGFloat _inkMaxRippleRadius;
116+
117+
MDCShapeMediator *_shapedLayer;
118+
CGFloat _currentElevation;
115119
}
116120
@property(nonatomic, strong, readonly, nonnull) MDCStatefulRippleView *rippleView;
117121
#pragma clang diagnostic push
@@ -130,14 +134,21 @@ @interface MDCButton () {
130134

131135
@implementation MDCButton
132136

137+
static BOOL gEnablePerformantShadow = NO;
138+
133139
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
134140
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
135141
@synthesize visibleAreaInsets = _visibleAreaInsets;
136142
@synthesize visibleAreaLayoutGuide = _visibleAreaLayoutGuide;
143+
@synthesize shadowsCollection = _shadowsCollection;
137144
@dynamic layer;
138145

139146
+ (Class)layerClass {
140-
return [MDCShapedShadowLayer class];
147+
if (gEnablePerformantShadow) {
148+
return [super class];
149+
} else {
150+
return [MDCShapedShadowLayer class];
151+
}
141152
}
142153

143154
- (instancetype)init {
@@ -170,7 +181,9 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
170181

171182
// Storyboards will set the backgroundColor via the UIView backgroundColor setter, so we have
172183
// to write that in to our _backgroundColors dictionary.
173-
_backgroundColors[@(UIControlStateNormal)] = self.layer.shapedBackgroundColor;
184+
_backgroundColors[@(UIControlStateNormal)] = gEnablePerformantShadow
185+
? _shapedLayer.shapedBackgroundColor
186+
: self.layer.shapedBackgroundColor;
174187
[self updateBackgroundColor];
175188
}
176189
return self;
@@ -179,6 +192,9 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
179192
- (void)commonMDCButtonInit {
180193
// TODO(b/142861610): Default to `NO`, then remove once all internal usage is migrated.
181194
_enableTitleFontForState = YES;
195+
if (gEnablePerformantShadow) {
196+
_shapedLayer = [[MDCShapeMediator alloc] initWithViewLayer:self.layer];
197+
}
182198
_disabledAlpha = MDCButtonDisabledAlpha;
183199
_enabledAlpha = self.alpha;
184200
_uppercaseTitle = YES;
@@ -191,6 +207,7 @@ - (void)commonMDCButtonInit {
191207
_accessibilityTraitsIncludesButton = YES;
192208
_adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable = YES;
193209
_mdc_overrideBaseElevation = -1;
210+
_currentElevation = 0;
194211

195212
if (!_backgroundColors) {
196213
// _backgroundColors may have already been initialized by setting the backgroundColor setter.
@@ -206,11 +223,12 @@ - (void)commonMDCButtonInit {
206223
#endif
207224

208225
self.layer.cornerRadius = MDCButtonDefaultCornerRadius;
209-
if (!self.layer.shapeGenerator) {
210-
self.layer.shadowPath = [self boundingPath].CGPath;
226+
if (gEnablePerformantShadow) {
227+
self.layer.shadowColor = MDCShadowColor().CGColor;
228+
} else {
229+
self.layer.shadowColor = [UIColor blackColor].CGColor;
230+
self.layer.elevation = [self elevationForState:self.state];
211231
}
212-
self.layer.shadowColor = [UIColor blackColor].CGColor;
213-
self.layer.elevation = [self elevationForState:self.state];
214232

215233
_shadowColors = [NSMutableDictionary dictionary];
216234
_shadowColors[@(UIControlStateNormal)] = [UIColor colorWithCGColor:self.layer.shadowColor];
@@ -336,8 +354,15 @@ - (void)layoutSubviews {
336354
}
337355
}
338356

339-
if (!self.layer.shapeGenerator) {
340-
self.layer.shadowPath = [self boundingPath].CGPath;
357+
if (gEnablePerformantShadow) {
358+
if (_shapedLayer.shapeGenerator) {
359+
[_shapedLayer layoutShapedSublayers];
360+
}
361+
[self updateShadow];
362+
} else {
363+
if (!self.layer.shapeGenerator) {
364+
self.layer.shadowPath = [self boundingPath].CGPath;
365+
}
341366
}
342367

343368
// Center unbounded ink view frame taking into account possible insets using contentRectForBounds.
@@ -732,12 +757,17 @@ - (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior {
732757

733758
- (void)animateButtonToHeightForState:(UIControlState)state {
734759
CGFloat newElevation = [self elevationForState:state];
735-
if (MDCCGFloatEqual(self.layer.elevation, newElevation)) {
760+
if (MDCCGFloatEqual(self.mdc_currentElevation, newElevation)) {
736761
return;
737762
}
763+
_currentElevation = newElevation;
738764
[CATransaction begin];
739765
[CATransaction setAnimationDuration:MDCButtonAnimationDuration];
740-
self.layer.elevation = newElevation;
766+
if (gEnablePerformantShadow) {
767+
[self updateShadow];
768+
} else {
769+
self.layer.elevation = newElevation;
770+
}
741771
[CATransaction commit];
742772
[self mdc_elevationDidChange];
743773
}
@@ -754,7 +784,8 @@ - (void)setBackgroundColor:(nullable UIColor *)backgroundColor {
754784
}
755785

756786
- (UIColor *)backgroundColor {
757-
return self.layer.shapedBackgroundColor;
787+
return gEnablePerformantShadow ? _shapedLayer.shapedBackgroundColor
788+
: self.layer.shapedBackgroundColor;
758789
}
759790

760791
- (UIColor *)backgroundColorForState:(UIControlState)state {
@@ -818,10 +849,15 @@ - (void)setElevation:(CGFloat)elevation forState:(UIControlState)state {
818849
_userElevations[@(state)] = @(elevation);
819850
MDCShadowElevation newElevation = [self elevationForState:self.state];
820851
// If no change to the current elevation, don't perform updates
821-
if (MDCCGFloatEqual(newElevation, self.layer.elevation)) {
852+
if (MDCCGFloatEqual(newElevation, self.mdc_currentElevation)) {
822853
return;
823854
}
824-
self.layer.elevation = newElevation;
855+
_currentElevation = newElevation;
856+
if (gEnablePerformantShadow) {
857+
[self updateShadow];
858+
} else {
859+
self.layer.elevation = newElevation;
860+
}
825861
[self mdc_elevationDidChange];
826862

827863
// The elevation of the normal state controls whether this button is flat or not, and flat buttons
@@ -832,6 +868,18 @@ - (void)setElevation:(CGFloat)elevation forState:(UIControlState)state {
832868
}
833869
}
834870

871+
- (void)updateShadow {
872+
if (_shapedLayer.shapeGenerator == nil) {
873+
MDCConfigureShadowForView(self,
874+
[self.shadowsCollection shadowForElevation:self.mdc_currentElevation],
875+
[self shadowColorForState:self.state] ?: MDCShadowColor());
876+
} else {
877+
MDCConfigureShadowForViewWithPath(
878+
self, [self.shadowsCollection shadowForElevation:self.mdc_currentElevation],
879+
[self shadowColorForState:self.state] ?: MDCShadowColor(), self.layer.shadowPath);
880+
}
881+
}
882+
835883
#pragma mark - Border Color
836884

837885
- (UIColor *)borderColorForState:(UIControlState)state {
@@ -891,7 +939,11 @@ - (void)updateBorderWidth {
891939
// We fall back to UIControlStateNormal if there is no value for the current state.
892940
width = _borderWidths[@(UIControlStateNormal)];
893941
}
894-
self.layer.shapedBorderWidth = (width != nil) ? (CGFloat)width.doubleValue : 0;
942+
if (gEnablePerformantShadow) {
943+
_shapedLayer.shapedBorderWidth = (width != nil) ? (CGFloat)width.doubleValue : 0;
944+
} else {
945+
self.layer.shapedBorderWidth = (width != nil) ? (CGFloat)width.doubleValue : 0;
946+
}
895947
}
896948

897949
#pragma mark - Title Font
@@ -942,7 +994,14 @@ - (void)setTitleFont:(nullable UIFont *)font forState:(UIControlState)state {
942994
#pragma mark - MaterialElevation
943995

944996
- (CGFloat)mdc_currentElevation {
945-
return [self elevationForState:self.state];
997+
return _currentElevation;
998+
}
999+
1000+
- (MDCShadowsCollection *)shadowsCollection {
1001+
if (!_shadowsCollection) {
1002+
_shadowsCollection = MDCShadowsCollectionDefault();
1003+
}
1004+
return _shadowsCollection;
9461005
}
9471006

9481007
#pragma mark - Private methods
@@ -1025,7 +1084,11 @@ - (void)updateAlphaAndBackgroundColorAnimated:(BOOL)animated {
10251084
- (void)updateBackgroundColor {
10261085
// When shapeGenerator is unset then self.layer.shapedBackgroundColor sets the layer's
10271086
// backgroundColor. Whereas when shapeGenerator is set the sublayer's fillColor is set.
1028-
self.layer.shapedBackgroundColor = [self backgroundColorForState:self.state];
1087+
if (gEnablePerformantShadow) {
1088+
_shapedLayer.shapedBackgroundColor = [self backgroundColorForState:self.state];
1089+
} else {
1090+
self.layer.shapedBackgroundColor = [self backgroundColorForState:self.state];
1091+
}
10291092
[self updateDisabledTitleColor];
10301093
}
10311094

@@ -1060,7 +1123,11 @@ - (void)updateBorderColor {
10601123
// We fall back to UIControlStateNormal if there is no value for the current state.
10611124
color = _borderColors[@(UIControlStateNormal)];
10621125
}
1063-
self.layer.shapedBorderColor = color ?: NULL;
1126+
if (gEnablePerformantShadow) {
1127+
_shapedLayer.shapedBorderColor = color ?: NULL;
1128+
} else {
1129+
self.layer.shapedBorderColor = color ?: NULL;
1130+
}
10641131
}
10651132

10661133
- (void)updateTitleFont {
@@ -1088,29 +1155,53 @@ - (void)configureLayerWithShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator
10881155
if (shapeGenerator) {
10891156
self.layer.shadowPath = nil;
10901157
} else {
1091-
self.layer.shadowPath = [self boundingPath].CGPath;
1158+
if (gEnablePerformantShadow) {
1159+
MDCConfigureShadowForView(
1160+
self, [self.shadowsCollection shadowForElevation:self.mdc_currentElevation],
1161+
[self shadowColorForState:self.state] ?: MDCShadowColor());
1162+
} else {
1163+
self.layer.shadowPath = [self boundingPath].CGPath;
1164+
}
1165+
}
1166+
1167+
if (gEnablePerformantShadow) {
1168+
_shapedLayer.shapeGenerator = shapeGenerator;
1169+
} else {
1170+
self.layer.shapeGenerator = shapeGenerator;
10921171
}
1093-
self.layer.shapeGenerator = shapeGenerator;
10941172
// The imageView is added very early in the lifecycle of a UIButton, therefore we need to move
10951173
// the colorLayer behind the imageView otherwise the image will not show.
10961174
// Because the inkView needs to go below the imageView, but above the colorLayer
10971175
// we need to have the colorLayer be at the back
1098-
[self.layer.colorLayer removeFromSuperlayer];
1099-
if (self.enableRippleBehavior) {
1100-
[self.layer insertSublayer:self.layer.colorLayer below:self.rippleView.layer];
1176+
if (gEnablePerformantShadow) {
1177+
[_shapedLayer.colorLayer removeFromSuperlayer];
1178+
if (self.enableRippleBehavior) {
1179+
[self.layer insertSublayer:_shapedLayer.colorLayer below:self.rippleView.layer];
1180+
} else {
1181+
[self.layer insertSublayer:_shapedLayer.colorLayer below:self.inkView.layer];
1182+
}
11011183
} else {
1102-
[self.layer insertSublayer:self.layer.colorLayer below:self.inkView.layer];
1184+
[self.layer.colorLayer removeFromSuperlayer];
1185+
if (self.enableRippleBehavior) {
1186+
[self.layer insertSublayer:self.layer.colorLayer below:self.rippleView.layer];
1187+
} else {
1188+
[self.layer insertSublayer:self.layer.colorLayer below:self.inkView.layer];
1189+
}
11031190
}
11041191
[self updateBackgroundColor];
11051192
[self updateInkForShape];
11061193
}
11071194

11081195
- (id<MDCShapeGenerating>)shapeGenerator {
1196+
if (gEnablePerformantShadow) {
1197+
return _shapedLayer.shapeGenerator;
1198+
}
11091199
return self.layer.shapeGenerator;
11101200
}
11111201

11121202
- (void)updateInkForShape {
1113-
CGRect boundingBox = CGPathGetBoundingBox(self.layer.shapeLayer.path);
1203+
CGRect boundingBox = CGPathGetBoundingBox(gEnablePerformantShadow ? _shapedLayer.shapeLayer.path
1204+
: self.layer.shapeLayer.path);
11141205
self.inkView.maxRippleRadius =
11151206
(CGFloat)(hypot(CGRectGetHeight(boundingBox), CGRectGetWidth(boundingBox)) / 2 + 10);
11161207
self.inkView.layer.masksToBounds = NO;
@@ -1348,4 +1439,14 @@ - (void)inferMinimumAndMaximumSize {
13481439
}
13491440
}
13501441

1442+
#pragma mark - Performant Shadow Toggle
1443+
1444+
+ (void)setEnablePerformantShadow:(BOOL)enable {
1445+
gEnablePerformantShadow = enable;
1446+
}
1447+
1448+
+ (BOOL)enablePerformantShadow {
1449+
return gEnablePerformantShadow;
1450+
}
1451+
13511452
@end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#import <UIKit/UIKit.h>
2+
3+
#import "MDCButton.h"
4+
5+
/**
6+
This category is temporary and is meant to allow us to A/B test the performance improvements of
7+
using the new shadow vs the old shadow. When such test is finalized, we will delete this file and
8+
the internal corresponding static property and move all clients to the new shadow.
9+
TODO(b/185206035): Remove category for enabling performant shadow.
10+
*/
11+
@interface MDCButton (ShadowsPrivate)
12+
13+
/**
14+
Returns a bool indicating if the performant shadow is turned on or not.
15+
*/
16+
+ (BOOL)enablePerformantShadow;
17+
18+
/**
19+
Enables/disables the performant shadow for the button.
20+
*/
21+
+ (void)setEnablePerformantShadow:(BOOL)enable;
22+
23+
@end

components/Buttons/src/private/MDCButton+Subclassing.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,17 @@
4141
- (nonnull UIBezierPath *)boundingPath;
4242

4343
@end
44+
45+
@interface MDCButton ()
46+
47+
/**
48+
A collection of MDCShadow instances each assigned an elevation (in dp).
49+
50+
To create your own MDCShadowsCollection, please use the provided MDCShadowsCollectionBuilder and
51+
populate it with MDCShadow instances using the provided MDCShadowBuilder.
52+
53+
Defaults to MDCShadowsCollectionDefault().
54+
*/
55+
@property(nonatomic, strong, null_resettable) MDCShadowsCollection *shadowsCollection;
56+
57+
@end

0 commit comments

Comments
 (0)