// Copyright 2021-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 UIKit import MaterialComponents.MaterialShadow import MaterialComponents.MaterialContainerScheme /// Typical use-case for a view with Material Shadows at a fixed elevation. final class ShadowedView: UIView { override init(frame: CGRect) { super.init(frame: frame) layer.cornerRadius = 4 } required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") } override func layoutSubviews() { super.layoutSubviews() MDCConfigureShadow( for: self, shadow: MDCShadowsCollectionDefault().shadow(forElevation: 1), color: MDCShadowColor()) } } /// Typical use-case for a shaped view with Material Shadows. final class ShapedView: UIView { let shapeLayer = CAShapeLayer() override init(frame: CGRect) { super.init(frame: frame) layer.addSublayer(shapeLayer) } required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") } override var backgroundColor: UIColor? { get { guard let color = shapeLayer.fillColor else { return nil } return UIColor(cgColor: color) } set { shapeLayer.fillColor = newValue?.cgColor } } override func layoutSubviews() { super.layoutSubviews() guard let path = polygonPath(bounds: self.bounds, numSides: 3, numPoints: 3) else { return } shapeLayer.path = path MDCConfigureShadow( for: self, shadow: MDCShadowsCollectionDefault().shadow(forElevation: 1), color: MDCShadowColor(), path: path) } } /// More complex use-case for a view with a custom shape which animates. final class AnimatedShapedView: UIView { let shapeLayer = CAShapeLayer() let firstNumSides = 3 let lastNumSides = 12 let animationStepDuration: CFTimeInterval = 0.6 override init(frame: CGRect) { super.init(frame: frame) layer.addSublayer(shapeLayer) updatePathAndAnimations() } required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") } override var backgroundColor: UIColor? { get { guard let color = shapeLayer.fillColor else { return nil } return UIColor(cgColor: color) } set { shapeLayer.fillColor = newValue?.cgColor } } override func layoutSubviews() { super.layoutSubviews() updatePathAndAnimations() } private func updatePathAndAnimations() { guard let startPath = polygonPath( bounds: bounds, numSides: firstNumSides, numPoints: lastNumSides) else { return } shapeLayer.path = startPath MDCConfigureShadow( for: self, shadow: MDCShadowsCollectionDefault().shadow(forElevation: 1), color: MDCShadowColor(), path: startPath) var polygonPaths = (firstNumSides...lastNumSides).map { polygonPath(bounds: bounds, numSides: $0, numPoints: lastNumSides) } polygonPaths.shuffle() var beginTime: CFTimeInterval = 0 var pathAnimations: [CAAnimation] = [] var shadowPathAnimations: [CAAnimation] = [] for (i, polygonPath) in polygonPaths.enumerated() { let fromValue = i == 0 ? polygonPaths[polygonPaths.count - 1] : polygonPaths[i - 1] let toValue = polygonPath let pathAnimation = CABasicAnimation(keyPath: "path") pathAnimation.fromValue = fromValue pathAnimation.toValue = toValue pathAnimation.beginTime = beginTime pathAnimation.duration = animationStepDuration pathAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) pathAnimations.append(pathAnimation) let shadowPathAnimation = pathAnimation.copy() as! CABasicAnimation shadowPathAnimation.keyPath = "shadowPath" shadowPathAnimations.append(shadowPathAnimation) beginTime += animationStepDuration } let pathAnimationGroup = CAAnimationGroup() pathAnimationGroup.animations = pathAnimations pathAnimationGroup.duration = animationStepDuration * CFTimeInterval(pathAnimations.count) pathAnimationGroup.repeatCount = .greatestFiniteMagnitude shapeLayer.add(pathAnimationGroup, forKey: "path") let shadowPathAnimationGroup = CAAnimationGroup() shadowPathAnimationGroup.animations = shadowPathAnimations shadowPathAnimationGroup.duration = animationStepDuration * CFTimeInterval(pathAnimations.count) shadowPathAnimationGroup.repeatCount = .greatestFiniteMagnitude layer.add(shadowPathAnimationGroup, forKey: "shadowPath") } } /// Returns a regular polygon within `bounds` having `numSides` sides. /// /// If `numSides` < 3 or `numPoints` < `numSides`, returns nil. /// /// If `numPoints` > `numSides`, the polygon will invisibly repeat /// points along the vertices to ensure it has `numPoints` points. /// This allows smooth animations between multiple polygons with /// differing number of sides. func polygonPath(bounds: CGRect, numSides: Int, numPoints: Int) -> CGPath? { guard numSides > 2 else { return nil } guard numPoints >= numSides else { return nil } let xRadius = bounds.width / 2 let yRadius = bounds.height / 2 let path = UIBezierPath() for pointIdx in 0.. [String: Any] { return [ "breadcrumbs": ["Shadow", "New Shadow"], "primaryDemo": true, "presentable": false, ] } @objc class func minimumOSVersion() -> OperatingSystemVersion { return OperatingSystemVersion(majorVersion: 12, minorVersion: 0, patchVersion: 0) } }