-
Notifications
You must be signed in to change notification settings - Fork 944
/
MDCAppBarButtonBarBuilder.m
301 lines (248 loc) · 12 KB
/
MDCAppBarButtonBarBuilder.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/*
Copyright 2016-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 "MDCAppBarButtonBarBuilder.h"
#import <objc/runtime.h>
#import <MDFInternationalization/MDFInternationalization.h>
#import "MaterialButtons.h"
#import "MDCButtonBarButton.h"
#import "MDCButtonBar+Private.h"
// Additional insets for the left-most or right-most items, primarily for image buttons.
static const CGFloat kEdgeButtonAdditionalMarginPhone = 4.f;
static const CGFloat kEdgeButtonAdditionalMarginPad = 12.f;
// The default MDCButton's alpha for display state is 0.1f which in the context of bar buttons
// makes it practically invisible. Setting button to a higher opacity is closer to what the
// button should look like when it is disabled.
static const CGFloat kDisabledButtonAlpha = 0.45f;
// Content insets for text-only buttons.
static const UIEdgeInsets kTextOnlyButtonInset = {0, 24.f, 0, 24.f};
// Content insets for image-only buttons.
static const UIEdgeInsets kImageOnlyButtonInset = {0, 12.0f, 0, 12.0f};
// Indiana Jones style placeholder view for UINavigationBar. Ownership of UIBarButtonItem.customView
// and UINavigationItem.titleView are normally transferred to UINavigationController but we plan to
// steal them away. In order to avoid crashing during KVO updates, we steal the view away and
// replace it with a sandbag view.
@interface MDCButtonBarSandbagView : UIView
@end
@interface UIBarButtonItem (MDCHeaderInternal)
// Internal version of the standard -customView property. When an item is pushed onto a
// UINavigationController stack, any -customView object is moved over to this property. This
// prevents UINavigationController from adding the customView to its own view hierarchy.
@property(nonatomic, strong, setter=mdc_setCustomView:) UIView *mdc_customView;
@end
@implementation MDCAppBarButtonBarBuilder
#pragma mark - MDCBarButtonItemBuilding
- (UIView *)buttonBar:(MDCButtonBar *)buttonBar
viewForItem:(UIBarButtonItem *)buttonItem
layoutHints:(MDCBarButtonItemLayoutHints)layoutHints {
if (buttonItem == nil) {
return nil;
}
// Transfer custom view ownership if necessary.
[self transferCustomViewOwnershipForBarButtonItem:buttonItem];
// Take the real custom view if it exists instead of sandbag view.
UIView *customView =
buttonItem.mdc_customView ? buttonItem.mdc_customView : buttonItem.customView;
if (customView) {
return customView;
}
// NOTE: This assertion does not occur in release builds because it is accessing a private api.
#if DEBUG
NSAssert(![[buttonItem valueForKey:@"isSystemItem"] boolValue],
@"Instances of %@ must not be initialized with %@ when working with %@."
@" This is because we cannot extract the system item type from the item.",
NSStringFromClass([buttonItem class]),
NSStringFromSelector(@selector(initWithBarButtonSystemItem:target:action:)),
NSStringFromClass([MDCButtonBar class]));
#endif
MDCButtonBarButton *button = [[MDCButtonBarButton alloc] init];
[button setBackgroundColor:[UIColor clearColor] forState:UIControlStateNormal];
button.disabledAlpha = kDisabledButtonAlpha;
if (buttonBar.inkColor) {
button.inkColor = buttonBar.inkColor;
}
button.exclusiveTouch = YES;
[MDCAppBarButtonBarBuilder configureButton:button fromButtonItem:buttonItem];
[button setTitleColor:self.buttonTitleColor forState:UIControlStateNormal];
[button setUnderlyingColorHint:self.buttonUnderlyingColor];
[self updateButton:button withItem:buttonItem barMetrics:UIBarMetricsDefault];
// Contrary to intuition, UIKit provides the UIBarButtonItem as the action's first argument when
// bar buttons are tapped, NOT the button itself. Simply adding the item's target/action to the
// button does not allow us to pass the expected argument to the target.
//
// MDCButtonBar provides didTapButton:event: to which we can pass button events
// so that the correct argument is ultimately passed along.
[button addTarget:buttonBar
action:@selector(didTapButton:event:)
forControlEvents:UIControlEventTouchUpInside];
UIEdgeInsets contentInsets = [MDCAppBarButtonBarBuilder
contentInsetsForButton:button
layoutPosition:buttonBar.layoutPosition
layoutHints:layoutHints
layoutDirection:[buttonBar mdf_effectiveUserInterfaceLayoutDirection]
userInterfaceIdiom:[self usePadInsetsForButtonBar:buttonBar] ?
UIUserInterfaceIdiomPad : UIUserInterfaceIdiomPhone];
button.contentEdgeInsets = contentInsets;
button.enabled = buttonItem.enabled;
button.accessibilityLabel = buttonItem.accessibilityLabel;
button.accessibilityHint = buttonItem.accessibilityHint;
button.accessibilityValue = buttonItem.accessibilityValue;
button.accessibilityIdentifier = buttonItem.accessibilityIdentifier;
return button;
}
#pragma mark - Private
// Used to determine whether or not to apply insets relevant for iPad or use smaller iPhone size
// Because only widths are affected, we use horizontal size class
- (BOOL)usePadInsetsForButtonBar:(MDCButtonBar *)buttonBar {
const BOOL isPad = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad;
if (isPad && buttonBar.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
return YES;
}
return NO;
}
+ (UIEdgeInsets)contentInsetsForButton:(MDCButton *)button
layoutPosition:(MDCButtonBarLayoutPosition)layoutPosition
layoutHints:(MDCBarButtonItemLayoutHints)layoutHints
layoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom {
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
UIEdgeInsets (^addInsets)(UIEdgeInsets, UIEdgeInsets) = ^(UIEdgeInsets i1, UIEdgeInsets i2) {
UIEdgeInsets sum = i1;
sum.left += i2.left;
sum.top += i2.top;
sum.right += i2.right;
sum.bottom += i2.bottom;
return sum;
};
if ([[button currentTitle] length]) { // Text-only buttons.
contentInsets = addInsets(contentInsets, kTextOnlyButtonInset);
} else if ([button currentImage]) { // Image-only buttons.
contentInsets = addInsets(contentInsets, kImageOnlyButtonInset);
BOOL isPad = userInterfaceIdiom == UIUserInterfaceIdiomPad;
CGFloat additionalInset =
(isPad ? kEdgeButtonAdditionalMarginPad : kEdgeButtonAdditionalMarginPhone);
BOOL isFirstButton = (layoutHints & MDCBarButtonItemLayoutHintsIsFirstButton) ==
MDCBarButtonItemLayoutHintsIsFirstButton;
BOOL isLastButton = (layoutHints & MDCBarButtonItemLayoutHintsIsLastButton) ==
MDCBarButtonItemLayoutHintsIsLastButton;
if (isFirstButton && layoutPosition == MDCButtonBarLayoutPositionLeading) {
// Left-most button in LTR, and right-most button in RTL.
if (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
contentInsets.left += additionalInset;
} else {
contentInsets.right += additionalInset;
}
} else if (isFirstButton && layoutPosition == MDCButtonBarLayoutPositionTrailing) {
// Right-most button in LTR, and left-most button in RTL.
if (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
contentInsets.right += additionalInset;
} else {
contentInsets.left += additionalInset;
}
}
if (isLastButton && layoutPosition == MDCButtonBarLayoutPositionTrailing) {
// Left-most button in LTR, and right-most button in RTL.
if (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
contentInsets.left += additionalInset;
} else {
contentInsets.right += additionalInset;
}
} else if (isLastButton && layoutPosition == MDCButtonBarLayoutPositionLeading) {
// Right-most button in LTR, and left-most button in RTL.
if (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
contentInsets.right += additionalInset;
} else {
contentInsets.left += additionalInset;
}
}
} else {
NSAssert(0, @"No button title or image");
}
return contentInsets;
}
+ (void)configureButton:(MDCButton *)destinationButton
fromButtonItem:(UIBarButtonItem *)sourceButtonItem {
if (sourceButtonItem == nil || destinationButton == nil) {
return;
}
if (sourceButtonItem.title != nil) {
[destinationButton setTitle:sourceButtonItem.title forState:UIControlStateNormal];
}
if (sourceButtonItem.image != nil) {
[destinationButton setImage:sourceButtonItem.image forState:UIControlStateNormal];
}
if (sourceButtonItem.tintColor != nil) {
destinationButton.tintColor = sourceButtonItem.tintColor;
}
if (sourceButtonItem.title) {
destinationButton.inkStyle = MDCInkStyleBounded;
} else {
destinationButton.inkStyle = MDCInkStyleUnbounded;
}
destinationButton.tag = sourceButtonItem.tag;
}
- (void)updateButton:(UIButton *)button
withItem:(UIBarButtonItem *)item
barMetrics:(UIBarMetrics)barMetrics {
[self updateButton:button withItem:item forState:UIControlStateNormal barMetrics:barMetrics];
[self updateButton:button withItem:item forState:UIControlStateHighlighted barMetrics:barMetrics];
[self updateButton:button withItem:item forState:UIControlStateDisabled barMetrics:barMetrics];
}
- (void)updateButton:(UIButton *)button
withItem:(UIBarButtonItem *)item
forState:(UIControlState)state
barMetrics:(UIBarMetrics)barMetrics {
NSString *title = item.title ? item.title : @"";
if ([UIButton instancesRespondToSelector:@selector(setAttributedTitle:forState:)]) {
NSMutableDictionary<NSString *, id> *attributes = [NSMutableDictionary dictionary];
// UIBarButtonItem's appearance proxy values don't appear to come "for free" like they do with
// typical UIView instances, so we're attempting to recreate the behavior here.
NSArray *appearanceProxies = @[ [item.class appearance] ];
for (UIBarButtonItem *appearance in appearanceProxies) {
[attributes addEntriesFromDictionary:[appearance titleTextAttributesForState:state]];
}
[attributes addEntriesFromDictionary:[item titleTextAttributesForState:state]];
if ([attributes count] > 0) {
[button
setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:attributes]
forState:state];
}
} else {
[button setTitle:title forState:state];
}
UIImage *backgroundImage = [item backgroundImageForState:state barMetrics:barMetrics];
if (backgroundImage) {
[button setBackgroundImage:backgroundImage forState:state];
}
}
- (void)transferCustomViewOwnershipForBarButtonItem:(UIBarButtonItem *)barButtonItem {
UIView *customView = barButtonItem.customView;
if (customView && ![customView isKindOfClass:[MDCButtonBarSandbagView class]]) {
// Transfer ownership of any UIBarButtonItem.customView to the internal property
// so that UINavigationController won't steal the view from us.
barButtonItem.mdc_customView = customView;
barButtonItem.customView = [[MDCButtonBarSandbagView alloc] init];
}
}
@end
@implementation MDCButtonBarSandbagView
@end
@implementation UIBarButtonItem (MDCHeaderInternal)
@dynamic mdc_customView;
- (UIView *)mdc_customView {
return objc_getAssociatedObject(self, _cmd);
}
- (void)mdc_setCustomView:(UIView *)customView {
objc_setAssociatedObject(self, @selector(mdc_customView), customView,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end