效果图如下
思路:
根据cell距离屏幕中间的距离,设置cell的缩小系数,并通过设置 attributes.transform 缩小cell
attributes.transform = CGAffineTransformMakeScale(1.0, scale);
核心代码
//
// LBMiddleExpandLayout.m
// LiuboMiddleExpandLayout
//
// Created by liubo on 2023/7/8.
//
#import "LBMiddleExpandLayout.h"
@implementation LBMiddleExpandLayout
//设置放大动画
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *arr = [self getCopyOfAttributes:[super layoutAttributesForElementsInRect:rect]];
//屏幕中线
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width/2.0f;
//刷新cell缩放
for (UICollectionViewLayoutAttributes *attributes in arr) {
CGFloat distance = fabs(attributes.center.x - centerX);
//移动的距离和屏幕宽度的的比例
CGFloat apartScale = distance/self.collectionView.bounds.size.width;
//把卡片移动范围固定到 -π/6到 +π/6这一个范围内
CGFloat scale = fabs(cos(apartScale * M_PI/6));
//设置cell的缩放 按照余弦函数曲线 越居中越趋近于1
attributes.transform = CGAffineTransformMakeScale(1.0, scale);
}
return arr;
}
//防止报错 先复制attributes
- (NSArray *)getCopyOfAttributes:(NSArray *)attributes
{
NSMutableArray *copyArr = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *attribute in attributes) {
[copyArr addObject:[attribute copy]];
}
return copyArr;
}
//是否需要重新计算布局
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return true;
}
@end
//手指拖动开始
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.m_dragStartX = scrollView.contentOffset.x;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.endDraggingOffset < 0 && scrollView.contentOffset.x >= self.endDraggingOffset) {
//这里是为了上刷新的时候,停留在固定的偏移量,展示loading
scrollView.contentOffset = CGPointMake(self.endDraggingOffset, 0);
}
if (!self.hasNextPage) {
return;
}
/*
遇到一次精度问题,正在刷新的时候 应该展示loading,但是由于精度问题,这时候
scrollView.contentOffset.x = -52.333333 , refreshThreshold = -52.399998,
如果 是 (scrollView.contentOffset.x <= refreshThreshold ),就会造成loading隐藏,
所以这里+0.5
*/
if (scrollView.contentOffset.x <= (refreshThreshold + 0.5)) {
self.refreshView.hidden = NO;
} else {
self.refreshView.hidden = YES;
}
}
//手指拖动停止
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
self.m_dragEndX = scrollView.contentOffset.x;
dispatch_async(dispatch_get_main_queue(), ^{
[self fixCellToCenter];
});
if (scrollView.contentOffset.x < refreshThreshold) {
if (!self.hasNextPage) {
return;
}
if (self.isLoadingMore) {
return;
}
self.endDraggingOffset = refreshThreshold;
NSMutableArray *array = [NSMutableArray array];
for (int i = (self.signsArray.count + 10); i > self.signsArray.count; i --) {
[array addObject:[NSString stringWithFormat:@"%d", i]];
}
[self refreshHandleWithArray:array];
self.isLoadingMore = YES;
}
}
#pragma mark - private
- (void)fixCellToCenter {
//最小滚动距离
float dragMiniDistance = self.view.bounds.size.width/20.0f;
if (self.m_dragStartX - self.m_dragEndX >= dragMiniDistance) {
self.m_currentIndex -= 1;//向右
} else if(self.m_dragEndX - self.m_dragStartX >= dragMiniDistance){
self.m_currentIndex += 1;//向左
}
NSInteger maxIndex = [_collectionView numberOfItemsInSection:0] - 1;
self.m_currentIndex = self.m_currentIndex <= 0 ? 0 : self.m_currentIndex;
self.m_currentIndex = self.m_currentIndex >= maxIndex ? maxIndex : self.m_currentIndex;
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.m_currentIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
- (void)refreshHandleWithArray:(NSArray <NSString *> *)array
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSArray *originArray = [self.signsArray copy];
[self.signsArray removeAllObjects];
[self.signsArray addObjectsFromArray:array];
[self.signsArray addObjectsFromArray:originArray];
self.refreshView.hidden = YES;
[self.collectionView reloadData];
self.m_currentIndex = array.count - 1;
CGFloat temp = self.endDraggingOffset;
self.endDraggingOffset = 0;
/*
刷新过之后首先要隐性偏移到之前滚动到的日签卡片处,(index.item已经不一样了,之前是0,现在是请求到的
日签数量),
*/
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.m_currentIndex + 1 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
CGFloat currentOffset = self.collectionView.contentOffset.x;
currentOffset = currentOffset + temp;
[self.collectionView setContentOffset:CGPointMake(currentOffset, 0)];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
/*
然后显性的滚动到请求到的日签处的最后一个,从而达到动画切换到最新的一个选项的效果
*/
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.m_currentIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
self.isLoadingMore = NO;
});
});
}
注意上面,加载更多的时候,有一个衔接动画,由于是从左边加载更多的,但是我们正常的添加新cell是添加到右边的,
这里我们 做了一个假动作,即将新数据添加到前面之后,刷新整个collectionView,然后立即隐性(无动画)滚动到刷新前那个cell的位置
,停留0.01秒时候,在有动画的滚动到新添加的最后一个cell (即和之前的cell紧邻的),这样就实现了刷新的一个动画效果,
总的思路就是刷新过之后,无动效滚动到原来的cell,然后又动效滚动到目标cell
///这里是为了隐性滚动到原位置
[self.collectionView setContentOffset:CGPointMake(currentOffset, 0)];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
/*
然后显性的滚动到请求到的日签处的最后一个,从而达到动画切换到最新的一个选项的效果
*/
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.m_currentIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
self.isLoadingMore = NO;
});
链接: link