[iOS] UICollectionView實現圖片水平滾動

jopen 8年前發布 | 117K 次閱讀 iOS開發 移動開發

首先先加入一些資源文件:

先建立一個 xcassets 文件,放入圖片:

再建立一個plist文件,寫入與圖片對應的內容:

在ViewController中讀取 plist 到詞典中:

@property (nonatomic, strong) NSArray *itemTitles;

NSString *path = [[NSBundle mainBundle] pathForResource:@"titles" ofType:@"plist"];
NSDictionary *rootDictionary = [[NSDictionary alloc] initWithContentsOfFile:path];
self.itemTitles = [rootDictionary objectForKey:@"heros"];

可以打 log 輸出,可以看到 plist 的內容已經讀取出來,后面就可以用 _itemTitle 作為數據源了。

添加UICollectionView初步顯示圖片

每個 CollectionView 都有一個對應的布局 layout ,對于默認的的 UICollectionViewFlowLayout ,效果是類似Android的 GridView 的布局。如果要自定義 CollectionView 的樣式,就要對這個 layout 進行修改。

建立自己的 HorizontalFlowLayout ,繼承自 UICollectionViewFlowLayout ,然后在初始化方法里將滾動方向設置為水平:

- (instancetype) init {
    if (self = [super init]) {
        self.scrollDirection = UICollectionViewScrollDirectionHorizontal;

    }
    return self;
}

接下來定制我們的 cell 的顯示樣式,建立 DotaCell ,繼承自 UICollectionViewCell 。由于我們要實現的是圖片和文字的上下布局,所以增加兩個屬性:

@interface DotaCell : UICollectionViewCell

@property (nonatomic, strong) UIImageView *image;
@property (nonatomic, strong) UILabel *name;

@end

然后設置圖片與文字上下對齊布局,這里我使用 pod 導入 Masonry 庫來寫自動布局:

 - (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self initialize];
    }
    return self;
}

- (void)initialize {
    self.layer.doubleSided = NO;

    self.image = [[UIImageView alloc] init];
    self.image.backgroundColor = [UIColor clearColor];
    self.image.contentMode = UIViewContentModeCenter;
    self.image.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    self.name = [[UILabel alloc] init];
    self.name.font = [UIFont fontWithName:@"Helvetica Neue" size:20];
    self.name.textAlignment = NSTextAlignmentCenter;

    [self.contentView addSubview:self.image];
    [self.contentView addSubview:self.name];

    [_image mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.equalTo(self.contentView);
        make.top.equalTo(self.contentView).offset(30);
        make.bottom.equalTo(_name.mas_top).offset(-10);
    }];

    [_name mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.equalTo(self.contentView);
        make.top.equalTo(_image.mas_bottom).offset(10);
        make.bottom.equalTo(self.contentView).offset(-20);
    }];
}

寫好 layout 和 cell 后就可以用這兩個類來初始化我們的 collectionView 了:

//add in view did load
    self.layout = [[HorizontalFlowLayout alloc] init];

    CGRect rct = self.view.bounds;
    rct.size.height = 150;
    rct.origin.y = [[UIScreen mainScreen] bounds].size.height / 2.0 - rct.size.height;

    self.collectionView = [[UICollectionView alloc] initWithFrame:rct collectionViewLayout:_layout];
    self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.collectionView.showsHorizontalScrollIndicator = NO;
    self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal;

    [self.collectionView registerClass:[DotaCell class] forCellWithReuseIdentifier:NSStringFromClass([DotaCell class])];
    [self.collectionView setBackgroundColor:[UIColor clearColor]];
    [self.collectionView setDelegate:self];
    [self.collectionView setDataSource:self];

    [self.view addSubview:_collectionView];

添加 UICollectionViewDataSource 的代理方法,使其顯示數據。

 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return [self.itemTitles count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    DotaCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([DotaCell class]) forIndexPath:indexPath];

    cell.image.image = [UIImage imageNamed:[self.itemTitles objectAtIndex:indexPath.row]];
    cell.name.text = [self.itemTitles objectAtIndex:indexPath.row];

    return cell;
}

這樣程序就有了我們想要的初步效果:

圖片水平排放

但…效果的確很差!

下面要做的就是逐步完善效果,首先我們要讓兩排圖像變成一排去展示。那要怎么去做?首先,我們在初始化 collectionView 的地方設置了高度為150,所以圖片就擠在這個150的高度里盡可能的壓縮顯示。由于 collectionView 的尺寸已經設定,那么就剩 cell 的尺寸可以控制了。實現 CollectionViewFlowLayoutDelegate 的代理方法 sizeForItemAtIndexPath :

 - (CGSize)collectionView:(nonnull UICollectionView *)collectionView layout:(nonnull UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
    return CGSizeMake(64, collectionView.bounds.size.height); 
}

