featherless 37e92a1acb
[ShadowLayer] Fix bug where headless layers would not piggyback shadowPath changes. (#8666)
Definitions:

- Headless layer: a CALayer without a delegate.
- Mounted layer: a CALayer that has been flushed to the render server either via a runloop pump or via `[CATransaction flush]`.

Context:

MDCShadowLayer includes some logic for animating shadowPath changes alongside corresponding bounds animations. The intent is that, if `shadowPath` is changed then it attempts to inherit any animation traits from existing bounds-related animations, if they exist. This behavior was added in https://github.com/material-components/material-components-ios/pull/2523. This behavior enables changes to shadowPath (which are not animatable by UIView-based animations) to inherit existing UIView-based animations.

Prior to this change, only bounds.size would be inspected for this piggy-backing behavior. While this works for UIView-based animations, which decompose `.frame` and `.bounds` animations into `bounds.size` and `bounds.origin`, this does not work for headless layers which directly animate the `bounds` property. E.g. the following snippet would piggy back as expected:

```objc
[UIView animateWithDuration:0.1 animations:^{
  someView.frame = CGRectMake(0, 0, 100, 50);
  someView.layer.shadowPath = [UIBezierPath bezierPathWithRect:someView.bounds].CGPath;
}];
```

But the following would not:

```objc
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
shadowLayer.bounds = CGRectMake(0, 0, 100, 50);
shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:shadowLayer.bounds].CGPath;
[CATransaction commit];
```

After this change, MDCShadowLayer will check for animations to `bounds` as well, increasing the likelihood that the shadowPath is able to piggy-back existing animations.

This change is part of addressing https://github.com/material-components/material-components-ios/issues/8644
2019-10-28 15:00:11 -04:00

97 lines
3.3 KiB
Objective-C

// 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 <XCTest/XCTest.h>
#import "MaterialShadowLayer.h"
@interface ShadowLayerTestsView : UIView
@end
@implementation ShadowLayerTestsView
+ (Class)layerClass {
return [MDCShadowLayer class];
}
@end
@interface ShadowLayerTests : XCTestCase
@end
@implementation ShadowLayerTests
- (void)testDefaultValues {
// Given
MDCShadowLayer *shadowLayer = [[MDCShadowLayer alloc] init];
// Then
XCTAssertEqualWithAccuracy(shadowLayer.elevation, 0, 0.0001);
XCTAssertTrue(shadowLayer.isShadowMaskEnabled);
}
- (void)testShadowLayerBackedViewShadowPathAnimationPiggyBacksUIKitFrameAnimation {
// Given
UIView *someView = [[ShadowLayerTestsView alloc] init];
// When
[UIView animateWithDuration:0.1
animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
someView.frame = CGRectMake(0, 0, 100, 50);
someView.layer.shadowPath =
[UIBezierPath bezierPathWithRect:someView.bounds].CGPath;
[CATransaction commit];
}];
// Then
XCTAssertNotNil([someView.layer animationForKey:@"position"]);
XCTAssertNotNil([someView.layer animationForKey:@"bounds.origin"]);
XCTAssertNotNil([someView.layer animationForKey:@"bounds.size"]);
CFTimeInterval boundsDuration = [someView.layer animationForKey:@"bounds.origin"].duration;
for (CALayer *sublayer in someView.layer.sublayers) {
CAAnimation *animation = [sublayer animationForKey:@"shadowPath"];
XCTAssertNotNil(animation);
XCTAssertEqualWithAccuracy(animation.duration, boundsDuration, 0.001);
}
}
- (void)testShadowLayerBackedViewShadowPathAnimationPiggyBacksUIKitBoundsAnimation {
// Given
UIView *someView = [[ShadowLayerTestsView alloc] init];
// When
[UIView animateWithDuration:0.1
animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
someView.bounds = CGRectMake(0, 0, 100, 50);
someView.layer.shadowPath =
[UIBezierPath bezierPathWithRect:someView.bounds].CGPath;
[CATransaction commit];
}];
// Then
XCTAssertNotNil([someView.layer animationForKey:@"bounds.origin"]);
XCTAssertNotNil([someView.layer animationForKey:@"bounds.size"]);
CFTimeInterval boundsDuration = [someView.layer animationForKey:@"bounds.origin"].duration;
for (CALayer *sublayer in someView.layer.sublayers) {
CAAnimation *animation = [sublayer animationForKey:@"shadowPath"];
XCTAssertNotNil(animation);
XCTAssertEqualWithAccuracy(animation.duration, boundsDuration, 0.001);
}
}
@end