UIStackView 更改项目间距作为堆栈视图更改大小
UIStackView change item spacing as stack view changes sizes
概览
我有一个堆栈视图,其中包含多个圆形视图。圈子视图可以是图像(如个人资料图片)或任何东西。如果堆栈视图的大小对于子视图来说太小,这些视图应该能够重叠。如果堆栈视图对于子视图来说太大,视图应该展开。此外,即使堆栈视图的大小没有改变,也可以动态添加或删除子视图。
例如,在下图中,顶部堆栈视图具有这些重叠的圆形视图,并且那里一切正常(框架恰好是子视图视图的大小)。但随后,查看第二个堆栈视图,在添加更多视图后,第一个视图被压缩。但我想要发生的是让所有视图重叠更多一点并且不压缩任何视图。
问题
实现此行为的最佳方法是什么?我应该覆盖 layoutSubviews
,就像我在下一节中提议的那样,还是有更好的方法来实现它?同样,如果堆栈视图对它们来说太大,我只希望视图展开,或者如果堆栈视图太窄,它们可以相互重叠。堆栈视图可以随时更改大小,也可以随时添加或删除排列的子视图,所有这些都会导致重新计算视图间距。
建议的解决方案
我正在考虑覆盖堆栈视图的 layoutSubviews
方法,然后以某种方式测量所有视图,将这些宽度加在一起,然后是当前存在的间距(我想通过每个排列子视图并查看该子视图的间距)。因此,如果项目实际展开,则重叠的间距为负间距或正间距。然后,我会将该宽度与 layoutSubviews 中的框架进行比较,如果它太宽,那么我会减小间距。否则,如果视图没有占据整个堆栈视图,那么我会增加它们的间距。
这是我的代码和layoutSubviews
中提出的算法。
代码
MyShelf.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
@interface MyShelf : UIStackView
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfItemShape itemShape;
@property (strong, nonatomic) UIColor *itemBorderColor;
@property (assign, nonatomic) CGFloat itemBorderWidth;
@property (assign, nonatomic) CGFloat preferredMinimumSpacing;
@property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
@interface MyShelf ()
@property (strong, nonatomic) UIStackView *stackView;
@end
@implementation MyShelf
#pragma mark - Initializing the View
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
self.spacing = -10;
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.distribution = UIStackViewDistributionFillProportionally;
self.itemSize = CGSizeZero;
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
}
- (void)layoutSubviews {
//if the new frame is different from the old frame
//if the size of the items in the stack view is too large, reduce the spacing down to a minimum of preferredMinimumSpacing
//else if the size of the items in the stack view is too small, increase the spacing up to a maximum of preferredMaximumSpacing
//otherwise keep the spacing as-is
[super layoutSubviews];
}
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
if (animated) {
//prepare the view to be initially hidden so it can be animated in
view.alpha = 0.0;
view.hidden = YES;
[super insertArrangedSubview:view atIndex:stackIndex];
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 1.0; view.hidden = NO; }
completion:nil];
} else {
[super insertArrangedSubview:view atIndex:stackIndex];
}
[self reorderArrangedSubviews];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.arrangedSubviews.count animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 0.0; view.hidden = YES; }
completion:^(BOOL finished) { [super removeArrangedSubview:view]; }];
} else {
[super removeArrangedSubview:view];
}
}
- (void)reorderArrangedSubviews {
for (__kindof UIView *arrangedSubview in self.arrangedSubviews) {
[self sendSubviewToBack:arrangedSubview];
}
}
@end
要求
如果视图是固定宽度
对于这种情况,包含这些圆形子视图的视图是固定宽度的。可能是它有一个宽度约束来指定它的宽度点数,或者它可能受到其他视图的约束,因此它的宽度是预先确定的。
在这种情况下,子视图应该彼此相邻排列,直到它们不再适合框架,此时它们开始折叠(项目之间的负间距)。
如果视图是灵活宽度
对于这种情况,包含圆形子视图的视图没有指定宽度。相反,它的宽度由内容的宽度决定。所以它应该继续增长,直到它不能再增长,在这一点上,子视图开始重叠。
调整distribution
属性即可。
self.distribution = UIStackViewDistributionEqualCentering;
此外,UIStackView
总体思路是在您的圆形视图上使用 centerX 约束 - 我将它们称为 ShelfItem
,并将它们约束为“不可见的定位视图”。
之所以这样做,是因为当项目的 centerX 位于前缘(或后缘)时,它的一半将延伸到定位视图的左侧或右侧。
考虑将宽度分成相等的部分(所有值均以 % 为单位)...
如果我们有 3 件商品,我们需要 2 等份。要获得百分比间距,我们使用 1.0 / (numItems - 1)
:
4 项,我们需要 3 等份:
5 项,我们需要 4 等份:
对于 6 项,我们需要 5 等份:
因此,通过使“项目”视图成为“定位”视图的子视图,我们可以像这样循环并设置它们的 centerX 约束:
UIView *thisItem;
CGFloat pct = 1.0 / (CGFloat)([subviews count] - 1);
for (int i = 0; i < subviews.count; i++) {
thisItem = subviews[i];
CGFloat thisPCT = pct * i;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
c.active = YES;
}
这不是相当那么简单...
首先,auto-layout 不喜欢 multiplier: 0.0
... 所以 left-most 项目需要有 centerX 等于定位视图 Leading。
第二件事是,当有足够的空间时,您的布局要求项目视图 left-aligned,而不是均匀分布。
为了实现这一点,我们将使每个项目视图的 centerX lessThanOrEqualTo
成为前一个项目的 centerX + itemWidth...并且我们将为“百分比”约束赋予 less-than-required 优先级。
所以,每次我们添加(或删除)一个项目时,我们都会调用一个方法来更新 centerX 约束......它看起来像这样:
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
最后一个任务是添加一个“框架”视图,它将匹配 laid-out 项视图的边界。
这是一个完整的例子...
ShelfItem.h - 带有标签的简单圆形视图
#import <UIKit/UIKit.h>
@interface ShelfItem : UIView
@property (strong, nonatomic) UILabel *label;
@end
ShelfItem.m
#import "ShelfItem.h"
@implementation ShelfItem
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
self.backgroundColor = UIColor.whiteColor;
_label = [UILabel new];
_label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight];
_label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_label];
[_label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES;
[_label.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES;
self.layer.borderColor = UIColor.blueColor.CGColor;
self.layer.borderWidth = 1.0;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.bounds.size.height * 0.5;
}
@end
ShelfView.h - 我们的视图完成所有工作
#import <UIKit/UIKit.h>
@interface ShelfView : UIView
- (void)addItem:(NSInteger)n;
- (void)removeItem;
@end
ShelfView.m
#import "ShelfView.h"
#import "ShelfItem.h"
@interface ShelfView () {
UIView *positionView;
UIView *framingView;
CGFloat itemWidth;
NSLayoutConstraint *framingViewTrailingConstraint;
}
@end
@implementation ShelfView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
itemWidth = 60.0;
// framingView will match the bounds of the items
// it will not be their superView, but will look like it
framingView = [UIView new];
framingView.translatesAutoresizingMaskIntoConstraints = NO;
framingView.backgroundColor = UIColor.systemYellowColor;
[self addSubview:framingView];
// positionView is used for the item position constraints
// but is not seen
positionView = [UIView new];
positionView.translatesAutoresizingMaskIntoConstraints = NO;
positionView.backgroundColor = UIColor.clearColor;
[self addSubview:positionView];
// initialize framingView trailing constraint -- it will be updated in updatePositions
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:positionView.leadingAnchor];
framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
[NSLayoutConstraint activateConstraints:@[
// positioning view is at vertical center with no height
[positionView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[positionView.heightAnchor constraintEqualToConstant:0.0],
// leading and trailing are 1/2 the item width
[positionView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:itemWidth * 0.5],
[positionView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-itemWidth * 0.5],
// framing view leading is at positioning view leading minus 1/2 item width
[framingView.leadingAnchor constraintEqualToAnchor:positionView.leadingAnchor constant:-itemWidth * 0.5],
// constrained top and bottom
[framingView.topAnchor constraintEqualToAnchor:self.topAnchor],
[framingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
]];
}
- (void)updatePositions {
if ([positionView.subviews count] == 0) {
// no items, so all we have to do is update the framing view
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:self.leadingAnchor];
framingViewTrailingConstraint.active = YES;
return;
}
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
// update the trailing anchor of the framing view to the last shelf item
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:thisItem.trailingAnchor];
framingViewTrailingConstraint.active = YES;
}
- (void)addItem:(NSInteger)n {
// create a new shelf item
ShelfItem *v = [ShelfItem new];
v.translatesAutoresizingMaskIntoConstraints = NO;
v.label.text = [NSString stringWithFormat:@"%ld", (long)n];
// add it as a subview of positionView
// at index Zero (so it will be underneath existing items)
[positionView insertSubview:v atIndex:0];
// width and height
[v.widthAnchor constraintEqualToConstant:itemWidth].active = YES;
[v.heightAnchor constraintEqualToAnchor:v.widthAnchor].active = YES;
// vertically centered on positionView
[v.centerYAnchor constraintEqualToAnchor:positionView.centerYAnchor constant:0.0].active = YES;
// update all shelf items
[self updatePositions];
}
- (void)removeItem {
// remove the last-added item
[positionView.subviews[0] removeFromSuperview];
// update all shelf items
[self updatePositions];
}
@end
ViewController.h - 带有两个 ShelfView 和添加/删除按钮的控制器:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
ViewController.m
#import "ViewController.h"
#import "ShelfView.h"
@interface ViewController ()
{
ShelfView *shelfViewA;
ShelfView *shelfViewB;
NSInteger counter;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
counter = 0;
// top shelf view has systemYellow background, so we see its
// full width all the time
shelfViewA = [ShelfView new];
shelfViewA.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewA.backgroundColor = UIColor.systemYellowColor;
[self.view addSubview:shelfViewA];
// second shelf view has clear background, so we only see its
// framing view width when items are added
shelfViewB = [ShelfView new];
shelfViewB.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewB.backgroundColor = UIColor.clearColor;
[self.view addSubview:shelfViewB];
UIButton *addBtn = [UIButton new];
addBtn.translatesAutoresizingMaskIntoConstraints = NO;
addBtn.backgroundColor = UIColor.systemGreenColor;
[addBtn setTitle:@"Add" forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[addBtn addTarget:self action:@selector(addTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:addBtn];
UIButton *removeBtn = [UIButton new];
removeBtn.translatesAutoresizingMaskIntoConstraints = NO;
removeBtn.backgroundColor = UIColor.systemGreenColor;
[removeBtn setTitle:@"Remove" forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[removeBtn addTarget:self action:@selector(removeTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:removeBtn];
UILabel *info = [UILabel new];
info.translatesAutoresizingMaskIntoConstraints = NO;
info.backgroundColor = [UIColor colorWithWhite:0.90 alpha:1.0];
info.textAlignment = NSTextAlignmentCenter;
info.numberOfLines = 0;
info.text = @"Shelf View Width\n60-pts on each side.";
[self.view addSubview:info];
// respect safeArea
UILayoutGuide *g = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[shelfViewA.topAnchor constraintEqualToAnchor:g.topAnchor constant:60.0],
[shelfViewA.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewA.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewA.heightAnchor constraintEqualToConstant:60.0],
[info.topAnchor constraintEqualToAnchor:shelfViewA.bottomAnchor constant:8.0],
[info.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[info.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.topAnchor constraintEqualToAnchor:info.bottomAnchor constant:8.0],
[shelfViewB.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewB.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.heightAnchor constraintEqualToConstant:60.0],
[addBtn.topAnchor constraintEqualToAnchor:shelfViewB.bottomAnchor constant:20.0],
[addBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[addBtn.widthAnchor constraintEqualToConstant:200.0],
[removeBtn.topAnchor constraintEqualToAnchor:addBtn.bottomAnchor constant:20.0],
[removeBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[removeBtn.widthAnchor constraintEqualToConstant:200.0],
]];
}
- (void)addTapped {
++counter;
[shelfViewA addItem:counter];
[shelfViewB addItem:counter];
}
- (void)removeTapped {
if (counter > 0) {
--counter;
[shelfViewA removeItem];
[shelfViewB removeItem];
}
}
@end
运行 这给了我们这个 - 注意“顶部”货架视图显示其框架,“底部”货架视图仅显示“框架视图”:
并且当视图改变大小时,例如在设备旋转时,我们不需要做任何事情......auto-layout 为我们处理它:
概览
我有一个堆栈视图,其中包含多个圆形视图。圈子视图可以是图像(如个人资料图片)或任何东西。如果堆栈视图的大小对于子视图来说太小,这些视图应该能够重叠。如果堆栈视图对于子视图来说太大,视图应该展开。此外,即使堆栈视图的大小没有改变,也可以动态添加或删除子视图。
例如,在下图中,顶部堆栈视图具有这些重叠的圆形视图,并且那里一切正常(框架恰好是子视图视图的大小)。但随后,查看第二个堆栈视图,在添加更多视图后,第一个视图被压缩。但我想要发生的是让所有视图重叠更多一点并且不压缩任何视图。
问题
实现此行为的最佳方法是什么?我应该覆盖 layoutSubviews
,就像我在下一节中提议的那样,还是有更好的方法来实现它?同样,如果堆栈视图对它们来说太大,我只希望视图展开,或者如果堆栈视图太窄,它们可以相互重叠。堆栈视图可以随时更改大小,也可以随时添加或删除排列的子视图,所有这些都会导致重新计算视图间距。
建议的解决方案
我正在考虑覆盖堆栈视图的 layoutSubviews
方法,然后以某种方式测量所有视图,将这些宽度加在一起,然后是当前存在的间距(我想通过每个排列子视图并查看该子视图的间距)。因此,如果项目实际展开,则重叠的间距为负间距或正间距。然后,我会将该宽度与 layoutSubviews 中的框架进行比较,如果它太宽,那么我会减小间距。否则,如果视图没有占据整个堆栈视图,那么我会增加它们的间距。
这是我的代码和layoutSubviews
中提出的算法。
代码
MyShelf.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
@interface MyShelf : UIStackView
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfItemShape itemShape;
@property (strong, nonatomic) UIColor *itemBorderColor;
@property (assign, nonatomic) CGFloat itemBorderWidth;
@property (assign, nonatomic) CGFloat preferredMinimumSpacing;
@property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
@interface MyShelf ()
@property (strong, nonatomic) UIStackView *stackView;
@end
@implementation MyShelf
#pragma mark - Initializing the View
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
self.spacing = -10;
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.distribution = UIStackViewDistributionFillProportionally;
self.itemSize = CGSizeZero;
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
}
- (void)layoutSubviews {
//if the new frame is different from the old frame
//if the size of the items in the stack view is too large, reduce the spacing down to a minimum of preferredMinimumSpacing
//else if the size of the items in the stack view is too small, increase the spacing up to a maximum of preferredMaximumSpacing
//otherwise keep the spacing as-is
[super layoutSubviews];
}
#pragma mark - Managing Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
if (animated) {
//prepare the view to be initially hidden so it can be animated in
view.alpha = 0.0;
view.hidden = YES;
[super insertArrangedSubview:view atIndex:stackIndex];
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 1.0; view.hidden = NO; }
completion:nil];
} else {
[super insertArrangedSubview:view atIndex:stackIndex];
}
[self reorderArrangedSubviews];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.arrangedSubviews.count animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{ view.alpha = 0.0; view.hidden = YES; }
completion:^(BOOL finished) { [super removeArrangedSubview:view]; }];
} else {
[super removeArrangedSubview:view];
}
}
- (void)reorderArrangedSubviews {
for (__kindof UIView *arrangedSubview in self.arrangedSubviews) {
[self sendSubviewToBack:arrangedSubview];
}
}
@end
要求
如果视图是固定宽度
对于这种情况,包含这些圆形子视图的视图是固定宽度的。可能是它有一个宽度约束来指定它的宽度点数,或者它可能受到其他视图的约束,因此它的宽度是预先确定的。
在这种情况下,子视图应该彼此相邻排列,直到它们不再适合框架,此时它们开始折叠(项目之间的负间距)。
如果视图是灵活宽度
对于这种情况,包含圆形子视图的视图没有指定宽度。相反,它的宽度由内容的宽度决定。所以它应该继续增长,直到它不能再增长,在这一点上,子视图开始重叠。
调整distribution
属性即可。
self.distribution = UIStackViewDistributionEqualCentering;
此外,UIStackView
总体思路是在您的圆形视图上使用 centerX 约束 - 我将它们称为 ShelfItem
,并将它们约束为“不可见的定位视图”。
之所以这样做,是因为当项目的 centerX 位于前缘(或后缘)时,它的一半将延伸到定位视图的左侧或右侧。
考虑将宽度分成相等的部分(所有值均以 % 为单位)...
如果我们有 3 件商品,我们需要 2 等份。要获得百分比间距,我们使用 1.0 / (numItems - 1)
:
4 项,我们需要 3 等份:
5 项,我们需要 4 等份:
对于 6 项,我们需要 5 等份:
因此,通过使“项目”视图成为“定位”视图的子视图,我们可以像这样循环并设置它们的 centerX 约束:
UIView *thisItem;
CGFloat pct = 1.0 / (CGFloat)([subviews count] - 1);
for (int i = 0; i < subviews.count; i++) {
thisItem = subviews[i];
CGFloat thisPCT = pct * i;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
c.active = YES;
}
这不是相当那么简单...
首先,auto-layout 不喜欢 multiplier: 0.0
... 所以 left-most 项目需要有 centerX 等于定位视图 Leading。
第二件事是,当有足够的空间时,您的布局要求项目视图 left-aligned,而不是均匀分布。
为了实现这一点,我们将使每个项目视图的 centerX lessThanOrEqualTo
成为前一个项目的 centerX + itemWidth...并且我们将为“百分比”约束赋予 less-than-required 优先级。
所以,每次我们添加(或删除)一个项目时,我们都会调用一个方法来更新 centerX 约束......它看起来像这样:
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
最后一个任务是添加一个“框架”视图,它将匹配 laid-out 项视图的边界。
这是一个完整的例子...
ShelfItem.h - 带有标签的简单圆形视图
#import <UIKit/UIKit.h>
@interface ShelfItem : UIView
@property (strong, nonatomic) UILabel *label;
@end
ShelfItem.m
#import "ShelfItem.h"
@implementation ShelfItem
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
self.backgroundColor = UIColor.whiteColor;
_label = [UILabel new];
_label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight];
_label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_label];
[_label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES;
[_label.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES;
self.layer.borderColor = UIColor.blueColor.CGColor;
self.layer.borderWidth = 1.0;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.bounds.size.height * 0.5;
}
@end
ShelfView.h - 我们的视图完成所有工作
#import <UIKit/UIKit.h>
@interface ShelfView : UIView
- (void)addItem:(NSInteger)n;
- (void)removeItem;
@end
ShelfView.m
#import "ShelfView.h"
#import "ShelfItem.h"
@interface ShelfView () {
UIView *positionView;
UIView *framingView;
CGFloat itemWidth;
NSLayoutConstraint *framingViewTrailingConstraint;
}
@end
@implementation ShelfView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void) commonInit {
itemWidth = 60.0;
// framingView will match the bounds of the items
// it will not be their superView, but will look like it
framingView = [UIView new];
framingView.translatesAutoresizingMaskIntoConstraints = NO;
framingView.backgroundColor = UIColor.systemYellowColor;
[self addSubview:framingView];
// positionView is used for the item position constraints
// but is not seen
positionView = [UIView new];
positionView.translatesAutoresizingMaskIntoConstraints = NO;
positionView.backgroundColor = UIColor.clearColor;
[self addSubview:positionView];
// initialize framingView trailing constraint -- it will be updated in updatePositions
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:positionView.leadingAnchor];
framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
[NSLayoutConstraint activateConstraints:@[
// positioning view is at vertical center with no height
[positionView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[positionView.heightAnchor constraintEqualToConstant:0.0],
// leading and trailing are 1/2 the item width
[positionView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:itemWidth * 0.5],
[positionView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-itemWidth * 0.5],
// framing view leading is at positioning view leading minus 1/2 item width
[framingView.leadingAnchor constraintEqualToAnchor:positionView.leadingAnchor constant:-itemWidth * 0.5],
// constrained top and bottom
[framingView.topAnchor constraintEqualToAnchor:self.topAnchor],
[framingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
]];
}
- (void)updatePositions {
if ([positionView.subviews count] == 0) {
// no items, so all we have to do is update the framing view
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:self.leadingAnchor];
framingViewTrailingConstraint.active = YES;
return;
}
// clear existing centerX constraints
for (NSLayoutConstraint *oldC in positionView.constraints) {
if (oldC.firstAttribute == NSLayoutAttributeCenterX) {
oldC.active = NO;
}
}
// item views are top-down left-to-right, so reverse the order of the subviews
NSArray *reversedArray = [positionView.subviews.reverseObjectEnumerator allObjects];
// constraints don't like multiplier:0.0
// so first item centerX will always be equal to positionView's Leading
UIView *thisItem = reversedArray[0];
[NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0].active = YES;
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.333%
// item 2 centerX is at 66.666%
// item 3 centerX is at 100%
CGFloat pct = 1.0 / (CGFloat)([reversedArray count] - 1);
UIView *prevItem;
for (int i = 1; i < reversedArray.count; i++) {
prevItem = thisItem;
thisItem = reversedArray[i];
CGFloat thisPCT = pct * i;
// keep items next to each other (left-aligned) when overlap is not needed
[thisItem.centerXAnchor constraintLessThanOrEqualToAnchor:prevItem.centerXAnchor constant:itemWidth].active = YES;
// centerX as a percentage of positionView width
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:thisItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:positionView
attribute:NSLayoutAttributeTrailing
multiplier:thisPCT
constant:0.0];
// needs less-than-required priority so "left-aligned" constraint can be enforced
c.priority = UILayoutPriorityRequired - 1;
c.active = YES;
}
// update the trailing anchor of the framing view to the last shelf item
framingViewTrailingConstraint.active = NO;
framingViewTrailingConstraint = [framingView.trailingAnchor constraintEqualToAnchor:thisItem.trailingAnchor];
framingViewTrailingConstraint.active = YES;
}
- (void)addItem:(NSInteger)n {
// create a new shelf item
ShelfItem *v = [ShelfItem new];
v.translatesAutoresizingMaskIntoConstraints = NO;
v.label.text = [NSString stringWithFormat:@"%ld", (long)n];
// add it as a subview of positionView
// at index Zero (so it will be underneath existing items)
[positionView insertSubview:v atIndex:0];
// width and height
[v.widthAnchor constraintEqualToConstant:itemWidth].active = YES;
[v.heightAnchor constraintEqualToAnchor:v.widthAnchor].active = YES;
// vertically centered on positionView
[v.centerYAnchor constraintEqualToAnchor:positionView.centerYAnchor constant:0.0].active = YES;
// update all shelf items
[self updatePositions];
}
- (void)removeItem {
// remove the last-added item
[positionView.subviews[0] removeFromSuperview];
// update all shelf items
[self updatePositions];
}
@end
ViewController.h - 带有两个 ShelfView 和添加/删除按钮的控制器:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
ViewController.m
#import "ViewController.h"
#import "ShelfView.h"
@interface ViewController ()
{
ShelfView *shelfViewA;
ShelfView *shelfViewB;
NSInteger counter;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
counter = 0;
// top shelf view has systemYellow background, so we see its
// full width all the time
shelfViewA = [ShelfView new];
shelfViewA.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewA.backgroundColor = UIColor.systemYellowColor;
[self.view addSubview:shelfViewA];
// second shelf view has clear background, so we only see its
// framing view width when items are added
shelfViewB = [ShelfView new];
shelfViewB.translatesAutoresizingMaskIntoConstraints = NO;
shelfViewB.backgroundColor = UIColor.clearColor;
[self.view addSubview:shelfViewB];
UIButton *addBtn = [UIButton new];
addBtn.translatesAutoresizingMaskIntoConstraints = NO;
addBtn.backgroundColor = UIColor.systemGreenColor;
[addBtn setTitle:@"Add" forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[addBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[addBtn addTarget:self action:@selector(addTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:addBtn];
UIButton *removeBtn = [UIButton new];
removeBtn.translatesAutoresizingMaskIntoConstraints = NO;
removeBtn.backgroundColor = UIColor.systemGreenColor;
[removeBtn setTitle:@"Remove" forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[removeBtn setTitleColor:UIColor.lightGrayColor forState:UIControlStateHighlighted];
[removeBtn addTarget:self action:@selector(removeTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:removeBtn];
UILabel *info = [UILabel new];
info.translatesAutoresizingMaskIntoConstraints = NO;
info.backgroundColor = [UIColor colorWithWhite:0.90 alpha:1.0];
info.textAlignment = NSTextAlignmentCenter;
info.numberOfLines = 0;
info.text = @"Shelf View Width\n60-pts on each side.";
[self.view addSubview:info];
// respect safeArea
UILayoutGuide *g = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[shelfViewA.topAnchor constraintEqualToAnchor:g.topAnchor constant:60.0],
[shelfViewA.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewA.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewA.heightAnchor constraintEqualToConstant:60.0],
[info.topAnchor constraintEqualToAnchor:shelfViewA.bottomAnchor constant:8.0],
[info.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[info.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.topAnchor constraintEqualToAnchor:info.bottomAnchor constant:8.0],
[shelfViewB.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:60.0],
[shelfViewB.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-60.0],
[shelfViewB.heightAnchor constraintEqualToConstant:60.0],
[addBtn.topAnchor constraintEqualToAnchor:shelfViewB.bottomAnchor constant:20.0],
[addBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[addBtn.widthAnchor constraintEqualToConstant:200.0],
[removeBtn.topAnchor constraintEqualToAnchor:addBtn.bottomAnchor constant:20.0],
[removeBtn.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[removeBtn.widthAnchor constraintEqualToConstant:200.0],
]];
}
- (void)addTapped {
++counter;
[shelfViewA addItem:counter];
[shelfViewB addItem:counter];
}
- (void)removeTapped {
if (counter > 0) {
--counter;
[shelfViewA removeItem];
[shelfViewB removeItem];
}
}
@end
运行 这给了我们这个 - 注意“顶部”货架视图显示其框架,“底部”货架视图仅显示“框架视图”:
并且当视图改变大小时,例如在设备旋转时,我们不需要做任何事情......auto-layout 为我们处理它: