blob: ef88d82845d25afaed05e11c991b922ca3da171a [file] [log] [blame]
/*
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 "MDCOverlayWindow.h"
#import <objc/runtime.h>
#import "UIApplication+AppExtensions.h"
/**
A container view for overlay views.
Used by MDCOverlayWindow, overlay views are added to the overlay window container when
@c activateOverlay is called and removed from the overlay window when @c deactivateOverlay is
called.
*/
@interface MDCOverlayWindowContainerView : UIView
@end
@interface MDCOverlayWindow ()
@property(nonatomic, strong) NSMutableArray *overlays;
@property(nonatomic, strong) MDCOverlayWindowContainerView *overlayView;
// Forward declaration so that MDCOverlayWindowContainerView can call this method.
- (void)noteOverlayRemoved:(UIView *)overlay;
@end
@implementation MDCOverlayWindowContainerView
- (void)willRemoveSubview:(UIView *)subview {
[super willRemoveSubview:subview];
MDCOverlayWindow *window = (MDCOverlayWindow *)self.window;
[window noteOverlayRemoved:subview];
}
// Only allow a tap if it explicitly hit one of the overlays. Otherwise, behave as if this view
// doesn't exist at all.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
@end
@implementation MDCOverlayWindow
- (instancetype)init {
self = [super init];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
self.backgroundColor = [UIColor clearColor];
_overlays = [[NSMutableArray alloc] init];
_overlayView = [[MDCOverlayWindowContainerView alloc] initWithFrame:self.bounds];
_overlayView.autoresizingMask =
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
_overlayView.backgroundColor = [UIColor clearColor];
[self addSubview:_overlayView];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(handleRotationNotification:)
name:UIApplicationWillChangeStatusBarOrientationNotification
object:nil];
// Set a sane initial position.
[self updateOverlayViewForOrientation:[[UIApplication mdc_safeSharedApplication]
statusBarOrientation]];
// Set a sane hidden state.
[self updateOverlayHiddenState];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Rotation
- (void)updateOverlayViewForOrientation:(UIInterfaceOrientation)orientation {
// On iOS 8, the window orientation is corrected logically after transforms, so there is
// no need to apply this transform correction like we do for iOS 7 and below.
BOOL hasFixedCoordinateSpace = NO;
UIScreen *screen = [UIScreen mainScreen];
hasFixedCoordinateSpace = [screen respondsToSelector:@selector(fixedCoordinateSpace)];
if (!hasFixedCoordinateSpace) {
CGAffineTransform transform = CGAffineTransformIdentity;
BOOL swapBounds = NO;
switch (orientation) {
case UIInterfaceOrientationLandscapeLeft:
transform = CGAffineTransformMakeRotation((CGFloat)-M_PI_2);
swapBounds = YES;
break;
case UIInterfaceOrientationLandscapeRight:
transform = CGAffineTransformMakeRotation((CGFloat)M_PI_2);
swapBounds = YES;
break;
case UIInterfaceOrientationPortraitUpsideDown:
transform = CGAffineTransformMakeRotation((CGFloat)M_PI);
break;
case UIInterfaceOrientationPortrait:
default:
break;
}
CGRect bounds = self.bounds;
if (swapBounds) {
bounds = CGRectMake(0, 0, bounds.size.height, bounds.size.width);
}
self.overlayView.bounds = bounds;
self.overlayView.transform = transform;
[self.overlayView layoutIfNeeded];
}
}
// This method is called within an animation block, so we simply need to update the overlay view.
- (void)handleRotationNotification:(NSNotification *)notification {
UIInterfaceOrientation orientation =
[notification.userInfo[UIApplicationStatusBarOrientationUserInfoKey] integerValue];
[self updateOverlayViewForOrientation:orientation];
}
#pragma mark - Window positioning
// Regardless of what was added to this window, ensure that the overlay view is on top.
- (void)didAddSubview:(UIView *)subview {
[super didAddSubview:subview];
[self bringSubviewToFront:self.overlayView];
}
#pragma mark - Overlay Activation
- (void)updateOverlayHiddenState {
BOOL hasOverlays = [self.overlays count] > 0;
self.overlayView.hidden = !hasOverlays;
[self updateAccessibilityIsModal];
}
- (void)updateAccessibilityIsModal {
BOOL containsModal = NO;
for (UIView *overlay in self.overlays) {
if (overlay.accessibilityViewIsModal) {
containsModal = YES;
break;
}
}
self.overlayView.accessibilityViewIsModal = containsModal;
}
- (void)noteOverlayRemoved:(UIView *)overlay {
if (!overlay) {
return;
}
// If the overlay argument wasn't managed by us, don't do anything when it goes away.
if (![self.overlays containsObject:overlay]) {
return;
}
// Clean up the level information stored on the view.
[self removeLevelForOverlay:overlay];
// Stop tracking the overlay view.
[self.overlays removeObject:overlay];
// Show or hide ourself as needed.
[self updateOverlayHiddenState];
}
- (void)activateOverlay:(UIView *)overlay withLevel:(UIWindowLevel)level {
if (!overlay) {
return;
}
// Make sure the that the overlay is out of the view hierarchy, even our own (if this is a
// re-activation with a new level). If @c overlay is already in the overlay view, then this call
// will take care of cleaning up @c self.overlays, by way of @c noteOverlayRemoved:.
[overlay removeFromSuperview];
// Default to adding the overlay at the very end (on top) of all the other overlays. We'll check
// the existing overlays to see if this one needs to go in before.
__block NSUInteger insertionIndex = self.overlays.count;
// Because @c self.overlays is already sorted by level, we can pick the first index which has a
// level larger than @c level.
[self.overlays enumerateObjectsUsingBlock:^(UIView *existing, NSUInteger idx, BOOL *stop) {
UIWindowLevel existingLevel = [self windowLevelForOverlay:existing];
if (level < existingLevel) {
insertionIndex = idx;
*stop = YES;
}
}];
// Make sure that the overlay is as large as the overlay container view before adding it.
overlay.bounds = self.overlayView.bounds;
overlay.center = CGPointMake(CGRectGetMidX(overlay.bounds), CGRectGetMidY(overlay.bounds));
overlay.translatesAutoresizingMaskIntoConstraints = YES;
overlay.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self setLevel:level forOverlay:overlay];
[self.overlayView insertSubview:overlay atIndex:insertionIndex];
[self.overlays insertObject:overlay atIndex:insertionIndex];
[self updateOverlayHiddenState];
}
- (void)deactivateOverlay:(UIView *)overlay {
if (!overlay) {
return;
}
// If the overlay wasn't managed by us, don't do anything to deactivate it.
if (![self.overlays containsObject:overlay]) {
return;
}
// If @c overlay is already in the overlay view, then this call will take care of cleaning up
// @c self.overlays by way of @c noteOverlayRemoved:.
[overlay removeFromSuperview];
}
#pragma mark - Level Storage
static char kLevelKey;
- (UIWindowLevel)windowLevelForOverlay:(UIView *)overlay {
NSNumber *levelObject = objc_getAssociatedObject(self, &kLevelKey);
return [levelObject floatValue];
}
- (void)setLevel:(UIWindowLevel)level forOverlay:(UIView *)overlay {
NSNumber *levelObject = @(level);
objc_setAssociatedObject(overlay, &kLevelKey, levelObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)removeLevelForOverlay:(UIView *)overlay {
objc_setAssociatedObject(overlay, &kLevelKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end