這里寬度64是圖片的尺寸,高度設置填滿 collectionView 的高度是為了防止上圖中兩行圖片擠壓的情況,所以直接讓一個 cell 的高度占滿整個容器。

這時候的效果好了很多,已經有點樣子了:

頂端圖片滑到中間

但這離我們最終的效果還差很遠,接下來我需要實現讓第一張圖片和最后一張圖片都能滑到屏幕中點的位置,這應該是很常見的效果,實現起來也很簡單。首先我們的一排 cell 都默認為頂端與 collectionView 的兩端對齊的, collectionView 的左右兩端與 viewController.view 也是對齊的,所以顯示的效果是,兩端的圖片都與屏幕對齊。知道這個關系就好辦了,直接設置 collectionView 與其父 view 的內間距即可。

依舊是實現 flowLayout 的代理方法:

 //Asks the delegate for the margins to apply to content in the specified section.安排初始位置
//使前后項都能居中顯示
- (UIEdgeInsets)collectionView:(nonnull UICollectionView *)collectionView layout:(nonnull UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {

    NSInteger itemCount = [self collectionView:collectionView numberOfItemsInSection:section];

    NSIndexPath *firstIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
    CGSize firstSize = [self collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:firstIndexPath];

    NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:itemCount - 1 inSection:section];
    CGSize lastSize = [self collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:lastIndexPath];

    return UIEdgeInsetsMake(0, (collectionView.bounds.size.width - firstSize.width) / 2,
                            0, (collectionView.bounds.size.width - lastSize.width) / 2);


}

效果如圖:

居中圖片放大顯示

接下來添加一個我們需要的特效,就是中間的圖片放大顯示,其余的縮小并且增加一層半透明效果。

在 FlowLayout 中有一個名為 layoutAttributesForElementsInRect 的方法,功能如其名,就是設置范圍內元素的 layout 屬性。對于這個效果,首先需要設置放大的比例,其次要根據圖片大小和間距來設定一個合適的觸發放大的區域寬度,當圖滑入這個區域就進行縮放。

 static CGFloat const ActiveDistance = 80;
static CGFloat const ScaleFactor = 0.2;
//這里設置放大范圍
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{

    NSArray *array = [super layoutAttributesForElementsInRect:rect];

    CGRect visibleRect = (CGRect){self.collectionView.contentOffset, self.collectionView.bounds.size};

    for (UICollectionViewLayoutAttributes *attributes in array) {
        //如果cell在屏幕上則進行縮放
        if (CGRectIntersectsRect(attributes.frame, rect)) {

            attributes.alpha = 0.5;

            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;//距離中點的距離
            CGFloat normalizedDistance = distance / ActiveDistance;

            if (ABS(distance) < ActiveDistance) {
                CGFloat zoom = 1 + ScaleFactor * (1 - ABS(normalizedDistance)); //放大漸變
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
                attributes.zIndex = 1;
                attributes.alpha = 1.0;
            }
        }
    }

    return array;
}

效果如下:

滑動校正

這時候幾乎完成了,但還差點東西,就是讓其在滾動停止的時候,離屏幕中間最近的 cell 自動矯正位置到中間。還是在 FlowLayout 添加該方法,具體說明我都寫到注釋里了:

 //scroll 停止對中間位置進行偏移量校正
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat offsetAdjustment = MAXFLOAT;
    ////  |-------[-------]-------|
    ////  |滑動偏移|可視區域 |剩余區域|
    //是整個collectionView在滑動偏移后的當前可見區域的中點
    CGFloat centerX = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
    //    CGFloat centerX = self.collectionView.center.x; //這個中點始終是屏幕中點
    //所以這里對collectionView的具體尺寸不太理解,輸出的是屏幕大小,但實際上寬度肯定超出屏幕的

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *layoutAttr in array) {
        CGFloat itemCenterX = layoutAttr.center.x;

        if (ABS(itemCenterX - centerX) < ABS(offsetAdjustment)) { // 找出最小的offset 也就是最中間的item 偏移量
            offsetAdjustment = itemCenterX - centerX;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

增加圖片點擊效果

最后 添加一個點擊cell 將其滾動到中間

在 viewcontroller 添加 CollectionViewDelegate 的代理方法

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    [self.collectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionNone];

    //滾動到中間
    [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}

封裝成控件

當我們把效果實現之后,就可以考慮將代碼優化一下,合到一個類里,減少書寫常量,增加接口,封裝成一個控件去使用。比如可以設定文字的顯示與隱藏接口,再比如增加適應各種尺寸的圖片等等。這個代碼就不放了,畢竟不難,有問題給我留言好了。

來自: http://www.wossoneri.com/2016/01/09/[iOS] UICollectionView實現圖片水平滾動/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!