我正在开发一个具有UICollectionView
的应用程序 - 集合视图的工作是显示来自Web服务的数据。
我尝试实现的应用程序的一个功能是允许用户将此UICollectionView
的布局从网格视图更改为表格视图。
我花了很多时间试图完善这一点,并设法让它发挥作用。但是有一些问题。两个布局之间的转换看起来不太好,有时它在切换视图之间断开,我的应用程序留下了处于意外状态的视图。只有当用户在网格和表视图之间切换非常快速(连续按changeLayoutButton
)时才会发生这种情况。
所以,显然存在一些问题,我觉得代码有点脆弱。我还需要解决上面提到的问题。
我将从如何实现此视图开始。
实施
因为我需要两个不同的单元格(grideCell
和tableViewCell
)来展示不同的东西 - 我认为继承UICollectionViewFlowLayout
会更好,因为它可以完成我需要的一切 - 我所有需要做的是改变单元格大小。
考虑到这一点,我创建了两个子类UICollectionViewFlowLayout
这两个类的外观如下:
BBTradeFeedTableViewLayout.m
#import "BBTradeFeedTableViewLayout.h"
@implementation BBTradeFeedTableViewLayout
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(320, 80);
self.minimumLineSpacing = 0.1f;
}
return self;
}
@end
BBTradeFeedGridViewLayout.m
#import "BBTradeFeedGridViewLayout.h"
@implementation BBTradeFeedGridViewLayout
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(159, 200);
self.minimumInteritemSpacing = 2;
self.minimumLineSpacing = 3;
}
return self;
}
@end
非常简单,正如您所看到的 - 只需更改单元格大小即可。
然后在我的viewControllerA
课程中,我实现了UICollectionView
,如下所示:
创建了属性:
@property (strong, nonatomic) BBTradeFeedTableViewLayout *tableViewLayout;
@property (strong, nonatomic) BBTradeFeedGridViewLayout *grideLayout;
viewDidLoad
中的
/* Register the cells that need to be loaded for the layouts used */
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:@"BBItemTableViewCell" bundle:nil] forCellWithReuseIdentifier:@"TableItemCell"];
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:@"BBItemGridViewCell" bundle:nil] forCellWithReuseIdentifier:@"GridItemCell"];
用户点击按钮可在布局之间切换:
-(void)changeViewLayoutButtonPressed
我使用BOOL来确定当前处于活动状态的布局,并根据我使用此代码进行切换:
[self.collectionView performBatchUpdates:^{
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView setCollectionViewLayout:self.grideLayout animated:YES];
} completion:^(BOOL finished) {
}];
在cellForItemAtIndexPath
我确定应该使用哪些单元格(grid或tableView)并加载数据 - 该代码如下所示:
if (self.gridLayoutActive == NO){
self.switchToTableLayout = NO;
BBItemTableViewCell *tableItemCell = [collectionView dequeueReusableCellWithReuseIdentifier:tableCellIdentifier forIndexPath:indexPath];
if ([self.searchArray count] > 0){
self.switchToTableLayout = NO;
tableItemCell.gridView = NO;
tableItemCell.backgroundColor = [UIColor whiteColor];
tableItemCell.item = self.searchArray[indexPath.row];
}
return tableItemCell;
}else
{
BBItemTableViewCell *gridItemCell= [collectionView dequeueReusableCellWithReuseIdentifier:gridCellIdentifier forIndexPath:indexPath];
if ([self.searchArray count] > 0){
self.switchToTableLayout = YES;
gridItemCell.gridView = YES;
gridItemCell.backgroundColor = [UIColor whiteColor];
gridItemCell.item = self.searchArray[indexPath.row];
}
return gridItemCell;
}
最后在两个单元格类中 - 我只是使用数据来设置我需要的图像/文本。
同样在网格单元格中 - 图像更大,我删除了我不想要的文本 - 这是使用两个单元格的主要原因。
我对如何使这个视图在UI中看起来更流畅,更少错误感兴趣。我想要的外观就像eBays iOS应用程序 - 它们在三种不同的视图之间切换。我只需要在两个不同的视图之间切换。
答案 0 :(得分:18)
@ jrturton的答案很有帮助,但除非我遗漏了一些东西,否则它真的过于复杂,非常简单。我将从我们同意的观点开始......
首先,我同意在布局转换开始时禁用用户交互的方法。使用[[UIApplication sharedApplication] begin/endIgnoringInteractionEvents]
在最后(在完成块中)重新启用 - 这比尝试取消正在进行的过渡动画&立即开始从当前状态的反向过渡。
另外,我非常同意为每个布局使用相同的单元类的建议。在viewDidLoad
中注册一个单元格类,并简化您的collectionView:cellForItemAtIndexPath:
方法,使单元格出列并设置其数据:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
BBItemCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
if ([self.searchArray count] > 0) {
cell.backgroundColor = [UIColor whiteColor];
cell.item = self.searchArray[indexPath.row];
}
return cell;
}
(请注意,单元格本身不应该(除了特殊情况之外的所有情况)需要知道与当前使用的布局有关的任何内容,布局是否正在转换,或者当前转换是什么进步是)
然后当您致电setCollectionViewLayout:animated:completion:
时,集合视图不需要重新加载任何新单元格,它只需设置一个动画块来更改每个单元格的布局属性(您不需要...需要从performBatchUpdates:
块内调用此方法,也不需要手动使布局无效)。
然而,正如所指出的,您会注意到单元格的子视图会立即跳转到新布局的框架。解决方案是在布局属性更新时简单地强制单元子视图的立即布局:
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
[self layoutIfNeeded];
}
(无需为转换创建特殊的布局属性)
为什么这样做?当集合视图更改布局时,将为每个单元调用applyLayoutAttributes:
,因为集合视图正在为该过渡设置动画块。但是单元格子视图的布局不会立即完成 - 它会延迟到稍后的运行循环 - 导致实际的子视图布局更改未合并到动画块中,因此子视图会立即跳转到其最终位置。调用layoutIfNeeded
意味着我们告诉单元格我们希望子视图布局立即,因此布局在动画块中完成,并且子视图'帧与细胞本身一起动画化。
确实,使用标准setCollectionViewLayout:...
API会限制对动画时序的控制。如果您想应用自定义缓动动画曲线,那么像TLLayoutTransitioning
这样的解决方案就会展示一种利用交互式UICollectionViewTransitionLayout
对象来控制动画时序的便捷方法。但是,只要只需要子视图的线性动画,我想大多数人都会对默认动画感到满意,特别是考虑到实现它的单行简单。
为了记录,我自己并不热衷于缺乏对此动画的控制,因此实现了与TLLayoutTransitioning
类似的功能。如果这也适用于你,那么请忽略我对@ jrturton的严厉批评,否则很好的答案,并查看用计时器实现的TLLayoutTransitioning
或UICollectionViewTransitionLayout
:)
答案 1 :(得分:13)
网格/表格转换并不像简单的演示那么容易让你相信。当您在单元格中间有一个标签并且背景稳固时,它们可以正常工作,但是一旦您有任何真实的内容,它就会失败。这就是原因:
有许多不同的解决方案。在没有看到您的手机内容的情况下,很难推荐任何具体内容,但我已经取得了以下成功:
我使用here的方法取得了相当不错的效果。
依靠调整面具大小或Autolayout这项艰巨的工作,但这是让事情变得更好的额外工作。
至于用户可以在布局之间切换太快的问题 - 只需在转换开始时禁用按钮,并在完成后重新启用它。
作为一个更实际的例子,这里是上面链接的应用程序中的布局更改示例(其中一些被省略)。请注意,在转换发生时禁用交互,我使用上面链接的项目的转换布局,并且有一个完成处理程序:
-(void)toggleLayout:(UIButton*)sender
{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
HMNNewsLayout newLayoutType = self.layoutType == HMNNewsLayoutTable ? HMNNewsLayoutGrid : HMNNewsLayoutTable;
UICollectionViewLayout *newLayout = [HMNNewsCollectionViewController collectionViewLayoutForType:newLayoutType];
HMNTransitionLayout *transitionLayout = (HMNTransitionLayout *)[self.collectionView transitionToCollectionViewLayout:newLayout duration:0.5 easing:QuarticEaseInOut completion:^(BOOL completed, BOOL finish)
{
[[NSUserDefaults standardUserDefaults] setInteger:newLayoutType forKey:HMNNewsLayoutTypeKey];
self.layoutType = newLayoutType;
sender.selected = !sender.selected;
for (HMNNewsCell *cell in self.collectionView.visibleCells)
{
cell.layoutType = newLayoutType;
}
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
}];
[transitionLayout setUpdateLayoutAttributes:^UICollectionViewLayoutAttributes *(UICollectionViewLayoutAttributes *layoutAttributes, UICollectionViewLayoutAttributes *fromAttributes, UICollectionViewLayoutAttributes *toAttributes, CGFloat progress)
{
HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
attributes.progress = progress;
attributes.destinationLayoutType = newLayoutType;
return attributes;
}];
}
在单元格中,对于任一布局都是相同的单元格,我有一个图像视图和一个标签容器。标签容器包含所有标签,并使用自动布局在内部放置它们。图像视图和每个布局中的标签容器都有常量框架变量。
转换布局中的布局属性是一个自定义子类,其中包含一个转换进度属性,在上面的更新布局属性块中设置。这是使用applyLayoutAttributes方法传递给单元格的(省略了一些其他代码):
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
self.transitionProgress = 0;
if ([layoutAttributes isKindOfClass:[HMNTransitionLayoutAttributes class]])
{
HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
self.transitionProgress = attributes.progress;
}
[super applyLayoutAttributes:layoutAttributes];
}
如果正在进行转换,则单元子类中的 layoutSubviews
会对图像和标签的两个帧之间进行插值的艰苦工作:
-(void)layoutSubviews
{
[super layoutSubviews];
if (!self.transitionProgress)
{
switch (self.layoutType)
{
case HMNNewsLayoutTable:
self.imageView.frame = imageViewTableFrame;
self.labelContainer.frame = labelContainerTableFrame;
break;
case HMNNewsLayoutGrid:
self.imageView.frame = imageViewGridFrame;
self.labelContainer.frame = self.originalGridLabelFrame;
break;
}
}
else
{
CGRect fromImageFrame,toImageFrame,fromLabelFrame,toLabelFrame;
if (self.layoutType == HMNNewsLayoutTable)
{
fromImageFrame = imageViewTableFrame;
toImageFrame = imageViewGridFrame;
fromLabelFrame = labelContainerTableFrame;
toLabelFrame = self.originalGridLabelFrame;
}
else
{
fromImageFrame = imageViewGridFrame;
toImageFrame = imageViewTableFrame;
fromLabelFrame = self.originalGridLabelFrame;
toLabelFrame = labelContainerTableFrame;
}
CGFloat from = 1.0 - self.transitionProgress;
CGFloat to = self.transitionProgress;
self.imageView.frame = (CGRect)
{
.origin.x = from * fromImageFrame.origin.x + to * toImageFrame.origin.x,
.origin.y = from * fromImageFrame.origin.y + to * toImageFrame.origin.y,
.size.width = from * fromImageFrame.size.width + to * toImageFrame.size.width,
.size.height = from * fromImageFrame.size.height + to * toImageFrame.size.height
};
self.labelContainer.frame = (CGRect)
{
.origin.x = from * fromLabelFrame.origin.x + to * toLabelFrame.origin.x,
.origin.y = from * fromLabelFrame.origin.y + to * toLabelFrame.origin.y,
.size.width = from * fromLabelFrame.size.width + to * toLabelFrame.size.width,
.size.height = from * fromLabelFrame.size.height + to * toLabelFrame.size.height
};
}
self.headlineLabel.preferredMaxLayoutWidth = self.labelContainer.frame.size.width;
}
关于它。基本上你需要一种方法来告诉单元格过渡到底有多远,你需要布局转换库(或者,正如我所说,Facebook pop可能会这样做),然后你需要确保你得到很好的价值用于在两者之间转换时的布局。