首先查看效果圖
實現的原理就是通過自定義UICollectionView layout,然后
設置減速速率是快速就可以達到吸附的效果
_collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
下面貼出所有代碼
這里是.h
//
// LBMiddleExpandLayout.h
// LiuboMiddleExpandLayout
//
// Created by liubo on 2023/7/8.
//#import <UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@interface LBMiddleExpandLayout : UICollectionViewFlowLayout@property (nonatomic, assign) BOOL scrollAnimation;/**<是否有分頁動畫*/
@property (nonatomic, assign) CGPoint lastOffset;/**<記錄上次滑動停止時contentOffset值*/- (instancetype)initWithSectionInset:(UIEdgeInsets)insetsandMiniLineSapce:(CGFloat)miniLineSpaceandMiniInterItemSpace:(CGFloat)miniInterItemSpaceandItemSize:(CGSize)itemSize;@endNS_ASSUME_NONNULL_END
//
// LBMiddleExpandLayout.m
// LiuboMiddleExpandLayout
//
// Created by liubo on 2023/7/8.
//#import "LBMiddleExpandLayout.h"@interface LBMiddleExpandLayout ()@property (nonatomic, assign) UIEdgeInsets sectionInsets;
@property (nonatomic, assign) CGFloat miniLineSpace;
@property (nonatomic, assign) CGFloat miniInterItemSpace;
@property (nonatomic, assign) CGSize eachItemSize;@end@implementation LBMiddleExpandLayout/*初始化部分*/
- (instancetype)initWithSectionInset:(UIEdgeInsets)insetsandMiniLineSapce:(CGFloat)miniLineSpaceandMiniInterItemSpace:(CGFloat)miniInterItemSpaceandItemSize:(CGSize)itemSize
{self = [self init];if (self) {//基本尺寸/邊距設置self.sectionInsets = insets;self.miniLineSpace = miniLineSpace;self.miniInterItemSpace = miniInterItemSpace;self.eachItemSize = itemSize;}return self;
}- (instancetype)init
{self = [super init];if (self) {_lastOffset = CGPointZero;}return self;
}-(void)prepareLayout
{[super prepareLayout];self.scrollDirection = UICollectionViewScrollDirectionHorizontal;// 水平滾動/*設置內邊距*/self.sectionInset = _sectionInsets;self.minimumLineSpacing = _miniLineSpace;self.minimumInteritemSpacing = _miniInterItemSpace;self.itemSize = _eachItemSize;/*** decelerationRate系統給出了2個值:* 1. UIScrollViewDecelerationRateFast(速率快)* 2. UIScrollViewDecelerationRateNormal(速率慢)* 此處設置滾動加速度率為fast,這樣在移動cell后就會出現明顯的吸附效果*/self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
}/*** 這個方法的返回值,就決定了collectionView停止滾動時的偏移量*/
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{CGFloat pageSpace = [self stepSpace];//計算分頁步距CGFloat offsetMax = self.collectionView.contentSize.width - (pageSpace + self.sectionInset.right + self.miniLineSpace);CGFloat offsetMin = 0;/*修改之前記錄的位置,如果小于最小contentsize或者大于最大contentsize則重置值*/if (_lastOffset.x<offsetMin){_lastOffset.x = offsetMin;}else if (_lastOffset.x>offsetMax){_lastOffset.x = offsetMax;}CGFloat offsetForCurrentPointX = ABS(proposedContentOffset.x - _lastOffset.x);//目標位移點距離當前點的距離絕對值CGFloat velocityX = velocity.x;BOOL direction = (proposedContentOffset.x - _lastOffset.x) > 0;//判斷當前滑動方向,手指向左滑動:YES;手指向右滑動:NOif (offsetForCurrentPointX > pageSpace/8. && _lastOffset.x>=offsetMin && _lastOffset.x<=offsetMax){NSInteger pageFactor = 0;//分頁因子,用于計算滑過的cell個數if (velocityX != 0){/*滑動*/pageFactor = ABS(velocityX);//速率越快,cell滑過數量越多}else{/*** 拖動* 沒有速率,則計算:位移差/默認步距=分頁因子*/pageFactor = ABS(offsetForCurrentPointX/pageSpace);}/*設置pageFactor上限為2, 防止滑動速率過大,導致翻頁過多*/pageFactor = pageFactor<1?1:(pageFactor<3?1:2);CGFloat pageOffsetX = pageSpace*pageFactor;proposedContentOffset = CGPointMake(_lastOffset.x + (direction?pageOffsetX:-pageOffsetX), proposedContentOffset.y);}else{/*滾動距離,小于翻頁步距一半,則不進行翻頁操作*/proposedContentOffset = CGPointMake(_lastOffset.x, _lastOffset.y);}//記錄當前最新位置_lastOffset.x = proposedContentOffset.x;return proposedContentOffset;
}/***計算每滑動一頁的距離:步距*/
-(CGFloat)stepSpace
{return self.eachItemSize.width + self.miniLineSpace;
}/*** 當collectionView的顯示范圍發生改變的時候,是否需要重新刷新布局* 一旦重新刷新布局,就會重新調用 layoutAttributesForElementsInRect:方法*/
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{return YES;
}/***防止報錯先復制attributes*/
- (NSArray *)getCopyOfAttributes:(NSArray *)attributes
{NSMutableArray *copyArr = [NSMutableArray new];for (UICollectionViewLayoutAttributes *attribute in attributes) {[copyArr addObject:[attribute copy]];}return copyArr;
}/***設置放大動畫*/
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{/*獲取rect范圍內的所有subview的UICollectionViewLayoutAttributes*/NSArray *arr = [self getCopyOfAttributes:[super layoutAttributesForElementsInRect:rect]];/*動畫計算*/if (self.scrollAnimation){/*計算屏幕中線*/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;/*把卡片移動范圍固定到 -π/4到 +π/4這一個范圍內*/CGFloat scale = fabs(cos(apartScale * M_PI/4));/*設置cell的縮放按照余弦函數曲線越居中越趨近于1*/CATransform3D plane_3D = CATransform3DIdentity;plane_3D = CATransform3DScale(plane_3D, 1, scale, 1);attributes.transform3D = plane_3D;}}return arr;
}@end
使用的地方
//
// LLLeftPointBannerController.m
// LiuboMiddleExpandLayout_Example
//
// Created by liubo on 2023/10/10.
// Copyright ? 2023 liubo. All rights reserved.
//#import "LLLeftPointBannerController.h"
#import "LBMiddleExpandLayout.h"
#import "LBCollectionViewCell.h"@interface LLLeftPointBannerController () <UICollectionViewDataSource, UICollectionViewDelegate>@property (nonatomic, strong) UICollectionView *collectionView;@property (nonatomic, strong) NSMutableArray *signsArray;@end@implementation LLLeftPointBannerController- (void)viewDidLoad {[super viewDidLoad];self.view.backgroundColor = [UIColor whiteColor];[self.view addSubview:self.collectionView];for (int i = 0; i < 10; i ++) {[self.signsArray addObject:[NSString stringWithFormat:@"%d", i]];}[self.collectionView reloadData];// Do any additional setup after loading the view.
}#pragma mark - UICollectionViewDataSource, UICollectionViewDelegate- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {return 1;
}- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {return self.signsArray.count;
}- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {//自定義item的UIEdgeInsetsreturn UIEdgeInsetsMake(0, 10 * PLUS_SCALE, 0, 0);
}- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {LBCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([LBCollectionViewCell class]) forIndexPath:indexPath];NSString *titleString = self.signsArray[indexPath.item];cell.titleString = titleString;return cell;
}#pragma mark - lazy load- (UICollectionView *)collectionView
{if (!_collectionView) {LBMiddleExpandLayout *layout = [[LBMiddleExpandLayout alloc] initWithSectionInset:UIEdgeInsetsZeroandMiniLineSapce:15 * PLUS_SCALEandMiniInterItemSpace:15 * PLUS_SCALEandItemSize:CGSizeMake(250 * PLUS_SCALE, 150)];layout.scrollAnimation = NO;_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];_collectionView.delegate = self;_collectionView.dataSource = self;_collectionView.pagingEnabled = NO;_collectionView.decelerationRate = UIScrollViewDecelerationRateFast;_collectionView.showsHorizontalScrollIndicator = NO;_collectionView.backgroundColor = [UIColor clearColor];[_collectionView registerClass:[LBCollectionViewCell class] forCellWithReuseIdentifier:NSStringFromClass([LBCollectionViewCell class])];[_collectionView setFrame:CGRectMake(0, 100, SCREEN_WIDTH,150)];}return _collectionView;
}- (NSMutableArray *)signsArray
{if (!_signsArray) {_signsArray = [NSMutableArray array];}return _signsArray;
}/*
#pragma mark - Navigation// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {// Get the new view controller using [segue destinationViewController].// Pass the selected object to the new view controller.
}
*/@end
本文demo鏈接: link
這里是之前寫的一個,使用自定義視圖實現的,沒有使用
UICollectionViewLayout 實現
鏈接: link