Skip to content

Commit 70482cc

Browse files
bhamiltoncxmaterial-automation
authored andcommitted
Dragon Catalog example of new in-development Shadow component.
PiperOrigin-RevId: 363207155
1 parent 4e245ac commit 70482cc

File tree

1 file changed

+335
-0
lines changed

1 file changed

+335
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// Copyright 2021-present the Material Components for iOS authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import UIKit
16+
import MaterialComponents.MaterialShadow
17+
import MaterialComponents.MaterialContainerScheme
18+
19+
/// Typical use-case for a view with Material Shadows at a fixed elevation.
20+
final class ShadowedView: UIView {
21+
override init(frame: CGRect) {
22+
super.init(frame: frame)
23+
layer.cornerRadius = 4
24+
}
25+
26+
required init?(coder: NSCoder) {
27+
fatalError("init(coder:) is unavailable")
28+
}
29+
30+
override func layoutSubviews() {
31+
super.layoutSubviews()
32+
MDCConfigureShadow(for: self, color: MDCShadowColor(), elevation: 2)
33+
}
34+
}
35+
36+
/// Typical use-case for a shaped view with Material Shadows.
37+
final class ShapedView: UIView {
38+
let shapeLayer = CAShapeLayer()
39+
let shadow = MDCShadowForElevation(2)
40+
41+
override init(frame: CGRect) {
42+
super.init(frame: frame)
43+
layer.addSublayer(shapeLayer)
44+
}
45+
46+
required init?(coder: NSCoder) {
47+
fatalError("init(coder:) is unavailable")
48+
}
49+
50+
override var backgroundColor: UIColor? {
51+
get {
52+
guard let color = shapeLayer.fillColor else { return nil }
53+
return UIColor(cgColor: color)
54+
}
55+
set {
56+
shapeLayer.fillColor = newValue?.cgColor
57+
}
58+
}
59+
60+
override func layoutSubviews() {
61+
super.layoutSubviews()
62+
guard let path = polygonPath(bounds: self.bounds, numSides: 3, numPoints: 3) else { return }
63+
shapeLayer.path = path
64+
MDCConfigureShadow(for: self, color: MDCShadowColor(), shadow: shadow, path: path)
65+
}
66+
}
67+
68+
/// More complex use-case for a view with a custom shape which animates.
69+
final class AnimatedShapedView: UIView {
70+
let shapeLayer = CAShapeLayer()
71+
let shadow = MDCShadowForElevation(2)
72+
let firstNumSides = 3
73+
let lastNumSides = 12
74+
let animationStepDuration: CFTimeInterval = 0.6
75+
76+
override init(frame: CGRect) {
77+
super.init(frame: frame)
78+
layer.addSublayer(shapeLayer)
79+
updatePathAndAnimations()
80+
}
81+
82+
required init?(coder: NSCoder) {
83+
fatalError("init(coder:) is unavailable")
84+
}
85+
86+
override var backgroundColor: UIColor? {
87+
get {
88+
guard let color = shapeLayer.fillColor else { return nil }
89+
return UIColor(cgColor: color)
90+
}
91+
set {
92+
shapeLayer.fillColor = newValue?.cgColor
93+
}
94+
}
95+
96+
override func layoutSubviews() {
97+
super.layoutSubviews()
98+
updatePathAndAnimations()
99+
}
100+
101+
private func updatePathAndAnimations() {
102+
guard
103+
let startPath = polygonPath(
104+
bounds: bounds, numSides: firstNumSides, numPoints: lastNumSides)
105+
else { return }
106+
shapeLayer.path = startPath
107+
MDCConfigureShadow(for: self, color: MDCShadowColor(), shadow: shadow, path: startPath)
108+
109+
var polygonPaths = (firstNumSides...lastNumSides).map {
110+
polygonPath(bounds: bounds, numSides: $0, numPoints: lastNumSides)
111+
}
112+
polygonPaths.shuffle()
113+
var beginTime: CFTimeInterval = 0
114+
var pathAnimations: [CAAnimation] = []
115+
var shadowPathAnimations: [CAAnimation] = []
116+
for (i, polygonPath) in polygonPaths.enumerated() {
117+
let fromValue = i == 0 ? polygonPaths[polygonPaths.count - 1] : polygonPaths[i - 1]
118+
let toValue = polygonPath
119+
let pathAnimation = CABasicAnimation(keyPath: "path")
120+
pathAnimation.fromValue = fromValue
121+
pathAnimation.toValue = toValue
122+
pathAnimation.beginTime = beginTime
123+
pathAnimation.duration = animationStepDuration
124+
pathAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
125+
pathAnimations.append(pathAnimation)
126+
let shadowPathAnimation = pathAnimation.copy() as! CABasicAnimation
127+
shadowPathAnimation.keyPath = "shadowPath"
128+
shadowPathAnimations.append(shadowPathAnimation)
129+
beginTime += animationStepDuration
130+
}
131+
132+
let pathAnimationGroup = CAAnimationGroup()
133+
pathAnimationGroup.animations = pathAnimations
134+
pathAnimationGroup.duration = animationStepDuration * CFTimeInterval(pathAnimations.count)
135+
pathAnimationGroup.repeatCount = .greatestFiniteMagnitude
136+
shapeLayer.add(pathAnimationGroup, forKey: "path")
137+
138+
let shadowPathAnimationGroup = CAAnimationGroup()
139+
shadowPathAnimationGroup.animations = shadowPathAnimations
140+
shadowPathAnimationGroup.duration =
141+
animationStepDuration * CFTimeInterval(pathAnimations.count)
142+
shadowPathAnimationGroup.repeatCount = .greatestFiniteMagnitude
143+
layer.add(shadowPathAnimationGroup, forKey: "shadowPath")
144+
}
145+
}
146+
147+
/// Returns a regular polygon within `bounds` having `numSides` sides.
148+
///
149+
/// If `numSides` < 3 or `numPoints` < `numSides`, returns nil.
150+
///
151+
/// If `numPoints` > `numSides`, the polygon will invisibly repeat
152+
/// points along the vertices to ensure it has `numPoints` points.
153+
/// This allows smooth animations between multiple polygons with
154+
/// differing number of sides.
155+
func polygonPath(bounds: CGRect, numSides: Int, numPoints: Int) -> CGPath? {
156+
guard numSides > 2 else { return nil }
157+
guard numPoints >= numSides else { return nil }
158+
let xRadius = bounds.width / 2
159+
let yRadius = bounds.height / 2
160+
let path = UIBezierPath()
161+
for pointIdx in 0..<numPoints {
162+
// Map pointIdx to a float in the range [0, 1].
163+
let pointProgress = CGFloat(pointIdx) / CGFloat(numPoints - 1)
164+
// Choose the closest vertex of the polygon.
165+
let sideIdx = round(pointProgress * CGFloat(numSides - 1))
166+
let theta = CGFloat(2) * CGFloat.pi / CGFloat(numSides) * sideIdx
167+
let x = bounds.midX + xRadius * cos(theta)
168+
let y = bounds.midY + yRadius * sin(theta)
169+
let point = CGPoint(x: x, y: y)
170+
if pointIdx == 0 {
171+
path.move(to: point)
172+
} else if pointIdx < numPoints {
173+
path.addLine(to: point)
174+
}
175+
}
176+
path.close()
177+
return path.cgPath
178+
}
179+
180+
@available(iOS 12.0, *)
181+
final class ShadowTypicalUseExample: UIViewController {
182+
enum ExampleType: Int {
183+
case typical, shaped, animated
184+
}
185+
186+
private typealias Example = (label: String, type: ExampleType)
187+
private let examples: [Example] = [
188+
("Typical", .typical),
189+
("Shaped", .shaped),
190+
("Animated", .animated),
191+
]
192+
193+
private let containerScheme = MDCContainerScheme()
194+
195+
private lazy var typicalView: UIView = {
196+
let result = ShadowedView()
197+
result.translatesAutoresizingMaskIntoConstraints = false
198+
result.backgroundColor = containerScheme.colorScheme.primaryColor
199+
return result
200+
}()
201+
202+
private lazy var shapedView: UIView = {
203+
let result = ShapedView()
204+
result.translatesAutoresizingMaskIntoConstraints = false
205+
result.backgroundColor = containerScheme.colorScheme.primaryColor
206+
return result
207+
}()
208+
209+
private lazy var animatedView: UIView = {
210+
let result = AnimatedShapedView()
211+
result.translatesAutoresizingMaskIntoConstraints = false
212+
result.backgroundColor = containerScheme.colorScheme.primaryColor
213+
return result
214+
}()
215+
216+
private lazy var typeControl: UISegmentedControl = {
217+
let result = UISegmentedControl(items: examples.map { $0.label })
218+
result.backgroundColor = containerScheme.colorScheme.primaryColor
219+
result.translatesAutoresizingMaskIntoConstraints = false
220+
if #available(iOS 13, *) {
221+
result.selectedSegmentTintColor = containerScheme.colorScheme.surfaceColor
222+
}
223+
result.setTitleTextAttributes(
224+
[
225+
.foregroundColor: containerScheme.colorScheme.onPrimaryColor
226+
],
227+
for: .normal)
228+
result.setTitleTextAttributes(
229+
[
230+
.foregroundColor: containerScheme.colorScheme.onSurfaceColor
231+
],
232+
for: .selected)
233+
result.selectedSegmentIndex = 0
234+
return result
235+
}()
236+
237+
private let topSpacerLayoutGuide = UILayoutGuide()
238+
private let contentLayoutGuide = UILayoutGuide()
239+
private let bottomSpacerLayoutGuide = UILayoutGuide()
240+
241+
override func viewDidLoad() {
242+
super.viewDidLoad()
243+
view.backgroundColor = containerScheme.colorScheme.backgroundColor
244+
view.addSubview(typicalView)
245+
view.addSubview(shapedView)
246+
view.addSubview(animatedView)
247+
view.addLayoutGuide(topSpacerLayoutGuide)
248+
view.addLayoutGuide(contentLayoutGuide)
249+
view.addLayoutGuide(bottomSpacerLayoutGuide)
250+
view.addSubview(typeControl)
251+
updateExampleType()
252+
typeControl.addTarget(self, action: #selector(updateExampleType), for: .valueChanged)
253+
let safeAreaLayoutGuide = view.safeAreaLayoutGuide
254+
NSLayoutConstraint.activate([
255+
// Top spacer.
256+
topSpacerLayoutGuide.topAnchor.constraint(
257+
equalToSystemSpacingBelow: safeAreaLayoutGuide.topAnchor, multiplier: 1),
258+
topSpacerLayoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
259+
topSpacerLayoutGuide.bottomAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
260+
261+
// Content.
262+
contentLayoutGuide.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
263+
contentLayoutGuide.widthAnchor.constraint(equalTo: contentLayoutGuide.heightAnchor),
264+
contentLayoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: 100),
265+
contentLayoutGuide.widthAnchor.constraint(
266+
lessThanOrEqualTo: safeAreaLayoutGuide.widthAnchor, constant: -40),
267+
268+
typicalView.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor),
269+
typicalView.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor),
270+
typicalView.widthAnchor.constraint(equalTo: contentLayoutGuide.widthAnchor),
271+
typicalView.heightAnchor.constraint(equalTo: contentLayoutGuide.heightAnchor),
272+
273+
shapedView.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor),
274+
shapedView.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor),
275+
shapedView.widthAnchor.constraint(equalTo: contentLayoutGuide.widthAnchor),
276+
shapedView.heightAnchor.constraint(equalTo: contentLayoutGuide.heightAnchor),
277+
278+
animatedView.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor),
279+
animatedView.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor),
280+
animatedView.widthAnchor.constraint(equalTo: contentLayoutGuide.widthAnchor),
281+
animatedView.heightAnchor.constraint(equalTo: contentLayoutGuide.heightAnchor),
282+
283+
// Bottom spacer.
284+
bottomSpacerLayoutGuide.topAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
285+
286+
// Ensure the top spacer and bottom spacer both have the same height (to center the content).
287+
bottomSpacerLayoutGuide.heightAnchor.constraint(equalTo: topSpacerLayoutGuide.heightAnchor),
288+
289+
// Segmented type control.
290+
typeControl.topAnchor.constraint(
291+
equalToSystemSpacingBelow: bottomSpacerLayoutGuide.bottomAnchor,
292+
multiplier: 1),
293+
typeControl.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
294+
typeControl.widthAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.widthAnchor),
295+
296+
safeAreaLayoutGuide.bottomAnchor.constraint(
297+
equalToSystemSpacingBelow: typeControl.bottomAnchor, multiplier: 1),
298+
])
299+
}
300+
301+
@objc func updateExampleType() {
302+
guard let exampleType = ExampleType(rawValue: typeControl.selectedSegmentIndex) else { return }
303+
switch exampleType {
304+
case .typical:
305+
typicalView.isHidden = false
306+
shapedView.isHidden = true
307+
animatedView.isHidden = true
308+
case .shaped:
309+
typicalView.isHidden = true
310+
shapedView.isHidden = false
311+
animatedView.isHidden = true
312+
case .animated:
313+
typicalView.isHidden = true
314+
shapedView.isHidden = true
315+
animatedView.isHidden = false
316+
}
317+
}
318+
}
319+
320+
// MARK: Catalog by Convensions
321+
@available(iOS 12.0, *)
322+
extension ShadowTypicalUseExample {
323+
324+
@objc class func catalogMetadata() -> [String: Any] {
325+
return [
326+
"breadcrumbs": ["Shadow", "New Shadow"],
327+
"primaryDemo": true,
328+
"presentable": false,
329+
]
330+
}
331+
332+
@objc class func minimumOSVersion() -> OperatingSystemVersion {
333+
return OperatingSystemVersion(majorVersion: 12, minorVersion: 0, patchVersion: 0)
334+
}
335+
}

0 commit comments

Comments
 (0)