blob: 0232081893b1c7878b7cf50479bc196a0a5cc64a [file] [log] [blame] [edit]
// 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 "MDCCollectionViewController.h"
#import "private/MDCCollectionInfoBarView.h"
#import "private/MDCCollectionStringResources.h"
#import "private/MDCCollectionViewEditor.h"
#import "private/MDCCollectionViewStyler.h"
#import "MaterialCollectionCells.h"
#import "MDCCollectionViewEditing.h"
#import "MDCCollectionViewEditingDelegate.h"
#import "MDCCollectionViewFlowLayout.h"
#import "MDCCollectionInfoBarViewDelegate.h"
#include <tgmath.h>
NSString *const MDCCollectionInfoBarKindHeader = @"MDCCollectionInfoBarKindHeader";
NSString *const MDCCollectionInfoBarKindFooter = @"MDCCollectionInfoBarKindFooter";
@interface MDCCollectionViewController () <MDCCollectionInfoBarViewDelegate,
MDCInkTouchControllerDelegate,
MDCRippleTouchControllerDelegate>
@property(nonatomic, assign) BOOL currentlyActiveInk;
@end
@implementation MDCCollectionViewController {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
MDCInkTouchController *_inkTouchController;
#pragma clang diagnostic pop
MDCRippleTouchController *_rippleTouchController;
MDCCollectionInfoBarView *_headerInfoBar;
MDCCollectionInfoBarView *_footerInfoBar;
BOOL _headerInfoBarDismissed;
CGPoint _inkTouchLocation;
}
@synthesize collectionViewLayout = _collectionViewLayout;
- (instancetype)init {
MDCCollectionViewFlowLayout *defaultLayout = [[MDCCollectionViewFlowLayout alloc] init];
return [self initWithCollectionViewLayout:defaultLayout];
}
- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout {
self = [super initWithCollectionViewLayout:layout];
if (self) {
_collectionViewLayout = layout;
}
return self;
}
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil
bundle:(nullable NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self != nil) {
// TODO(#): Why is this nil, the decoder should have created it
if (!_collectionViewLayout) {
_collectionViewLayout = [[MDCCollectionViewFlowLayout alloc] init];
}
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self != nil) {
// TODO(#): Why is this nil, the decoder should have created it
if (!_collectionViewLayout) {
_collectionViewLayout = [[MDCCollectionViewFlowLayout alloc] init];
}
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
if (@available(iOS 11.0, *)) {
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
[self.collectionView setCollectionViewLayout:self.collectionViewLayout];
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.alwaysBounceVertical = YES;
_styler = [[MDCCollectionViewStyler alloc] initWithCollectionView:self.collectionView];
_styler.delegate = self;
_editor = [[MDCCollectionViewEditor alloc] initWithCollectionView:self.collectionView];
_editor.delegate = self;
// Set up ink touch controller.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_inkTouchController = [[MDCInkTouchController alloc] initWithView:self.collectionView];
#pragma clang diagnostic pop
_inkTouchController.delegate = self;
_rippleTouchController = [[MDCRippleTouchController alloc] initWithView:self.collectionView
deferred:YES];
_rippleTouchController.delegate = self;
// Register our supplementary header and footer
NSString *classIdentifier = NSStringFromClass([MDCCollectionInfoBarView class]);
NSString *headerKind = MDCCollectionInfoBarKindHeader;
NSString *footerKind = MDCCollectionInfoBarKindFooter;
[self.collectionView registerClass:[MDCCollectionInfoBarView class]
forSupplementaryViewOfKind:headerKind
withReuseIdentifier:classIdentifier];
[self.collectionView registerClass:[MDCCollectionInfoBarView class]
forSupplementaryViewOfKind:footerKind
withReuseIdentifier:classIdentifier];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// Fixes an iOS 11 bug where supplementary views would be given a zPosition of 1, meaning the
// scroll view indicator (with a zPosition of 0) would be placed behind supplementary views.
// We know that iOS keeps the scroll indicator as the top-most view in the hierarchy as a subview,
// so we grab it and give it a better zPosition ourselves.
if (@available(iOS 11.0, *)) {
UIView *maybeScrollViewIndicator = self.collectionView.subviews.lastObject;
if ([maybeScrollViewIndicator isKindOfClass:[UIImageView class]]) {
maybeScrollViewIndicator.layer.zPosition = 2;
}
}
}
- (void)setCollectionView:(__kindof UICollectionView *)collectionView {
[super setCollectionView:collectionView];
// Reset editor and ink to provided collection view.
_editor = [[MDCCollectionViewEditor alloc] initWithCollectionView:collectionView];
_editor.delegate = self;
if (self.enableRippleBehavior) {
_rippleTouchController = [[MDCRippleTouchController alloc] initWithView:collectionView
deferred:YES];
_rippleTouchController.delegate = self;
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_inkTouchController = [[MDCInkTouchController alloc] initWithView:collectionView];
#pragma clang diagnostic pop
_inkTouchController.delegate = self;
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self.collectionViewLayout invalidateLayout];
}
#pragma mark - <MDCCollectionInfoBarViewDelegate>
- (void)updateControllerWithInfoBar:(MDCCollectionInfoBarView *)infoBar {
// Updates info bar styling for header/footer.
if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindHeader]) {
_headerInfoBar = infoBar;
_headerInfoBar.message = MDCCollectionStringResources(infoBarGestureHintString);
_headerInfoBar.style = MDCCollectionInfoBarViewStyleHUD;
[self updateHeaderInfoBarIfNecessary];
} else if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
_footerInfoBar = infoBar;
_footerInfoBar.message = MDCCollectionStringResources(deleteButtonString);
_footerInfoBar.style = MDCCollectionInfoBarViewStyleActionable;
[self updateFooterInfoBarIfNecessary];
}
}
- (void)didTapInfoBar:(MDCCollectionInfoBarView *)infoBar {
if ([infoBar isEqual:_footerInfoBar]) {
[self deleteIndexPaths:self.collectionView.indexPathsForSelectedItems];
}
}
- (void)infoBar:(MDCCollectionInfoBarView *)infoBar
willShowAnimated:(__unused BOOL)animated
willAutoDismiss:(__unused BOOL)willAutoDismiss {
if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
[self updateContentWithBottomInset:MDCCollectionInfoBarFooterHeight];
}
}
- (void)infoBar:(MDCCollectionInfoBarView *)infoBar
willDismissAnimated:(__unused BOOL)animated
willAutoDismiss:(BOOL)willAutoDismiss {
if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindHeader]) {
_headerInfoBarDismissed = willAutoDismiss;
} else {
[self updateContentWithBottomInset:-MDCCollectionInfoBarFooterHeight];
}
}
#pragma mark - <MDCCollectionViewStylingDelegate>
- (MDCCollectionViewCellStyle)collectionView:(__unused UICollectionView *)collectionView
cellStyleForSection:(__unused NSInteger)section {
return _styler.cellStyle;
}
#pragma mark - <UICollectionViewDelegateFlowLayout>
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attr =
[collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
CGSize size = [self sizeWithAttribute:attr collectionView:collectionView];
size = [self inlaidSizeAtIndexPath:indexPath withSize:size collectionView:collectionView];
return size;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(__unused UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
return [self insetsAtSectionIndex:section collectionView:collectionView];
}
- (CGFloat)collectionView:(__unused UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(__unused NSInteger)section {
if ([collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
return _styler.gridPadding;
}
return [(UICollectionViewFlowLayout *)collectionViewLayout minimumLineSpacing];
}
return 0;
}
- (CGSize)sizeWithAttribute:(UICollectionViewLayoutAttributes *)attr
collectionView:(UICollectionView *)collectionView {
CGFloat height = MDCCellDefaultOneLineHeight;
if ([_styler.delegate respondsToSelector:@selector(collectionView:cellHeightAtIndexPath:)]) {
height = [_styler.delegate collectionView:collectionView cellHeightAtIndexPath:attr.indexPath];
}
CGFloat width = [self cellWidthAtSectionIndex:attr.indexPath.section
collectionView:collectionView];
return CGSizeMake(width, height);
}
// Note that this method is only exposed temporarily until self-sizing cells are supported.
- (CGFloat)cellWidthAtSectionIndex:(NSInteger)section
collectionView:(UICollectionView *)collectionView {
UIEdgeInsets contentInset = collectionView.contentInset;
// On the iPhone X, we need to use the offset which might take into account the safe area.
if (@available(iOS 11.0, *)) {
contentInset = collectionView.adjustedContentInset;
}
CGFloat bounds = CGRectGetWidth(UIEdgeInsetsInsetRect(collectionView.bounds, contentInset));
UIEdgeInsets sectionInsets = [self collectionView:collectionView
layout:collectionView.collectionViewLayout
insetForSectionAtIndex:section];
CGFloat insets = sectionInsets.left + sectionInsets.right;
if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
CGFloat cellWidth = bounds - insets - (_styler.gridPadding * (_styler.gridColumnCount - 1));
return cellWidth / _styler.gridColumnCount;
}
return bounds - insets;
}
- (UIEdgeInsets)insetsAtSectionIndex:(NSInteger)section
collectionView:(UICollectionView *)collectionView {
// Determine insets based on cell style.
CGFloat inset = (CGFloat)floor(MDCCollectionViewCellStyleCardSectionInset);
UIEdgeInsets insets = UIEdgeInsetsZero;
NSInteger numberOfSections = collectionView.numberOfSections;
BOOL isTop = (section == 0);
BOOL isBottom = (section == numberOfSections - 1);
MDCCollectionViewCellStyle cellStyle = [_styler cellStyleAtSectionIndex:section];
BOOL isCardStyle = cellStyle == MDCCollectionViewCellStyleCard;
BOOL isGroupedStyle = cellStyle == MDCCollectionViewCellStyleGrouped;
// Set left/right insets.
if (isCardStyle) {
insets.left = inset;
insets.right = inset;
if (@available(iOS 11.0, *)) {
if (collectionView.contentInsetAdjustmentBehavior ==
UIScrollViewContentInsetAdjustmentAlways) {
// We don't need section insets if there are already safe area insets.
insets.left = MAX(0, insets.left - collectionView.safeAreaInsets.left);
insets.right = MAX(0, insets.right - collectionView.safeAreaInsets.right);
}
}
}
// Set top/bottom insets.
if (isCardStyle || isGroupedStyle) {
insets.top = (CGFloat)floor((isTop) ? inset : inset / 2);
insets.bottom = (CGFloat)floor((isBottom) ? inset : inset / 2);
}
return insets;
}
- (CGSize)inlaidSizeAtIndexPath:(NSIndexPath *)indexPath
withSize:(CGSize)size
collectionView:(UICollectionView *)collectionView {
// If object is inlaid, return its adjusted size.
if ([_styler isItemInlaidAtIndexPath:indexPath]) {
CGFloat inset = MDCCollectionViewCellStyleCardSectionInset;
UIEdgeInsets inlayInsets = UIEdgeInsetsZero;
BOOL prevCellIsInlaid = NO;
BOOL nextCellIsInlaid = NO;
BOOL hasSectionHeader = NO;
if ([self respondsToSelector:@selector(collectionView:
layout:referenceSizeForHeaderInSection:)]) {
CGSize headerSize = [self collectionView:collectionView
layout:collectionView.collectionViewLayout
referenceSizeForHeaderInSection:indexPath.section];
hasSectionHeader = !CGSizeEqualToSize(headerSize, CGSizeZero);
}
BOOL hasSectionFooter = NO;
if ([self respondsToSelector:@selector(collectionView:
layout:referenceSizeForFooterInSection:)]) {
CGSize footerSize = [self collectionView:collectionView
layout:_collectionViewLayout
referenceSizeForFooterInSection:indexPath.section];
hasSectionFooter = !CGSizeEqualToSize(footerSize, CGSizeZero);
}
// Check if previous cell is inlaid.
if (indexPath.item > 0 || hasSectionHeader) {
NSIndexPath *prevIndexPath = [NSIndexPath indexPathForItem:(indexPath.item - 1)
inSection:indexPath.section];
prevCellIsInlaid = [_styler isItemInlaidAtIndexPath:prevIndexPath];
inlayInsets.top = prevCellIsInlaid ? inset / 2 : inset;
}
// Check if next cell is inlaid.
if (indexPath.item < [collectionView numberOfItemsInSection:indexPath.section] - 1 ||
hasSectionFooter) {
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:(indexPath.item + 1)
inSection:indexPath.section];
nextCellIsInlaid = [_styler isItemInlaidAtIndexPath:nextIndexPath];
inlayInsets.bottom = nextCellIsInlaid ? inset / 2 : inset;
}
// Apply top/bottom height adjustments to inlaid object.
size.height += inlayInsets.top + inlayInsets.bottom;
}
return size;
}
#pragma mark - Subclassing Methods
/*
The below method is solely used for subclasses to retrieve width information in order to
calculate cell height. Not meant to call method cellWidthAtSectionIndex:collectionView as
that method recalculates section insets which we don't want to do.
*/
- (CGFloat)cellWidthAtSectionIndex:(NSInteger)section {
UIEdgeInsets contentInset = self.collectionView.contentInset;
// On the iPhone X, we need to use the offset which might take into account the safe area.
if (@available(iOS 11.0, *)) {
contentInset = self.collectionView.adjustedContentInset;
}
CGFloat bounds = CGRectGetWidth(UIEdgeInsetsInsetRect(self.collectionView.bounds, contentInset));
UIEdgeInsets sectionInsets = [self collectionView:self.collectionView
layout:self.collectionView.collectionViewLayout
insetForSectionAtIndex:section];
CGFloat insets = sectionInsets.left + sectionInsets.right;
if (_styler != nil) {
if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
CGFloat cellWidth = bounds - insets - (_styler.gridPadding * (_styler.gridColumnCount - 1));
if (_styler.gridColumnCount > 0) {
return cellWidth / _styler.gridColumnCount;
}
}
}
return bounds - insets;
}
#pragma mark - <MDCInkTouchControllerDelegate>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (BOOL)inkTouchController:(__unused MDCInkTouchController *)inkTouchController
shouldProcessInkTouchesAtTouchLocation:(CGPoint)location {
// Only store touch location and do not allow ink processing. This ink location will be used when
// manually starting/stopping the ink animation during cell highlight/unhighlight states.
if (!self.currentlyActiveInk) {
_inkTouchLocation = location;
}
return NO;
}
- (MDCInkView *)inkTouchController:(MDCInkTouchController *)inkTouchController
inkViewAtTouchLocation:(CGPoint)location {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
MDCInkView *ink = nil;
if ([_styler.delegate respondsToSelector:@selector(collectionView:
inkTouchController:inkViewAtIndexPath:)]) {
return [_styler.delegate collectionView:self.collectionView
inkTouchController:inkTouchController
inkViewAtIndexPath:indexPath];
}
if ([cell isKindOfClass:[MDCCollectionViewCell class]]) {
MDCCollectionViewCell *inkCell = (MDCCollectionViewCell *)cell;
if (!inkCell.enableRippleBehavior) {
if ([inkCell respondsToSelector:@selector(inkView)]) {
// Set cell ink.
ink = [cell performSelector:@selector(inkView)];
}
}
}
return ink;
}
#pragma clang diagnostic pop
#pragma mark - <MDCRippleTouchControllerDelegate>
- (BOOL)rippleTouchController:(MDCRippleTouchController *)rippleTouchController
shouldProcessRippleTouchesAtTouchLocation:(CGPoint)location {
// Only store touch location and do not allow ripple processing. This ripple location will be used
// when manually starting/stopping the ripple animation during cell highlight/unhighlight states.
if (!self.currentlyActiveInk) {
_inkTouchLocation = location;
}
return NO;
}
- (MDCRippleView *)rippleTouchController:(MDCRippleTouchController *)rippleTouchController
rippleViewAtTouchLocation:(CGPoint)location {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
MDCRippleView *ripple = nil;
if ([_styler.delegate respondsToSelector:@selector(collectionView:
rippleTouchController:rippleViewAtIndexPath:)]) {
return [_styler.delegate collectionView:self.collectionView
rippleTouchController:rippleTouchController
rippleViewAtIndexPath:indexPath];
}
if ([cell isKindOfClass:[MDCCollectionViewCell class]]) {
MDCCollectionViewCell *rippleCell = (MDCCollectionViewCell *)cell;
if (rippleCell.enableRippleBehavior) {
if ([rippleCell respondsToSelector:@selector(rippleView)]) {
// Set cell ripple.
ripple = [cell performSelector:@selector(rippleView)];
}
}
}
return ripple;
}
#pragma mark - <UICollectionViewDataSource>
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath {
// TODO (shepj): This implementation of registering cell classes in data source methods should be
// rethought. This causes a crash without a workaround when collections with headers or
// footers entering editing mode. Also, we should find a way around implementing a data source
// method in a super class.
// Issue: https://github.com/material-components/material-components-ios/issues/1208
// Editing info bar.
if ([kind isEqualToString:MDCCollectionInfoBarKindHeader] ||
[kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
NSString *identifier = NSStringFromClass([MDCCollectionInfoBarView class]);
UICollectionReusableView *supplementaryView =
[collectionView dequeueReusableSupplementaryViewOfKind:kind
withReuseIdentifier:identifier
forIndexPath:indexPath];
// Update info bar.
if ([supplementaryView isKindOfClass:[MDCCollectionInfoBarView class]]) {
MDCCollectionInfoBarView *infoBarView = (MDCCollectionInfoBarView *)supplementaryView;
infoBarView.delegate = self;
infoBarView.kind = kind;
[self updateControllerWithInfoBar:infoBarView];
}
return supplementaryView;
} else {
return [super collectionView:collectionView
viewForSupplementaryElementOfKind:kind
atIndexPath:indexPath];
}
}
#pragma mark - <UICollectionViewDelegate>
- (BOOL)collectionView:(__unused UICollectionView *)collectionView
shouldHighlightItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView
didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
if ([_styler.delegate respondsToSelector:@selector(collectionView:hidesInkViewAtIndexPath:)] &&
[_styler.delegate collectionView:collectionView hidesInkViewAtIndexPath:indexPath]) {
return;
}
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
CGPoint location = [collectionView convertPoint:_inkTouchLocation toView:cell];
if (self.enableRippleBehavior) {
MDCRippleView *rippleView;
if ([cell respondsToSelector:@selector(rippleView)]) {
rippleView = [cell performSelector:@selector(rippleView)];
} else {
return;
}
if ([_styler.delegate respondsToSelector:@selector(collectionView:inkColorAtIndexPath:)]) {
rippleView.rippleColor = [_styler.delegate collectionView:collectionView
inkColorAtIndexPath:indexPath];
if (!rippleView.rippleColor) {
rippleView.rippleColor = [UIColor colorWithWhite:0 alpha:0.12f];
}
}
self.currentlyActiveInk = YES;
[rippleView beginRippleTouchDownAtPoint:location animated:YES completion:nil];
} else {
// Start cell ink show animation.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
MDCInkView *inkView;
#pragma clang diagnostic pop
if ([cell respondsToSelector:@selector(inkView)]) {
inkView = [cell performSelector:@selector(inkView)];
} else {
return;
}
// Update ink color if necessary.
if ([_styler.delegate respondsToSelector:@selector(collectionView:inkColorAtIndexPath:)]) {
inkView.inkColor = [_styler.delegate collectionView:collectionView
inkColorAtIndexPath:indexPath];
if (!inkView.inkColor) {
inkView.inkColor = inkView.defaultInkColor;
}
}
self.currentlyActiveInk = YES;
[inkView startTouchBeganAnimationAtPoint:location completion:nil];
}
}
- (void)collectionView:(UICollectionView *)collectionView
didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
CGPoint location = [collectionView convertPoint:_inkTouchLocation toView:cell];
if (self.enableRippleBehavior) {
MDCRippleView *rippleView;
if ([cell respondsToSelector:@selector(rippleView)]) {
rippleView = [cell performSelector:@selector(rippleView)];
} else {
return;
}
self.currentlyActiveInk = NO;
[rippleView beginRippleTouchUpAnimated:YES completion:nil];
} else {
// Start cell ink evaporate animation.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
MDCInkView *inkView;
#pragma clang diagnostic pop
if ([cell respondsToSelector:@selector(inkView)]) {
inkView = [cell performSelector:@selector(inkView)];
} else {
return;
}
self.currentlyActiveInk = NO;
[inkView startTouchEndedAnimationAtPoint:location completion:nil];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView
shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (_editor.isEditing) {
if ([self collectionView:collectionView canEditItemAtIndexPath:indexPath]) {
return [self collectionView:collectionView canSelectItemDuringEditingAtIndexPath:indexPath];
}
return NO;
}
return YES;
}
- (BOOL)collectionView:(UICollectionView *)collectionView
shouldDeselectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return collectionView.allowsMultipleSelection;
}
- (void)collectionView:(__unused UICollectionView *)collectionView
didSelectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
[self updateFooterInfoBarIfNecessary];
}
- (void)collectionView:(__unused UICollectionView *)collectionView
didDeselectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
[self updateFooterInfoBarIfNecessary];
}
#pragma mark - <MDCCollectionViewEditingDelegate>
- (BOOL)collectionViewAllowsEditing:(__unused UICollectionView *)collectionView {
return NO;
}
- (void)collectionViewWillBeginEditing:(__unused UICollectionView *)collectionView {
if (self.currentlyActiveInk) {
if (self.enableRippleBehavior) {
MDCRippleView *activeRippleView = [self rippleTouchController:_rippleTouchController
rippleViewAtTouchLocation:_inkTouchLocation];
[activeRippleView beginRippleTouchUpAnimated:YES completion:nil];
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
MDCInkView *activeInkView = [self inkTouchController:_inkTouchController
inkViewAtTouchLocation:_inkTouchLocation];
#pragma clang diagnostic pop
[activeInkView startTouchEndedAnimationAtPoint:_inkTouchLocation completion:nil];
}
}
// Inlay all items.
_styler.allowsItemInlay = YES;
_styler.allowsMultipleItemInlays = YES;
[_styler applyInlayToAllItemsAnimated:YES];
[self updateHeaderInfoBarIfNecessary];
}
- (void)collectionViewWillEndEditing:(__unused UICollectionView *)collectionView {
// Remove inlay of all items.
[_styler removeInlayFromAllItemsAnimated:YES];
[self updateFooterInfoBarIfNecessary];
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canEditItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return [self collectionViewAllowsEditing:collectionView];
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canSelectItemDuringEditingAtIndexPath:(NSIndexPath *)indexPath {
if ([self collectionViewAllowsEditing:collectionView]) {
return [self collectionView:collectionView canEditItemAtIndexPath:indexPath];
}
return NO;
}
#pragma mark - Item Moving
- (BOOL)collectionViewAllowsReordering:(__unused UICollectionView *)collectionView {
return NO;
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canMoveItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return ([self collectionViewAllowsEditing:collectionView] &&
[self collectionViewAllowsReordering:collectionView]);
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canMoveItemAtIndexPath:(NSIndexPath *)indexPath
toIndexPath:(NSIndexPath *)newIndexPath {
// First ensure both source and target items can be moved.
return ([self collectionView:collectionView canMoveItemAtIndexPath:indexPath] &&
[self collectionView:collectionView canMoveItemAtIndexPath:newIndexPath]);
}
- (void)collectionView:(UICollectionView *)collectionView
didMoveItemAtIndexPath:(NSIndexPath *)indexPath
toIndexPath:(NSIndexPath *)newIndexPath {
[collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
}
#pragma mark - Swipe-To-Dismiss-Items
- (BOOL)collectionViewAllowsSwipeToDismissItem:(__unused UICollectionView *)collectionView {
return NO;
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canSwipeToDismissItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return [self collectionViewAllowsSwipeToDismissItem:collectionView];
}
- (void)collectionView:(__unused UICollectionView *)collectionView
didEndSwipeToDismissItemAtIndexPath:(NSIndexPath *)indexPath {
[self deleteIndexPaths:@[ indexPath ]];
}
#pragma mark - Swipe-To-Dismiss-Sections
- (BOOL)collectionViewAllowsSwipeToDismissSection:(__unused UICollectionView *)collectionView {
return NO;
}
- (BOOL)collectionView:(UICollectionView *)collectionView
canSwipeToDismissSection:(__unused NSInteger)section {
return [self collectionViewAllowsSwipeToDismissSection:collectionView];
}
- (void)collectionView:(__unused UICollectionView *)collectionView
didEndSwipeToDismissSection:(NSInteger)section {
[self deleteSections:[NSIndexSet indexSetWithIndex:section]];
}
#pragma mark - Private
- (void)deleteIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
if ([self respondsToSelector:@selector(collectionView:willDeleteItemsAtIndexPaths:)]) {
void (^batchUpdates)(void) = ^{
// Notify delegate to delete data.
[self collectionView:self.collectionView willDeleteItemsAtIndexPaths:indexPaths];
// Delete index paths.
[self.collectionView deleteItemsAtIndexPaths:indexPaths];
};
void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) {
[self updateFooterInfoBarIfNecessary];
// Notify delegate of deletion.
if ([self respondsToSelector:@selector(collectionView:didDeleteItemsAtIndexPaths:)]) {
[self collectionView:self.collectionView didDeleteItemsAtIndexPaths:indexPaths];
}
};
// Animate deletion.
[self.collectionView performBatchUpdates:batchUpdates completion:completionBlock];
}
}
- (void)deleteSections:(NSIndexSet *)sections {
if ([self respondsToSelector:@selector(collectionView:willDeleteSections:)]) {
void (^batchUpdates)(void) = ^{
// Notify delegate to delete data.
[self collectionView:self.collectionView willDeleteSections:sections];
// Delete sections.
[self.collectionView deleteSections:sections];
};
void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) {
[self updateFooterInfoBarIfNecessary];
// Notify delegate of deletion.
if ([self respondsToSelector:@selector(collectionView:didDeleteSections:)]) {
[self collectionView:self.collectionView didDeleteSections:sections];
}
};
// Animate deletion.
[self.collectionView performBatchUpdates:batchUpdates completion:completionBlock];
}
}
- (void)updateHeaderInfoBarIfNecessary {
if (_editor.isEditing) {
// Show HUD only once before autodissmissing.
BOOL allowsSwipeToDismissItem = NO;
if ([self respondsToSelector:@selector(collectionViewAllowsSwipeToDismissItem:)]) {
allowsSwipeToDismissItem = [self collectionViewAllowsSwipeToDismissItem:self.collectionView];
}
if (!_headerInfoBar.isVisible && !_headerInfoBarDismissed && allowsSwipeToDismissItem) {
[_headerInfoBar showAnimated:YES];
} else {
[_headerInfoBar dismissAnimated:YES];
}
}
}
- (void)updateFooterInfoBarIfNecessary {
NSInteger selectedItemCount = [self.collectionView.indexPathsForSelectedItems count];
if (_editor.isEditing) {
// Invalidate layout to add info bar if necessary.
[self.collectionView.collectionViewLayout invalidateLayout];
if (_footerInfoBar) {
if (selectedItemCount > 0 && !_footerInfoBar.isVisible) {
[_footerInfoBar showAnimated:YES];
} else if (selectedItemCount == 0 && _footerInfoBar.isVisible) {
[_footerInfoBar dismissAnimated:YES];
}
}
} else if (selectedItemCount == 0 && _footerInfoBar.isVisible) {
[_footerInfoBar dismissAnimated:YES];
}
}
- (void)updateContentWithBottomInset:(CGFloat)inset {
// Update bottom inset to account for footer info bar.
UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.bottom += inset;
[UIView animateWithDuration:MDCCollectionInfoBarAnimationDuration
animations:^{
self.collectionView.contentInset = contentInset;
}];
}
@end