最近在項目碰到一個需求:一個輪播視圖,頁面之間有一定間距,要求每次滾動時候,一次只能拖動一頁并且頁面居中。當(dāng)時粗略一想,應(yīng)該設(shè)置pagingEnabled,但是使用這個屬性后,scrollView每次翻頁就是它frame的寬度,貌似不能用(提前劇透下:確實要使用pagingEnabled,并結(jié)合clipsToBounds),再加上這種輪播視圖習(xí)慣使用collectionView,就決定使用collectionView了。
關(guān)鍵代碼如下:
-(UICollectionView *)collectionView{
if (!_collectionView) {
CommissionShopsLayout * flowLayout = [CommissionShopsLayout new];
_collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:flowLayout];
_collectionView.scrollsToTop = NO;
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.showsHorizontalScrollIndicator = NO;
_collectionView.showsVerticalScrollIndicator = NO;
_collectionView.dataSource = self;
_collectionView.delegate = self;
_collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
[_collectionView registerClass:[ImageClCell class] forCellWithReuseIdentifier:@"ImageClCell"];
}
return _collectionView;
}
@implementation CommissionShopsLayout
- (void)prepareLayout{
[super prepareLayout];
self.itemSize = CGSizeMake(SCREEN_WIDTH - 44, ceil((SCREEN_WIDTH - 44) / 1.655));
self.minimumLineSpacing = 10;
self.sectionInset = UIEdgeInsetsMake(0, 22, 0, 22);
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
}
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
return [super layoutAttributesForElementsInRect:rect];
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return YES;
}
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
///計算一下當(dāng)前的位置
CGFloat contentOffset = self.collectionView.contentOffset.x;
NSInteger currentIndex = MAX(0,(contentOffset - self.sectionInset.left) / (self.itemSize.width + self.minimumLineSpacing));
///計算原本應(yīng)該停留的位置
CGRect lastRect ;
lastRect.origin = proposedContentOffset;
lastRect.size = self.itemSize;
NSArray *array = [self.collectionView.collectionViewLayout layoutAttributesForElementsInRect:lastRect];
CGFloat startX = proposedContentOffset.x;
CGFloat adjustOffsetX = MAXFLOAT;
///居中吸附
for (UICollectionViewLayoutAttributes *attrs in array) {
CGFloat attrsX = CGRectGetMinX(attrs.frame);
CGFloat attrsW = CGRectGetWidth(attrs.frame) ;
if (startX - attrsX < attrsW/2) {
adjustOffsetX = -(startX - attrsX + self.sectionInset.left);
}else{
adjustOffsetX = attrsW - (startX - attrsX + self.sectionInset.left - self.minimumLineSpacing);
}
break ;
}
NSInteger calculateIndex = (proposedContentOffset.x + adjustOffsetX) / self.itemSize.width;
NSInteger finalIndex = calculateIndex <= currentIndex ? currentIndex : currentIndex + 1;
return CGPointMake((self.itemSize.width + self.minimumLineSpacing) * (finalIndex) , proposedContentOffset.y);
}
@end
這么寫貌似可以完成需求,一次也是滾一頁,每次翻頁也是居中,但是體驗很不好,設(shè)置了滑動的減速度為fast,如果每次拖動的距離很短,視圖停止?jié)L動的太快,跟原生的pagingEnabled的體驗相比還是不好。但是如果你只想滑動結(jié)束的時候頁面居中,這段代碼還是可以用的,hahaha!(直接返回proposedContentOffset.x + adjustOffsetX,proposedContentOffset.y))
扯遠(yuǎn)了!那怎么辦?回歸基礎(chǔ)!通過實現(xiàn)UIScrollView的代理方法自己實現(xiàn)分頁,可能是自己太菜了,雖然最終位置沒錯,但是體驗不好。這里介紹一下UIScrollViewDelegate幾個常用的方法
UIScrollView代理方法調(diào)用順序:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
scrollView滾動的時候就會調(diào)用這個方法
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
scrollView被拖拽的時候調(diào)用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
scrollView的拖拽將要結(jié)束的時候,通過targetContentOffset可以獲取到最后停留的那個位置,雖然沒有經(jīng)過測試,但是這個方法應(yīng)該和collectionView layout的那個代理方法是一樣的,
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
scrollView的拖拽結(jié)束的時候的調(diào)用。如果decelerate為yes,說明scrollView將進(jìn)入一個減速滑動的狀態(tài);如果為no,說明減速已經(jīng)停止,將會調(diào)用scrollViewDidEndDecelerating方法
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
scrollView將開始減速滑動,這個時候的decelerate為YES
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
scrollView結(jié)束滑動時候被調(diào)用。
再仔細(xì)分析一下需求(所以永遠(yuǎn)不要急著動手,多思考),還是應(yīng)該使用pagingEnabled,但是應(yīng)該結(jié)合一下其他屬性,后來發(fā)現(xiàn)使用clipsToBounds ,既然有思路了,那就干起來了吧
最終解決方法
思路大概設(shè)置pagingEnabled為yes,這樣一次只能翻一頁;設(shè)置clipsToBounds為NO,這樣就可以顯示溢出部分的內(nèi)容,但是scrollView的寬度要設(shè)置為一頁內(nèi)容的寬度 + 分頁之間的間距
這里我封裝了一個PageView視圖控件。
核心代碼如下:
.h文件
#import <UIKit/UIKit.h>
@protocol PageViewDelegate <NSObject>
-(void)didSelectPicWithIndexPath:(NSInteger)index;
-(void)roll_scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;
@end
@interface PageView : UIView
@property (nonatomic, assign) id<PageViewDelegate> delegate;
@property (nonatomic, assign) NSInteger selectIndex;//當(dāng)前頁面的下標(biāo),默認(rèn)為0
/**
@param frame 設(shè)置View大小
@param distance 設(shè)置Scroll距離View兩側(cè)距離
@param spacing 設(shè)置Scroll內(nèi)部 圖片間距,注意點(diǎn):distance + spacing / 2 = (pageView的寬度 - 單頁寬度)/ 2
@return 初始化返回值
*/
- (instancetype)initWithFrame:(CGRect)frame withDistanceToScrollView:(CGFloat)distance withSpacing:(CGFloat)spacing;
-(void)loadView:(NSArray *)data;
@end
.m文件
#import "PageView.h"
@interface PageView ()<UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView * scrollView;
@property (nonatomic, strong) NSArray * data;
@property (nonatomic, assign) CGFloat halfSpacing;
@end
@implementation PageView
#pragma mark - Lazy
-(UIScrollView *)scrollView{
if (!_scrollView) {
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
_scrollView.scrollsToTop = NO;
_scrollView.delegate = self;
_scrollView.pagingEnabled = YES;
_scrollView.clipsToBounds = NO;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];
tap.numberOfTapsRequired = 1;
tap.numberOfTouchesRequired = 1;
[_scrollView addGestureRecognizer:tap];
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.showsVerticalScrollIndicator = NO;
}
return _scrollView;
}
-(NSArray *)data{
if (!_data) {
_data = @[];
}
return _data;
}
#pragma mark - Init
- (instancetype)initWithFrame:(CGRect)frame withDistanceToScrollView:(CGFloat)distance withSpacing:(CGFloat)spacing{
if (self = [super initWithFrame:frame]) {
self.halfSpacing = spacing * 0.5;
self.selectIndex = 0;
[self addSubview:self.scrollView];
self.scrollView.frame = CGRectMake(distance, 0, self.frame.size.width - 2 * distance, self.frame.size.height);
}
return self;
}
#pragma mark - load view
-(void)loadView:(NSArray *)data{
self.data = data;
if (!data.count) return;
for (int i = 0; i < self.data.count; i++) {
for (UIView *subView in self.scrollView.subviews) {
if (subView.tag == 100 + i) {
[subView removeFromSuperview];
}
}
UIImageView *imageView = [[UIImageView alloc] init];
imageView.userInteractionEnabled = YES;
imageView.tag = 100 + i ;
/** 注意點(diǎn)
* 1. ScrollView的width應(yīng)該等于單頁寬度 + spacing
* 2. 假設(shè)單個頁面寬為 W 間距為 S, 想要居中,那么
* 單個頁面x值
* 0 -> 1 * halfSpacing ;
* 1 -> 3 * halfSpacing + W ;
* 2 -> 5 * halfSpacing + 2 * W ;
.
.
* i -> (2 * i + 1) * halfSpacing + 2 *(W - 2 * halfSpacing)
*/
imageView.frame = CGRectMake((2 * i + 1) * self.halfSpacing + i * (self.scrollView.frame.size.width - 2 * self.halfSpacing), 0, (self.scrollView.frame.size.width - 2 * self.halfSpacing), self.frame.size.height);
///這里我內(nèi)部寫死了,就是一張圖片而已
imageView.backgroundColor = [UIColor redColor];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.layer.masksToBounds = YES;
[imageView sd_setImageWithURL:[NSURL URLWithString:self.data[i]]];
[self.scrollView addSubview:imageView];
}
self.scrollView.contentOffset = CGPointMake((2 * _selectIndex) * self.halfSpacing + self.selectIndex * (self.scrollView.frame.size.width - 2 * self.halfSpacing), 0);
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * self.data.count, 0);
}
#pragma mark - Action
-(void)tapAction:(UITapGestureRecognizer *)tap{
//點(diǎn)擊后的代理
if ([_delegate respondsToSelector:@selector(didSelectPicWithIndexPath:)]) {
[_delegate didSelectPicWithIndexPath:(self.scrollView.contentOffset.x / self.scrollView.frame.size.width)];
}
}
#pragma mark - UIScrollViewDelegate
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if ([_delegate respondsToSelector:@selector(roll_scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
[_delegate roll_scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
}
}
@end
swift 3.0代碼:
import Foundation
@objc protocol PageViewDelegate{
@objc optional func didSelectPicWithIndexPath(_ index:Int)
@objc optional func page_scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
}
class PageView : UIView {
var selectIndex : Int = 0
weak var pageDelegate : PageViewDelegate?
fileprivate var halfSpacing : CGFloat = 0.0
fileprivate var data : [Any] = [Any]()
fileprivate lazy var scrollView : UIScrollView = {
let tmp = UIScrollView(frame:CGRect.zero)
tmp.delegate = self
tmp.isPagingEnabled = true
tmp.clipsToBounds = false
tmp.showsVerticalScrollIndicator = false
tmp.showsHorizontalScrollIndicator = false
let tap = UITapGestureRecognizer(target: self, action:#selector(tapAction(tap:)))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
tmp.addGestureRecognizer(tap)
return tmp
}()
init(frame:CGRect, withDistanceToScrollView distance:CGFloat, withSpacing spacing:CGFloat){
super.init(frame: frame)
self.halfSpacing = spacing * 0.5
self.addSubview(self.scrollView)
self.scrollView.frame = CGRect(x:distance,y:0,width:self.frame.size.width - 2 * distance,height:self.frame.size.height)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func loadView(_ data:[Any]) {
self.data = data
if data.count == 0{
return
}
for i in 0..<data.count {
for subView in self.scrollView.subviews {
if subView.tag == 100 + i {
subView.removeFromSuperview()
}
}
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.tag = 100 + i
imageView.backgroundColor = UIColor.red
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
self.scrollView.addSubview(imageView)
let x : CGFloat = CGFloat(2 * i + 1) * self.halfSpacing + CGFloat(i) * (self.scrollView.frame.size.width - 2 * self.halfSpacing)
let w: CGFloat = (self.scrollView.frame.size.width - 2 * self.halfSpacing)
imageView.frame = CGRect(x:x,y:0,width:w,height:self.scrollView.frame.size.height)
}
let pointX : CGFloat = CGFloat(2 * self.selectIndex) * self.halfSpacing + CGFloat(self.selectIndex) * (self.scrollView.frame.size.width - 2 * self.halfSpacing)
self.scrollView.contentOffset = CGPoint(x: pointX, y: 0)
self.scrollView.contentSize = CGSize(width:self.scrollView.frame.size.width * CGFloat(self.data.count),height:0)
}
///Action
func tapAction(tap:UITapGestureRecognizer) {
let index = Int(self.scrollView.contentOffset.x / self.scrollView.frame.size.width)
self.pageDelegate?.didSelectPicWithIndexPath?(index)
}
}
extension PageView : UIScrollViewDelegate{
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
self.pageDelegate?.page_scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
}
這里關(guān)于UIScrollViewDelegate的代理,主要是我每拖拽一次,其他控件就會對應(yīng)改變,只是處理一些邏輯問題,但是為什么是在這個方法調(diào)用,是因為scrollView在一次減速動畫還沒有結(jié)束的時候再次拖拽scrollView,didEndDecelerating這個代理方法是不會被調(diào)用的。
另外單頁圖片的frame和scrollView寬度設(shè)置只是一個數(shù)學(xué)問題了,就不再詳細(xì)介紹了,畢竟程序猿都是數(shù)學(xué)寶寶,要是不懂那只能轉(zhuǎn)行啦