這是一種很常見的布局,可以使用CollectionViewFlowLayout戈稿,在其代理方法中通過相關設置來達到此效果笛质,但還是比較麻煩吹泡。如若直接用CollectionViewLayout來實現(xiàn),會簡單不少经瓷,且靈活性較好爆哑。
其實此布局主要考慮的問題就兩點:
1.實現(xiàn)任意尺寸的分頁大小,即每次滑動后某一張卡片都能停在屏幕中間(默認collectionView開啟pageEnable后是以其尺寸來作為分頁大杏咚薄)揭朝。
2.放大過程中實現(xiàn)每張卡片的縮放。
從官方文檔得知自定義CollectionViewLayout至少需要重寫以下方法:
// 當collectionView滑動的時候,可見區(qū)域改變的時候是否使當前布局失效以重新布局
1.shouldInvalidateLayoutForBoundsChange:
// 需要在此方法中返回collectionView的內(nèi)容大小
2.collectionViewContentSize
// 為每個Cell返回一個對應的Attributes色冀,我們需要在該Attributes中設置對應的屬性潭袱,如Frame等
3.layoutAttributesForItemAtIndexPath:
// 可在此方法中對可見rect中的cell的屬性進行相應設置
4.layoutAttributesForElementsInRect:
我們就以橫向滾動的布局為例來實現(xiàn)此布局,首先锋恬,定義好所需的屬性:
@property (nonatomic, assign) CGFloat spacing; //cell間距
@property (nonatomic, assign) CGSize itemSize; //cell的尺寸
@property (nonatomic, assign) CGFloat scale; //縮放率
@property (nonatomic, assign) UIEdgeInsets edgeInset; //邊距
接下來是重寫上面提到的幾個方法
1. shouldInvalidateLayoutForBoundsChange:
// 由于是此布局是平鋪的效果屯换,所以當collectionView的bounds變化時,所展現(xiàn)的cell的個數(shù)及顯示效果可能會發(fā)生變化与学,故此方法應返回YES彤悔。
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
2. collectionViewContentSize
// 由于此布局一般只有一個section,故在此例中僅考慮只有一個section的情況
// 內(nèi)容的寬度為: 左邊距 + n*cell的寬 + (n-1)*cell的間距 + 右邊距索守。
- (CGSize)collectionViewContentSize {
NSInteger count = [self.collectionView numberOfItemsInSection:0];
CGFloat width = count*(self.itemSize.width+self.spacing)-self.spacing+self.edgeInset.left+self.edgeInset.right;
CGFloat height = self.collectionView.bounds.size.height;
return CGSizeMake(width, height);
}
3.layoutAttributesForItemAtIndexPath:
// 此方法要求返回一個UICollectionViewLayoutAttributes * 類型的對象
// 該對象包含對應cell外觀所需的必要屬性晕窑,包括center、frame卵佛、transform杨赤、alpha及其他屬性
// 在此方法中只需要做一件事,那就是給cell設置好正確的frame截汪。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attribute.size = self.itemSize;
CGFloat x = self.edgeInset.left + indexPath.item*(self.spacing+self.itemSize.width);
CGFloat y = 0.5*(self.collectionView.bounds.size.height - self.itemSize.height);
attribute.frame = CGRectMake(x, y, attribute.size.width, attribute.size.height);
return attribute;
}
4.layoutAttributesForElementsInRect:
// 可在此方法中設置縮放效果
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *indexPaths = [self indexPathsInRect:rect];
//找到屏幕中間的位置
CGFloat centerX = self.collectionView.contentOffset.x + 0.5*self.collectionView.bounds.size.width;
NSMutableArray *attributes = [NSMutableArray array];
for (NSIndexPath *indexPath in indexPaths) {
UICollectionViewLayoutAttributes* attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
// 判斷可見區(qū)域和此cell的frame是否有重疊疾牲,因為indexPathsInRect返回的indexPath并不是十分準確。
if (!CGRectIntersectsRect(rect, attribute.frame)) {
//若不重疊則無需進行以下的步驟
continue;
}
[attributes addObject:attribute];
//計算每一個cell離屏幕中間的距離
CGFloat offsetX = ABS(attribute.center.x - centerX);
//這是設置一個縮放區(qū)域的閾值衙解,當cell在此區(qū)域之外不進行縮放阳柔,改值可視具體情況進行修改。
CGFloat space = self.itemSize.width+self.spacing;
if (offsetX<space) {
CGFloat scale = 1+(1-offsetX/space)*(self.scale-1);
attribute.transform = CGAffineTransformMakeScale(scale, scale);
// 設置此屬性是為了當cell層疊后丢郊,使得位于中間的cell總是位于最前面盔沫,若不明白可將此行注釋一試便知医咨。
attribute.zIndex = 1;
}
}
return attributes;
}
- (NSArray *)indexPathsInRect:(CGRect)rect {
NSInteger leftIndex = (rect.origin.x-self.edgeInset.left)/(self.itemSize.width+self.spacing);
NSInteger rightIndex = (CGRectGetMaxX(rect)-self.edgeInset.left)/(self.itemSize.width+self.spacing);
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
preIndex = preIndex<0 ? 0 : preIndex;
latIndex = latIndex>=itemCount ? itemCount-1 : latIndex;
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i=leftIndex; i<=rightIndex; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
[indexPaths addObject:indexPath];
}
return indexPaths;
}
至此枫匾,我們已經(jīng)實現(xiàn)了在滾動過程中靠近屏幕中間的cell放大的效果,但是還沒實現(xiàn)滾動停止時某一個cell正好在屏幕中間拟淮,要想實現(xiàn)此效果干茉,需要在targetContentOffsetForProposedContentOffset:withScrollingVelocity
方法中實現(xiàn)此邏輯。此方法會給一個系統(tǒng)默認計算好的collectionView應該停下來的位置很泊,返回一個collectionView最后要停下來的位置角虫。
// 需要在此方法中獲取默認情況下停止?jié)L動時離屏幕中間最近的那個cell沾谓,并計算兩者的距離,將此距離補到proposedContentOffset上即可戳鹅。
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGRect rect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *attributes = [self layoutAttributesForElementsInRect:rect];
CGFloat centerX = proposedContentOffset.x + 0.5*self.collectionView.bounds.size.width;
CGFloat minOffsetX = MAXFLOAT;
for (UICollectionViewLayoutAttributes* attribute in attributes) {
CGFloat offsetX = attribute.center.x - centerX;
if (ABS(offsetX) < ABS(minOffsetX)) {
minOffsetX = offsetX;
}
}
return CGPointMake(proposedContentOffset.x + minOffsetX, proposedContentOffset.y);
}
現(xiàn)在已經(jīng)實現(xiàn)了文章開頭動圖中展示的效果了均驶。再將之前定義的各個屬性的set方法重寫,以便在設置這些屬性的時候進行重新布局枫虏。
注意:若想實現(xiàn)多個section以及含有sectionHeader或sectionFooter妇穴,請參照此思路來實現(xiàn),并且重寫以下兩個方法:
// 含有sectionHeader或sectionFooter應重寫此方法
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
// cell含有裝飾視圖時要重寫此方法隶债。
layoutAttributesForDecorationViewOfKind:atIndexPath:
想要源碼的可以點擊這里獲取腾它,GitHub上的版本支持水平和垂直布局且已加以優(yōu)化。
此外還有Swift版本的哦死讹,請點擊這里以獲取瞒滴。
若覺得對你有用的話,還請給個star哈~