blob: 55e3b889f5960e25984f0b2d3fdfd6bcdcf412b4 [file] [edit]
// Copyright 2015-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 CatalogByConvention
import MaterialCatalog
import MaterialComponents.MaterialFlexibleHeader
import MaterialComponents.MaterialFlexibleHeader_ColorThemer
import MaterialComponents.MaterialIcons_ic_arrow_back
import MaterialComponents.MaterialInk
import MaterialComponents.MaterialLibraryInfo
import MaterialComponents.MaterialShadowElevations
import MaterialComponents.MaterialShadowLayer
import MaterialComponents.MaterialThemes
import MaterialComponents.MaterialTypography
import UIKit
class MDCCatalogComponentsController: UICollectionViewController, UICollectionViewDelegateFlowLayout, MDCInkTouchControllerDelegate {
fileprivate struct Constants {
static let headerScrollThreshold: CGFloat = 30
static let inset: CGFloat = 16
static let menuTopVerticalSpacing: CGFloat = 38
static let logoWidthHeight: CGFloat = 30
static let menuButtonWidthHeight: CGFloat = 24
static let spacing: CGFloat = 1
}
fileprivate lazy var headerViewController = MDCFlexibleHeaderViewController()
private lazy var inkController: MDCInkTouchController = {
let controller = MDCInkTouchController(view: self.collectionView!)
controller.delaysInkSpread = true
controller.delegate = self
return controller
}()
private lazy var logo: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
private lazy var menuButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
let dotsImage = MDCIcons.imageFor_ic_more_horiz()?.withRenderingMode(.alwaysTemplate)
button.setImage(dotsImage, for: .normal)
button.adjustsImageWhenHighlighted = false
button.accessibilityLabel = "Menu"
button.accessibilityHint = "Opens catalog configuration options."
return button
}()
private let node: CBCNode
private lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.adjustsFontSizeToFitWidth = true
return titleLabel
}()
private var logoLeftPaddingConstraint: NSLayoutConstraint?
private var menuButtonRightPaddingConstraint: NSLayoutConstraint?
private var menuTopPaddingConstraint: NSLayoutConstraint?
init(collectionViewLayout ignoredLayout: UICollectionViewLayout, node: CBCNode) {
self.node = node
let layout = UICollectionViewFlowLayout()
let sectionInset: CGFloat = Constants.spacing
layout.sectionInset = UIEdgeInsets(top: sectionInset,
left: sectionInset,
bottom: sectionInset,
right: sectionInset)
layout.minimumInteritemSpacing = Constants.spacing
layout.minimumLineSpacing = Constants.spacing
super.init(collectionViewLayout: layout)
title = "Material Components for iOS"
addChild(headerViewController)
headerViewController.isTopLayoutGuideAdjustmentEnabled = true
headerViewController.inferTopSafeAreaInsetFromViewController = true
headerViewController.headerView.minMaxHeightIncludesSafeArea = false
headerViewController.headerView.maximumHeight = 128
headerViewController.headerView.minimumHeight = 56
collectionView?.register(MDCCatalogCollectionViewCell.self,
forCellWithReuseIdentifier: "MDCCatalogCollectionViewCell")
collectionView?.backgroundColor = AppTheme.containerScheme.colorScheme.backgroundColor
MDCIcons.ic_arrow_backUseNewStyle(true)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.themeDidChange),
name: AppTheme.didChangeGlobalThemeNotificationName,
object: nil)
}
@objc func themeDidChange(notification: NSNotification) {
let colorScheme = AppTheme.containerScheme.colorScheme
MDCFlexibleHeaderColorThemer.applySemanticColorScheme(colorScheme,
to: headerViewController.headerView)
setNeedsStatusBarAppearanceUpdate()
titleLabel.textColor = colorScheme.onPrimaryColor
menuButton.tintColor = colorScheme.onPrimaryColor
collectionView?.collectionViewLayout.invalidateLayout()
collectionView?.reloadData()
}
convenience init(node: CBCNode) {
self.init(collectionViewLayout: UICollectionViewLayout(), node: node)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
inkController.addInkView()
let containerView = UIView(frame: headerViewController.headerView.bounds)
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
titleLabel.text = title!
titleLabel.textColor = AppTheme.containerScheme.colorScheme.onPrimaryColor
titleLabel.textAlignment = .center
titleLabel.font = AppTheme.containerScheme.typographyScheme.headline6
titleLabel.sizeToFit()
let titleInsets = UIEdgeInsets(top: 0,
left: Constants.inset,
bottom: Constants.inset,
right: Constants.inset)
let titleSize = titleLabel.sizeThatFits(containerView.bounds.size)
containerView.addSubview(titleLabel)
headerViewController.headerView.addSubview(containerView)
headerViewController.headerView.forwardTouchEvents(for: containerView)
containerView.addSubview(logo)
let colorScheme = AppTheme.containerScheme.colorScheme
let image = MDCDrawImage(CGRect(x:0,
y:0,
width: Constants.logoWidthHeight,
height: Constants.logoWidthHeight),
{ MDCCatalogDrawMDCLogoLight($0, $1) },
colorScheme)
logo.image = image
menuButton.addTarget(self.navigationController,
action: #selector(navigationController?.presentMenu),
for: .touchUpInside)
menuButton.tintColor = colorScheme.onPrimaryColor
containerView.addSubview(menuButton)
setupFlexibleHeaderContentConstraints()
constrainLabel(label: titleLabel,
containerView: containerView,
insets: titleInsets,
height: titleSize.height)
MDCFlexibleHeaderColorThemer.applySemanticColorScheme(colorScheme,
to: headerViewController.headerView)
headerViewController.headerView.trackingScrollView = collectionView
headerViewController.headerView.setShadowLayer(MDCShadowLayer()) { (layer, intensity) in
let shadowLayer = layer as? MDCShadowLayer
CATransaction.begin()
CATransaction.setDisableActions(true)
shadowLayer!.elevation = ShadowElevation(intensity * ShadowElevation.appBar.rawValue)
CATransaction.commit()
}
view.addSubview(headerViewController.view)
headerViewController.didMove(toParent: self)
collectionView?.accessibilityIdentifier = "collectionView"
if #available(iOS 11.0, *) {
collectionView?.contentInsetAdjustmentBehavior = .always
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView?.collectionViewLayout.invalidateLayout()
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func willAnimateRotation(
to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
collectionView?.collectionViewLayout.invalidateLayout()
}
override var childForStatusBarStyle: UIViewController? {
return headerViewController
}
override var childForStatusBarHidden: UIViewController? {
return headerViewController
}
@available(iOS 11, *)
override func viewSafeAreaInsetsDidChange() {
// Re-constraint the title label to account for changes in safeAreaInsets's left and right.
logoLeftPaddingConstraint?.constant = Constants.inset + view.safeAreaInsets.left
menuButtonRightPaddingConstraint?.constant = -1 * (Constants.inset + view.safeAreaInsets.right)
menuTopPaddingConstraint?.constant = Constants.inset + view.safeAreaInsets.top
}
func setupFlexibleHeaderContentConstraints() {
logoLeftPaddingConstraint = NSLayoutConstraint(item: logo,
attribute: .leading,
relatedBy: .equal,
toItem: logo.superview,
attribute: .leading,
multiplier: 1,
constant: Constants.inset)
logoLeftPaddingConstraint?.isActive = true
menuButtonRightPaddingConstraint = NSLayoutConstraint(item: menuButton,
attribute: .trailing,
relatedBy: .equal,
toItem: menuButton.superview,
attribute: .trailing,
multiplier: 1,
constant: -1 * Constants.inset)
menuButtonRightPaddingConstraint?.isActive = true
menuTopPaddingConstraint = NSLayoutConstraint(item: menuButton,
attribute: .top,
relatedBy: .equal,
toItem: menuButton.superview,
attribute: .top,
multiplier: 1,
constant: Constants.menuTopVerticalSpacing)
menuTopPaddingConstraint?.isActive = true
NSLayoutConstraint(item: logo,
attribute: .centerY,
relatedBy: .equal,
toItem: menuButton,
attribute: .centerY,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(item: logo,
attribute: .width,
relatedBy: .equal,
toItem: logo,
attribute: .height,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(item: logo,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: Constants.logoWidthHeight).isActive = true
NSLayoutConstraint(item: menuButton,
attribute: .width,
relatedBy: .equal,
toItem: menuButton,
attribute: .height,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(item: menuButton,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: Constants.menuButtonWidthHeight).isActive = true
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return node.children.count
}
func inkViewForView(_ view: UIView) -> MDCInkView {
let foundInkView = MDCInkView.injectedInkView(for: view)
foundInkView.inkStyle = .bounded
foundInkView.inkColor = UIColor(white:0.957, alpha: 0.2)
return foundInkView
}
// MARK: MDCInkTouchControllerDelegate
func inkTouchController(_ inkTouchController: MDCInkTouchController,
shouldProcessInkTouchesAtTouchLocation location: CGPoint) -> Bool {
return self.collectionView!.indexPathForItem(at: location) != nil
}
func inkTouchController(_ inkTouchController: MDCInkTouchController,
inkViewAtTouchLocation location: CGPoint) -> MDCInkView? {
if let indexPath = self.collectionView!.indexPathForItem(at: location) {
let cell = self.collectionView!.cellForItem(at: indexPath)
return inkViewForView(cell!)
}
return MDCInkView()
}
// MARK: UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell =
collectionView.dequeueReusableCell(withReuseIdentifier: "MDCCatalogCollectionViewCell",
for: indexPath)
cell.backgroundColor = AppTheme.containerScheme.colorScheme.backgroundColor
let componentName = node.children[indexPath.row].title
if let catalogCell = cell as? MDCCatalogCollectionViewCell {
catalogCell.populateView(componentName)
}
// Ensure that ink animations aren't recycled.
MDCInkView.injectedInkView(for: view).cancelAllAnimations(animated: false)
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let dividerWidth: CGFloat = 1
var safeInsets: CGFloat = 0
if #available(iOS 11, *) {
safeInsets = view.safeAreaInsets.left + view.safeAreaInsets.right
}
var cellWidthHeight: CGFloat
// iPhones have 2 columns in portrait and 3 in landscape
if UI_USER_INTERFACE_IDIOM() == .phone {
cellWidthHeight = (view.frame.size.width - 3 * dividerWidth - safeInsets) / 2
if view.frame.size.width > view.frame.size.height {
cellWidthHeight = (view.frame.size.width - 4 * dividerWidth - safeInsets) / 3
}
} else {
// iPads have 4 columns
cellWidthHeight = (view.frame.size.width - 5 * dividerWidth - safeInsets) / 4
}
return CGSize(width: cellWidthHeight, height: cellWidthHeight)
}
override func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
let node = self.node.children[indexPath.row]
var vc: UIViewController
if node.isExample() {
vc = node.createExampleViewController()
} else {
vc = MDCNodeListViewController(node: node)
}
self.navigationController?.setMenuBarButton(for: vc)
self.navigationController?.pushViewController(vc, animated: true)
}
// MARK: Private
func constrainLabel(label: UILabel,
containerView: UIView,
insets: UIEdgeInsets,
height: CGFloat) {
NSLayoutConstraint(item: label,
attribute: .leading,
relatedBy: .equal,
toItem: logo,
attribute: .trailing,
multiplier: 1.0,
constant: insets.left).isActive = true
NSLayoutConstraint(item: label,
attribute: .trailing,
relatedBy: .equal,
toItem: menuButton,
attribute: .leading,
multiplier: 1.0,
constant: -insets.right).isActive = true
NSLayoutConstraint(item: label,
attribute: .bottom,
relatedBy: .equal,
toItem: containerView,
attribute: .bottom,
multiplier: 1.0,
constant: -insets.bottom).isActive = true
NSLayoutConstraint(item: label,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: height).isActive = true
}
}
// UIScrollViewDelegate
extension MDCCatalogComponentsController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == headerViewController.headerView.trackingScrollView {
self.headerViewController.headerView.trackingScrollDidScroll()
}
}
override func scrollViewDidEndDragging(
_ scrollView: UIScrollView,
willDecelerate decelerate: Bool) {
let headerView = headerViewController.headerView
if scrollView == headerView.trackingScrollView {
headerView.trackingScrollDidEndDraggingWillDecelerate(decelerate)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == headerViewController.headerView.trackingScrollView {
self.headerViewController.headerView.trackingScrollDidEndDecelerating()
}
}
override func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let headerView = headerViewController.headerView
if scrollView == headerView.trackingScrollView {
headerView.trackingScrollWillEndDragging(
withVelocity: velocity,
targetContentOffset: targetContentOffset)
}
}
}