1. 程式人生 > >通過UICollectionView實現橫向滾動照片效果

通過UICollectionView實現橫向滾動照片效果

效果圖

效果截圖

思路

1. 介面搭建

介面的搭建十分簡單,採用UICollectionView和自定義cell進行搭建即可。

// ViewController.m

// 下面使用到的巨集和全域性變數
#define ScreenW [UIScreen mainScreen].bounds.size.width
#define ScreenH [UIScreen mainScreen].bounds.size.height
static NSString *const cellID = @"cellID";

// 建立collectionView的程式碼
- (void)setupCollectionView
{
    // 使用系統自帶的流佈局(繼承自UICollectionViewLayout)
UICollectionViewFlowLayout *layout = ({ UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; // 每個cell的大小 layout.itemSize = CGSizeMake(180, 180); // 橫向滾動 layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; // cell間的間距
layout.minimumLineSpacing = 40; //第一個cell和最後一個cell居中顯示(這裡我的Demo裡忘記改了我用的是160,最後微調資料cell的大小是180) CGFloat margin = (ScreenW - 180) * 0.5; layout.sectionInset = UIEdgeInsetsMake(0, margin, 0, margin); layout; }); // 使用UICollectionView必須設定UICollectionViewLayout屬性
UICollectionView *collectionView = ({ UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; collectionView.center = self.view.center; collectionView.bounds = CGRectMake(0, 0, ScreenW, 200); collectionView.backgroundColor = [UIColor brownColor]; // 這裡千萬記得在interface哪裡寫<UICollectionViewDataSource>!!! collectionView.dataSource = self; [collectionView setShowsHorizontalScrollIndicator:NO]; [self.view addSubview:collectionView]; collectionView; }); // 實現註冊cell,其中PhotoCell是我自定義的cell,繼承自UICollectionViewCell UINib *collectionNib = [UINib nibWithNibName:NSStringFromClass([PhotoCell class]) bundle:nil]; [collectionView registerNib:collectionNib forCellWithReuseIdentifier:cellID]; } // UICollectionViewCellDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return 10; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath]; // 圖片名是 0 ~ 9 cell.imageName = [NSString stringWithFormat:@"%ld", (long)indexPath.row]; return cell; }
// 介面是一個xib檔案,在cell裡拖了個ImageView,約束上下左右都是10
// 圖片名是數字 0 ~ 9

// PhotoCell.h
@property (nonatomic, strong) NSString *imageName;

// PhotoCell.m
@interface PhotoCell ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation PhotoCell

- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
}

- (void)setImageName:(NSString *)imageName
{
    _imageName = imageName;

    self.imageView.image = [UIImage imageNamed:imageName];
}

到這裡最基礎的效果就實現完了,一組大小相等的圖片cell。

2.大小變化已經居中效果實現

由於系統的UICollectionViewFlowLayout無法實現我想要的效果,因此我重寫下該類中的某些方法。
在UICollectionViewLayout中有這樣兩句註釋:
1. Methods in this class are meant to be overridden and will be called by its collection view to gather layout information.
在這個類中的方法意味著被重寫(overridden),並且將要被它的 collection view 呼叫,用於收集佈局資訊
2. To get the truth on the current state of the collection view, call methods on UICollectionView rather than these.
要獲取 collection view 的當前狀態的真相,呼叫UICollectionView上的方法,而不是這些

其中有一點需要解釋下,collectionView的bounds的x和y實際上就是collectionView的內容檢視的 x和y。關於這點,請看我寫的一篇博文的解釋:iOS bounds學習筆記以及仿寫UIScrollView的部分功能

// MyFlowLayout.h
#import <UIKit/UIKit.h>

// 注意!繼承自UICollectionViewFlowLayout,因為它繼承自UICollectionViewLayout。
@interface TWLayout : UICollectionViewFlowLayout

// MyFlowLayout.m
/**
    * The default implementation of this method returns NO. Subclasses can override it and return an appropriate value based on whether changes in the bounds of the collection view require changes to the layout of cells and supplementary views.
      此方法的預設實現返回NO。 子類可以覆蓋它,並根據 collection view 的 bounds 中的更改是否需要更改 cells 和 supplementary views(補充檢視) 的佈局返回適當的值。

    * If the bounds of the collection view change and this method returns YES, the collection view invalidates the layout by calling the invalidateLayoutWithContext: method.
      如果 collection view 的 bounds 更改並且此方法返回YES,則 collection view 通過呼叫invalidateLayoutWithContext:方法使佈局更新。

 @param newBounds The new bounds of the collection view.
 @return YES if the collection view requires a layout update or NO if the layout does not need to change.
 */
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}


/**
 * Returns the layout attributes for all of the cells and views in the specified rectangle.
   返回指定矩形中所有cells和views的佈局屬性。

 @param rect * The rectangle (specified in the collection view’s coordinate system) containing the target views.
               包含目標檢視的矩形(在集合檢視的座標系中指定)。

 @return * An array of UICollectionViewLayoutAttributes objects representing the layout information for the cells and views. The default implementation returns nil.
           UICollectionViewLayoutAttributes物件陣列,表示cell和view的佈局資訊。預設實現返回nil。
 */
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // 獲取collectionView的寬頻
    CGFloat collectionW = self.collectionView.bounds.size.width;

    // 獲取佈局屬性陣列
    NSArray<UICollectionViewLayoutAttributes *> *attrs = [super layoutAttributesForElementsInRect:self.collectionView.bounds];
    for (int i = 0; i < attrs.count; i++) {
        UICollectionViewLayoutAttributes *attr = attrs[i];

        //每個顯示的cell距離中心距離
        CGFloat margin = fabs((attr.center.x - self.collectionView.contentOffset.x) - collectionW * 0.5);

        // 縮放比例:(margin / (collectionW * 0.5))得出的結論相當於 0 ~ 1。而我們需要它的縮放比例是 1 ~ 0.65,這樣就是 (1 - 0)~(1 - 0.35)
        CGFloat scale = 1 - (margin / (collectionW * 0.5)) * 0.35;

        attr.transform = CGAffineTransformMakeScale(scale, scale);
    }

    return attrs;
}


/**
 * Returns the point at which to stop scrolling.
 * 關於這個方法,最終的偏移量,並不是由手指滑動過的偏移量決定的。如果手指滑動比較快,手指滑動過後,檢視還會多滾動一段距離;如果手指滑動緩慢,手指滑到何處,就停到何處。

 @param proposedContentOffset 建議的點(在集合檢視的內容檢視的座標空間中)用於可見內容的左上角。 這表示集合檢視計算為在動畫結束時最可能使用的值。
 @param velocity 沿著水平軸和垂直軸的當前滾動速度。 該值以每秒點數為單位。
 @return 要使用的內容偏移量。 此方法的預設實現返回proposedContentOffset引數中的值。
 */
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // 獲取collectionView的寬度
    CGFloat collectionW = self.collectionView.bounds.size.width;
    // 獲取當前的內容偏移量
    CGPoint targetP = proposedContentOffset;

    // 獲取顯示cell的佈局屬性陣列,橫向滾動,所以只用考慮橫向的x和width,縱向不用考慮
    NSArray *attrs = [super layoutAttributesForElementsInRect:CGRectMake(targetP.x, 0, collectionW, MAXFLOAT)];

    // 距離中心點最近的cell的間距(中間那個cell距離最近,值可正可負)
    CGFloat minSpacing = MAXFLOAT;
    for (UICollectionViewLayoutAttributes *attr in attrs) {
        // 距離中心點的偏移量
        CGFloat centerOffsetX = attr.center.x - targetP.x - collectionW * 0.5;
        // fabs():CGFloat絕對值
        if (fabs(centerOffsetX) < fabs(minSpacing)) {
            minSpacing = centerOffsetX;
        }
    }
    targetP.x += minSpacing;

    return targetP;
}

最後,記得把 UICollectionViewFlowLayout 改成你自定義的FlowLayout物件!!