blob: 76911cf4f57868893f2e0611f64705319c104112 [file]
/*
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 "MDCOverlayObserver.h"
#import <CoreFoundation/CoreFoundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <UIKit/UIKit.h>
#import "MDCOverlayImplementor.h"
#import "private/MDCOverlayAnimationObserver.h"
#import "private/MDCOverlayObserverOverlay.h"
#import "private/MDCOverlayObserverTransition.h"
// If this is ever required elsewhere in the code, just disable unused parameter warnings entirely
// with -Wno-unused-params.
#ifdef NS_BLOCK_ASSERTIONS
#define MDC_UNUSED_IN_RELEASE __unused
#else
#define MDC_UNUSED_IN_RELEASE
#endif
@interface MDCOverlayObserver () <MDCOverlayAnimationObserverDelegate>
/** The list of overlays currently known to this observer. */
@property(nonatomic) NSMutableDictionary *overlays;
/** The currently-pending transition. */
@property(nonatomic) MDCOverlayObserverTransition *pendingTransition;
/** The table holding the target-action mapping. */
@property(nonatomic) NSMapTable *actionTable;
/** The animation observer used to coalesce events. */
@property(nonatomic, strong) MDCOverlayAnimationObserver *observer;
@end
@implementation MDCOverlayObserver
static MDCOverlayObserver *_sOverlayObserver;
// This class must be available before the keyboard (or any other overlay contributor) has a chance
// to report overlay changes. The +load method is the only safe place early enough.
+ (void)load {
@autoreleasepool {
_sOverlayObserver = [[MDCOverlayObserver alloc] init];
}
}
+ (instancetype)observerForScreen:(MDC_UNUSED_IN_RELEASE UIScreen *)screen {
NSParameterAssert(screen == nil || screen == [UIScreen mainScreen]);
return _sOverlayObserver;
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_overlays = [NSMutableDictionary dictionary];
_observer = [[MDCOverlayAnimationObserver alloc] init];
_observer.delegate = self;
// Set up the target/action table. The keys will be weak pointers to objects, with no calls to
// hashing done only on the pointer address (as opposed to calling -hash/-isEqual:).
// The values will be strong references to objects.
NSPointerFunctionsOptions keyOptions =
(NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality);
NSPointerFunctionsOptions valueOptions =
(NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality);
_actionTable = [NSMapTable mapTableWithKeyOptions:keyOptions valueOptions:valueOptions];
// Register to hear when an overlay changes.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleOverlayChangeNotification:)
name:MDCOverlayDidChangeNotification
object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Overlays
- (MDCOverlayObserverOverlay *)overlayWithIdentifier:(NSString *)identifier {
return self.overlays[identifier];
}
- (MDCOverlayObserverOverlay *)buildOverlayWithIdentifier:(NSString *)identifier {
MDCOverlayObserverOverlay *overlay = self.overlays[identifier];
if (overlay == nil) {
overlay = [[MDCOverlayObserverOverlay alloc] init];
overlay.identifier = identifier;
self.overlays[identifier] = overlay;
}
return overlay;
}
- (void)removeOverlayWithIdentifier:(NSString *)identifier {
[self.overlays removeObjectForKey:identifier];
}
- (NSArray *)sortedOverlays {
NSArray *sortedKeys = [[self.overlays allKeys] sortedArrayUsingSelector:@selector(compare:)];
NSMutableArray *result = [NSMutableArray array];
for (NSString *key in sortedKeys) {
[result addObject:self.overlays[key]];
}
return result;
}
#pragma mark - Input Sources
- (BOOL)updateOverlay:(NSString *)identifier withFrame:(CGRect)frame {
BOOL changed = NO;
MDCOverlayObserverOverlay *existingOverlay = [self overlayWithIdentifier:identifier];
if (existingOverlay != nil) {
if (CGRectIsEmpty(frame)) {
// We're getting rid of this overlay entirely.
[self removeOverlayWithIdentifier:identifier];
changed = YES;
} else if (!CGRectEqualToRect(existingOverlay.frame, frame)) {
// We're changing to a new frame for this overlay.
existingOverlay.frame = frame;
changed = YES;
}
} else if (!CGRectIsEmpty(frame)) {
MDCOverlayObserverOverlay *overlay = [self buildOverlayWithIdentifier:identifier];
overlay.frame = frame;
changed = YES;
}
return changed;
}
- (void)handleOverlayChangeNotification:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo;
NSString *identifier = userInfo[MDCOverlayIdentifierKey];
// Don't even bother if an identifier wasn't provided.
if (identifier.length == 0) {
return;
}
NSValue *frame = userInfo[MDCOverlayFrameKey] ?
userInfo[MDCOverlayFrameKey] : [NSValue valueWithCGRect:CGRectNull];
NSNumber *duration = userInfo[MDCOverlayTransitionDurationKey];
// Update the overlay frame.
BOOL updated = [self updateOverlay:identifier withFrame:[frame CGRectValue]];
// If there was actually a change, set up a transition.
if (updated) {
if (self.pendingTransition == nil) {
self.pendingTransition = [[MDCOverlayObserverTransition alloc] init];
[self.observer messageDelegateOnNextRunloop];
}
}
// If we were given a duration, then update the animation parameters.
if (self.pendingTransition != nil && duration != nil) {
self.pendingTransition.duration = duration.doubleValue;
self.pendingTransition.customTimingFunction = userInfo[MDCOverlayTransitionTimingFunctionKey];
self.pendingTransition.animationCurve =
((NSNumber *)userInfo[MDCOverlayTransitionCurveKey]).integerValue;
// If this update requires us to run the animation immediately, go ahead and fire it off.
NSNumber *runImmediately = userInfo[MDCOverlayTransitionImmediacyKey];
if (runImmediately.boolValue) {
[self fireTransition];
}
}
}
#pragma mark - Target/Action
- (NSInvocation *)buildInvocationForTarget:(id)target action:(SEL)action {
NSMethodSignature *signature = [target methodSignatureForSelector:action];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:target];
[invocation setSelector:action];
return invocation;
}
- (void)invokeTarget:(NSInvocation *)invocation
withTransition:(MDCOverlayObserverTransition *)transition {
NSParameterAssert(invocation != nil);
NSParameterAssert(transition != nil);
[invocation setArgument:&transition atIndex:2];
[invocation invoke];
}
- (NSUInteger)indexOfInvocationForTarget:(id)target action:(SEL)action {
NSMutableArray *invocations = [self.actionTable objectForKey:target];
if (invocations == nil) {
return NSNotFound;
}
return [invocations indexOfObjectPassingTest:
^BOOL(NSInvocation *invocation, __unused NSUInteger idx, __unused BOOL *stop) {
return invocation.selector == action;
}];
}
- (void)addTarget:(id)target action:(SEL)action {
NSParameterAssert(target != nil);
NSUInteger foundIndex = [self indexOfInvocationForTarget:target action:action];
if (foundIndex != NSNotFound) {
return;
}
NSInvocation *invocation = [self buildInvocationForTarget:target action:action];
NSParameterAssert(invocation != nil);
NSMutableArray *invocations = [self.actionTable objectForKey:target];
if (invocations == nil) {
invocations = [NSMutableArray array];
[self.actionTable setObject:invocations forKey:target];
}
[invocations addObject:invocation];
if (self.overlays.count > 0) {
// If there's already a pending transition, let the runloop take care of it, otherwise create
// one and call it immediately on just the newly-added target, so that it can get its initial
// value.
if (self.pendingTransition == nil) {
MDCOverlayObserverTransition *transition = [[MDCOverlayObserverTransition alloc] init];
transition.overlays = [self sortedOverlays];
[self invokeTarget:invocation withTransition:transition];
// Run the (non-animated) transition.
[transition runAnimation];
}
}
}
- (void)removeTarget:(id)target {
NSParameterAssert(target != nil);
[self.actionTable removeObjectForKey:target];
}
- (void)removeTarget:(id)target action:(SEL)action {
NSParameterAssert(target != nil);
NSUInteger foundIndex = [self indexOfInvocationForTarget:target action:action];
if (foundIndex != NSNotFound) {
NSMutableArray *invocations = [self.actionTable objectForKey:target];
if (invocations.count == 1) {
// Clean up all the invocations if this was the only one.
[self removeTarget:target];
} else {
// Otherwise remove this single invocation.
[invocations removeObjectAtIndex:foundIndex];
}
}
}
#pragma mark - Runloop Observer
- (void)fireTransition {
if (self.pendingTransition == nil) {
return;
}
// Update the transition with the latest set of overlays.
self.pendingTransition.overlays = [self sortedOverlays];
// Call all of our targets and let them know a transition has happened.
for (id target in self.actionTable) {
NSArray *invocations = [self.actionTable objectForKey:target];
for (NSInvocation *invocation in invocations) {
[self invokeTarget:invocation withTransition:self.pendingTransition];
}
}
// Actually run the transition animation.
[self.pendingTransition runAnimation];
self.pendingTransition = nil;
}
- (void)animationObserverDidEndRunloop:(__unused MDCOverlayAnimationObserver *)observer {
[self fireTransition];
}
@end