最近在项目碰到一个需求:一个轮播视图,页面之间有一定间距,要求每次滚动时候,一次只能拖动一页并且页面居中。当时粗略一想,应该设置pagingEnabled,但是使用这个属性后,scrollView每次翻页就是它frame的宽度,貌似不能用(提前剧透下:确实要使用pagingEnabled,并结合clipsToBounds),再加上这种轮播视图习惯使用collectionView,就决定使用collectionView了。
关键代码如下:
-(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
{
///计算一下当前的位置
CGFloat contentOffset = self.collectionView.contentOffset.x;
NSInteger currentIndex = MAX(0,(contentOffset - self.sectionInset.left) / (self.itemSize.width + self.minimumLineSpacing));
///计算原本应该停留的位置
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
这么写貌似可以完成需求,一次也是滚一页,每次翻页也是居中,但是体验很不好,设置了滑动的减速度为fast,如果每次拖动的距离很短,视图停止滚动的太快,跟原生的pagingEnabled的体验相比还是不好。但是如果你只想滑动结束的时候页面居中,这段代码还是可以用的,hahaha!(直接返回proposedContentOffset.x + adjustOffsetX,proposedContentOffset.y))
扯远了!那怎么办?回归基础!通过实现UIScrollView的代理方法自己实现分页,可能是自己太菜了,虽然最终位置没错,但是体验不好。这里介绍一下UIScrollViewDelegate几个常用的方法
UIScrollView代理方法调用顺序:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
scrollView滚动的时候就会调用这个方法
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
scrollView被拖拽的时候调用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
scrollView的拖拽将要结束的时候,通过targetContentOffset可以获取到最后停留的那个位置,虽然没有经过测试,但是这个方法应该和collectionView layout的那个代理方法是一样的,
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
scrollView的拖拽结束的时候的调用。如果decelerate为yes,说明scrollView将进入一个减速滑动的状态;如果为no,说明减速已经停止,将会调用scrollViewDidEndDecelerating方法
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
scrollView将开始减速滑动,这个时候的decelerate为YES
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
scrollView结束滑动时候被调用。
再仔细分析一下需求(所以永远不要急着动手,多思考),还是应该使用pagingEnabled,但是应该结合一下其他属性,后来发现使用clipsToBounds ,既然有思路了,那就干起来了吧
最终解决方法
思路大概设置pagingEnabled为yes,这样一次只能翻一页;设置clipsToBounds为NO,这样就可以显示溢出部分的内容,但是scrollView的宽度要设置为一页内容的宽度 + 分页之间的间距
这里我封装了一个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;//当前页面的下标,默认为0
/**
@param frame 设置View大小
@param distance 设置Scroll距离View两侧距离
@param spacing 设置Scroll内部 图片间距,注意点: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 ;
/** 注意点
* 1. ScrollView的width应该等于单页宽度 + spacing
* 2. 假设单个页面宽为 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);
///这里我内部写死了,就是一张图片而已
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{
//点击后的代理
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)
}
}
这里关于UIScrollViewDelegate的代理,主要是我每拖拽一次,其他控件就会对应改变,只是处理一些逻辑问题,但是为什么是在这个方法调用,是因为scrollView在一次减速动画还没有结束的时候再次拖拽scrollView,didEndDecelerating这个代理方法是不会被调用的。
另外单页图片的frame和scrollView宽度设置只是一个数学问题了,就不再详细介绍了,毕竟程序猿都是数学宝宝,要是不懂那只能转行啦