AutoLayout对于UIScrollView的支持并不是很好。问题出在我们以为有一个contentView的属性,我们可以通过对它的布局来实现滚动的效果。但是并不存在这样的属性。苹果对此有两种解决方案,一种是用纯AutoLayout的方式解决,但是效果并不太好。另一种是自定义一个UIScrollView子类,并采用传统手动布局的方式自定义出一个contentView,避免了AutoLayout对它的无奈。
实现效果
(注:实现轮播图还有比这更好的方法,比如UICollectionView,但本文的主要目的是说明自动布局在滚动视图中的使用。)
思路
- 自定义的UIScrollView从视图层次上大体分为三层:
1.1. 最下面的是UIScrollView本身,AutoLayout可以很好的支持这一层。
1.2. 中间是自定义ContentView,用传统手动的的方式布局它,计算它的宽高。
1.3. 在ContentView之上,就是实际看到的视图。AutoLayout可以支持这一层。 - 重写添加子视图方法,把子视图添加到自定义ContentView上。
具体实现
1. 自定义UIScrollView
创建新类AutoLayoutScrollView,继承自UIScrollView。
// 本部分代码写在AutoLayoutScrollView.h文件中
@property (nonatomic, readonly) UIView *customContentView;
// 本部分代码写在AutoLayoutScrollView.m文件中
- (void) _commonSetupAutoLayoutScrollView {
self.showsHorizontalScrollIndicator = YES;
self.showsVerticalScrollIndicator = NO;
self.bounces = NO;
}
// 重写两个初始化方法
- (instancetype) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return self;
}
// 创建自定义content view
_customContentView = [[UIView alloc] initWithFrame: (CGRect){
.size = frame.size
}];
[self addSubview:_customContentView];
[self _commonSetupAutoLayoutScrollView];
return self;
}
- (instancetype) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder: aDecoder];
if (!self) {
return self;
}
// 创建自定义content view
_customContentView = [[UIView alloc] initWithFrame: (CGRect){
.size = self.frame.size
}];
[self addSubview:_customContentView];
[self _commonSetupAutoLayoutScrollView];
return self;
}
// 重写addSubView:方法,让新视图添加到自定义content view上。
- (void) addSubview:(UIView *)view {
if (view != _customContentView) {
[_customContentView addSubview:view];
} else {
[super addSubview: _customContentView];
}
}
// 当内容尺寸改变时,调整自定义content view。
- (void) setContentSize:(CGSize)contentSize {
_customContentView.frame = (CGRect){
.size = contentSize
};
[super setContentSize:contentSize];
}
2. 创建一个用于本例的特定滚动视图类
继承刚创建的AutoLayoutScrollView类,创建新类PagedImageScrollView。
// 本部分代码写在PagedImageScrollView.h中
@property (nonatomic, assign) NSInteger pageNumber;
- (void) addView: (UIView *) view;
// 本部分代码写在PagedImageScrollView.m文件中
- (void) updateContentSize {
CGFloat width = self.frame.size.width * views.count;
self.contentSize = CGSizeMake(width, self.frame.size.height);
}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"bounds"]) {
[self updateContentSize];
}
}
- (instancetype) initWithFrame:(CGRect)frame {
self = [super initWithFrame: frame];
if (!self) {
return self;
}
self.backgroundColor = [UIColor blackColor];
self.pagingEnabled = YES;
self.contentOffset = CGPointZero;
self.contentSize = CGSizeZero;
_pageNumber = 0;
[self addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew context:NULL];
return self;
}
#pragma mark - Layout
- (void) updateConstraints {
[super updateConstraints];
if (!views.count) {
return;
}
for (int i = 0; i < views.count; i++) {
UIView *view = views[i];
if (i == 0) {
// 第一个视图
[view mas_remakeConstraints:^(MASConstraintMaker *make) {
// 中点水平对齐,将视图与滚动视图的宽度设为相同。
make.centerY.mas_equalTo(view.superview.mas_centerY).priority(MASLayoutPriorityRequired);
make.width.mas_equalTo(self).priorityMedium();
make.left.mas_equalTo(view.superview.mas_left).priority(MASLayoutPriorityRequired);
}];
} else if (i == views.count - 1) {
// 最后一个视图
UIView *previousView = views[i - 1];
[view mas_remakeConstraints:^(MASConstraintMaker *make) {
// 中点水平对齐,将视图与滚动视图的宽度设为相同。
make.centerY.mas_equalTo(view.superview.mas_centerY).priority(MASLayoutPriorityRequired);
make.width.mas_equalTo(self).priorityMedium();
make.right.mas_equalTo(view.superview.mas_right).priority(MASLayoutPriorityRequired);
make.left.mas_equalTo(previousView.mas_right).priorityHigh();
}];
} else {
// 中间的视图
UIView *previousView = views[i - 1];
[view mas_remakeConstraints:^(MASConstraintMaker *make) {
// 中点水平对齐,将视图与滚动视图的宽度设为相同。
make.centerY.mas_equalTo(view.superview.mas_centerY).priority(MASLayoutPriorityRequired);
make.width.mas_equalTo(self).priorityMedium();
make.left.mas_equalTo(previousView.mas_right).priorityHigh();
}];
}
}
// 调整尺寸
[self updateContentSize];
[self setContentOffset:CGPointMake((CGFloat)_pageNumber * self.frame.size.width, 0)];
}
- (void) addView:(UIView *)view {
if (!views) {
views = [NSMutableArray array];
}
[views addObject:view];
[self.customContentView addSubview:view];
[self setNeedsUpdateConstraints]; // 调用updateConstraints
}
3. 最后在ViewControll里进行调用
// 本部分代码写在ViewController.m文件中
- (UIImageView *) imageView: (NSString *) source {
UIImage *image = [UIImage imageNamed:source];
if (!image) {
return nil;
}
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.contentMode = UIViewContentModeScaleToFill;
// 限制宽高比
CGFloat naturalAspect = image.size.width / image.size.height;
[imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(imageView.mas_height).multipliedBy(naturalAspect).priority(MASLayoutPriorityRequired);
}];
// 减少宽高的固定数值的优先级
[imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical];
[imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
return imageView;
}
#pragma mark - UIScrollView Delegate
- (void) scrollViewDidEndDecelerating:(UIScrollView *)sender {
// 在滚动后更新page control的值
CGFloat distance = scrollView.contentOffset.x / sender.contentSize.width;
NSInteger page = distance * pageControl.numberOfPages;
pageControl.currentPage = page;
scrollView.pageNumber = page;
}
#pragma mark - Creation
- (void) creationView {
self.view.backgroundColor = [UIColor blackColor];
// 准备scrollView
scrollView = [[PagedImageScrollView alloc] init];
scrollView.delegate = self;
[self.view addSubview:scrollView];
// 添加scrollView的子视图
NSMutableArray *views = [NSMutableArray arrayWithCapacity:0];
for (NSString *string in @[@"ferret.jpg", @"bear.jpg", @"pronghorn.jpg"]) {
UIView *view = [self imageView:string];
if (!view) {
continue;
}
[views addObject:view];
[scrollView addView:view];
}
// 添加page control
pageControl = [[UIPageControl alloc] init];
pageControl.numberOfPages = views.count;
pageControl.currentPage = 0;
pageControl.backgroundColor = [UIColor blackColor];
pageControl.pageIndicatorTintColor = [UIColor grayColor];
pageControl.currentPageIndicatorTintColor = [UIColor whiteColor];
pageControl.userInteractionEnabled = NO;
[self.view addSubview:pageControl];
// page control 布局
[pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(64).priority(950);
make.height.mas_equalTo(30).priority(950);
make.leading.mas_equalTo(scrollView.mas_leading).priority(MASLayoutPriorityRequired);
make.trailing.mas_equalTo(scrollView.mas_trailing).priority(MASLayoutPriorityRequired);
}];
// scorll view 布局
[scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.view.mas_centerX).priority(MASLayoutPriorityRequired);
make.top.mas_equalTo(pageControl.mas_bottom).priority(950);
make.width.mas_equalTo(scrollView.mas_height).priority(MASLayoutPriorityRequired);
make.centerY.mas_equalTo(self.view.mas_centerY).priority(MASLayoutPriorityDefaultMedium);
make.leading.mas_equalTo(self.view).priorityHigh();
make.trailing.mas_equalTo(self.view).priorityHigh();
make.bottom.mas_equalTo(self.view).priorityHigh();
}];
NSLog(@"scroll view width: %f, height: %f", scrollView.frame.size.width, scrollView.frame.size.height);
NSLog(@"scroll content view width: %f, height: %f", scrollView.customContentView.frame.size.width, scrollView.customContentView.frame.size.height);
}