股票场内基金交易,没时间盯盘?
相比于 UITableView 单纯的对表格进行上下拖动,UICollectionView 的特色之一便是它的布局——UICollectionViewLayout。通过它,只要拥有丰富的想象力和简单的数学基础,我们不仅可以做出最简单的网格排列,还能搭建出各种天马行空的创意布局。
UICollectionViewLayout 只是一个基类,无法直接使用。我们只能通过 UICollectionViewFlowLayout 或自定义 UICollectionViewLayout 子类来实现布局。
UICollectionViewFlowLayout
UICollectionViewFlowLayout (流水布局)是苹果为我们封装好了的一套布局方案,我们最常用的网格和线式布局都是由它来实现。
查看 UICollectionViewFlowLayout 头文件,可以看到主要分为两类:各类属性和代理方法。
如果对单一的 UICollectionView 内容视图没有特殊要求,可以通过调节属性进行统一的设置:
1 2 3 4 5 6 7 8 9 10 |
@property (nonatomic) CGFloat minimumLineSpacing; // 垂直滚动时的行间距,水平滚动时为列间距 @property (nonatomic) CGFloat minimumInteritemSpacing; // 垂直滚动时的列间距,水平滚动时为行间距 @property (nonatomic) CGSize itemSize; // cell 的尺寸 @property (nonatomic) UICollectionViewScrollDirection scrollDirection; // 滚动方向,默认为垂直滚动 @property (nonatomic) CGSize headerReferenceSize; // 头视图的尺寸。如果是垂直方向滑动,则只有高起作用;如果是水平方向滑动,则只有宽起作用 @property (nonatomic) CGSize footerReferenceSize; // 尾部视图的尺寸。如果是垂直方向滑动,则只有高起作用;如果是水平方向滑动,则只有宽起作用 @property (nonatomic) UIEdgeInsets sectionInset; // 每个 section 的边缘间距 @property (nonatomic) BOOL sectionHeadersPinToVisibleBounds; // 头视图是否固定在屏幕边缘 @property (nonatomic) BOOL sectionFootersPinToVisibleBounds; // 尾视图是否固定在屏幕边缘 |
如果需要进行一些针对性设置,可以遵守 UICollectionViewDelegateFlowLayout 协议使用代理方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 设置每个 cell 尺寸 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; // 设置每个 section 的边缘间距 - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; // 设置行间距 - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section; // 设置列间距 - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section; // 设置 section 头视图尺寸 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; // 设置 section 尾视图尺寸 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section; |
比如通过如下设置,可以得到一个类似相册模式单行水平滚动的布局(此处省略了数据源方法的实现):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (void)viewDidLoad { [super viewDidLoad]; UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; collectionView.backgroundColor = [UIColor groupTableViewBackgroundColor]; // 设置数据源代理对象 collectionView.dataSource = self; // 注册 cell [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:ID]; [self.view addSubview:collectionView]; // 水平方向滚动 layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; layout.minimumLineSpacing = 50; // cell 尺寸 layout.itemSize = CGSizeMake(200, 200); // 垂直方向边缘间距 CGFloat insetV = 0.5 * (self.view.frame.size.height - layout.itemSize.height); // 水平方向边缘间距 CGFloat insetH = 0.5 * (collectionView.frame.size.width - layout.itemSize.width); layout.sectionInset = UIEdgeInsetsMake(insetV, insetH, insetV, insetH); } |
布局效果:
自定义布局
显然 UICollectionViewFlowLayout 是无法实现我们许多要求的,这时只好自己动手丰衣足食了。
UICollectionViewLayoutAttributes
UICollectionViewLayout 实际上是通过 UICollectionViewLayoutAttributes 类来实现布局的,每一个 UICollectionView 内容视图都对应一个 UICollectionViewLayoutAttributes 对象。可以说自定义布局的绝大部分精力都用在设置 UICollectionViewLayoutAttributes 上。
它提供了三个类方法创建相应的 UICollectionViewLayoutAttributes 对象:
1 2 3 4 |
+ (instancetype)layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath; + (instancetype)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind withIndexPath:(NSIndexPath *)indexPath; + (instancetype)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind withIndexPath:(NSIndexPath *)indexPath; |
属性列表:
1 2 3 4 5 6 7 8 9 10 11 |
@property (nonatomic) CGRect frame; @property (nonatomic) CGPoint center; @property (nonatomic) CGSize size; @property (nonatomic) CATransform3D transform3D; @property (nonatomic) CGRect bounds; @property (nonatomic) CGAffineTransform transform; @property (nonatomic) CGFloat alpha; @property (nonatomic) NSInteger zIndex; // z 轴(垂直于屏幕的轴)优先级,默认为0,越大优先级越高 @property (nonatomic, getter=isHidden) BOOL hidden; // 是否隐藏;从优化性能考虑,隐藏的对象不会被创建 @property (nonatomic, strong) NSIndexPath *indexPath; |
实现方法
-
调用 prepareLayout 方法;
prepareLayout 方法是专门用来准备布局的,在布局之前,它会调用一次,之后只有在调用 shouldInvalidateLayoutForBoundsChange: 方法并返回YES、调用 invalidateLayout 方法和 UICollectionView 刷新的时候才会重新调用。因此,我们通常在这个方法中进行一些一次性的设置和计算,如 cell 中固定的布局属性等,以提高性能。
12345- (void)prepareLayout{[super prepareLayout];/** 一次性设置 */} -
分别通过以下三个方法获得并设置不同 UICollectionView 内容视图的 UICollectionViewLayoutAttributes;
1234- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath; -
调用 layoutAttributesForElementsInRect: 方法;
123456789/*** 指定 rect 内需要显示的 UICollectionView 视图** @param rect 指定区域** @return 由 rect 内所有 UICollectionView 视图对应 UICollectionViewLayoutAttributes 对象组成的数组*/- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;一般在这个方法中返回上一步中所有设置好的 UICollectionViewLayoutAttributes 组成的数组。注意此方法会多次调用,除非需要动态改变布局属性,否则最好将添加数组的代码放到 prepareLayout 方法中,以免重复执行消耗性能。
-
调用 collectionViewContentSize 方法设置 contentView 尺寸;
如果不设置,则默认为 collectionView 的视图尺寸。同样的,由于此方法会多次调用,最好在 prepareLayout 方法中进行尺寸计算。
-
如果需要动态控制 UICollectionViewLayoutAttributes,调用 shouldInvalidateLayoutForBoundsChange: 方法并返回YES。
综合实例
自定义 UICollectionViewFlowLayout
流水布局的核心概念在于 cell 依据布局顺序排列,也就是说,如果我们需要的布局模式是顺序排列的,那么就可以通过自定义 UICollectionViewFlowLayout 子类的方式忽略一些繁琐的设置,达到省时省力的效果。
比如说上面的例子,想要更进一步达到如下滚动放大的相册布局:
我们要做的其实就是实时监控并设置进入屏幕区域 cell 的缩放比例。新建一个继承 UICollectionViewFlowLayout 的类后,只需要进行两部设置:
-
调用 shouldInvalidateLayoutForBoundsChange: 方法并返回YES以动态设置布局属性;
1234- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{return YES;} -
在 layoutAttributesForElementsInRect: 方法中设置实时缩放比例;
12345678910111213141516171819202122// 最大缩放比例static CGFloat const kScale = 1.3;- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{// 取出默认的 UICollectionViewLayoutAttributes 数组NSArray *array = [super layoutAttributesForElementsInRect:rect];// 计算当前屏幕的中点XCGFloat centerX = self.collectionView.contentOffset.x + 0.5 * self.collectionView.frame.size.width;// 遍历数组for (UICollectionViewLayoutAttributes *attributes in array) {// 屏幕对应的 contentView 区域CGRect collctionViewRect;collctionViewRect.size = self.collectionView.frame.size;collctionViewRect.origin = self.collectionView.contentOffset;// 判断 cell 与屏幕区域是否相交,避免计算超出屏幕外的部分,提高性能if (!CGRectIntersectsRect(collctionViewRect, attributes.frame)) continue;// 计算比例CGFloat scale = 1 + (kScale - 1) * (1 - ABS(attributes.center.x - centerX) / (0.5 * self.collectionView.frame.size.width));attributes.transform = CGAffineTransformMakeScale(scale, scale);}return array;}
将这个自定义类替换上面例子代码中的 UICollectionViewFlowLayout 类,就能实现期望效果。
瀑布流布局
瀑布流布局显然不是顺序排列,因此只能完全自定义。
先看外部接口,除开基本的各类间距外,瀑布流的基本形式要求提供列数和每个 cell 的宽高比,这里可以用设置数据源代理的方式进行设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// // TimWaterfallLayout.h #import <UIKit/UIKit.h> @class TimWaterfallLayout; @protocol TimWaterfallLayoutDataSource <NSObject> @required - (CGFloat) waterfallLayout:(TimWaterfallLayout *)waterfallLayout cellAspectRatioAtIndexPath:(NSIndexPath *)indexPath; @end @interface TimWaterfallLayout : UICollectionViewLayout @property(assign, nonatomic)int columnCount; // 列数 @property(assign, nonatomic)CGFloat columnMargin; // 每一列间的间距 @property(assign, nonatomic)CGFloat rowMargin; // 每一行间的间距 @property(assign, nonatomic) UIEdgeInsets sectionInset; // 边界间距 @property(weak, nonatomic)id<TimWaterfallLayoutDataSource> delegate; // 提供一个类方法方便初始化 + (instancetype)waterfallLayoutWithColumnCount:(int)columnCount columnMargin:(CGFloat)columnMargin rowMargin:(CGFloat)rowMargin sectionInset:(UIEdgeInsets)sectionInset; @end |
布局实现思路:由于是静态布局,无需调用 shouldInvalidateLayoutForBoundsChange: 方法,只用考虑每个 cell 的摆放位置(itemSize 属性和 center 属性)及布局完成后的 contentViewSize。
鉴于瀑布流的原则是讲新添加的 cell 放到底部 Y 值最小的列下面,我们可以用一个数组或字典实时记录每一列的底部 Y 值,辅助 cell 属性及 contentViewSize 的计算:
声明数组:
1 2 |
@property(strong, nonatomic) NSMutableArray *columnBottomYArray; |
初始化:
1 2 3 4 5 6 7 8 9 10 11 |
- (NSMutableArray *)columnBottomYArray{ if (!_columnBottomYArray) { _columnBottomYArray = [NSMutableArray array]; for (int i = 0; i < self.columnCount; i++) { CGFloat bottomY = self.sectionInset.top; [_columnBottomYArray addObject:@(bottomY)]; } } return _columnBottomYArray; } |
接下来我们按照自定义布局步骤的思路一步步实现(这里不对代码按步骤进行割裂呈现,我认为完整版的可读性应该更流畅):
-
调用 prepareLayout 方法;
思考一下有哪些无需多次设置的固定属性:
-
每个 cell 的宽度;
-
layoutAttributesForElementsInRect: 方法所需的数组;
-
contentViewSize。
-
-
在 layoutAttributesForItemAtIndexPath: 方法中完成对 cell 的属性设置;
- 通过数据源方法获得的宽高比计算出 cell 高度;
- 通过 columnBottomYArray 计算出 cell 应该添加到的位置(中心点);
- 更新 columnBottomYArray。
-
分别调用 layoutAttributesForElementsInRect: 方法和 collectionViewContentSize 方法返回已在 prepareLayout 方法中计算好的属性。
下面是完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
// TimWaterfallLayout.m // #import "TimWaterfallLayout.h" @interface TimWaterfallLayout () @property(strong, nonatomic) NSMutableArray *columnBottomYArray; @property(assign, nonatomic) CGFloat cellW; @property(strong, nonatomic) NSMutableArray *layoutAttributesArray; @property(assign, nonatomic) CGSize contentSize; @end @implementation TimWaterfallLayout + (instancetype)waterfallLayoutWithColumnCount:(int)columnCount columnMargin:(CGFloat)columnMargin rowMargin:(CGFloat)rowMargin sectionInset:(UIEdgeInsets)sectionInset{ TimWaterfallLayout *waterfallLayout = [self new]; waterfallLayout.columnCount = columnCount; waterfallLayout.columnMargin = columnMargin; waterfallLayout.rowMargin = rowMargin; waterfallLayout.sectionInset = sectionInset; return waterfallLayout; } - (NSMutableArray *)columnBottomYArray{ if (!_columnBottomYArray) { _columnBottomYArray = [NSMutableArray array]; for (int i = 0; i < self.columnCount; i++) { CGFloat bottomY = self.sectionInset.top; [_columnBottomYArray addObject:@(bottomY)]; } } return _columnBottomYArray; } - (NSMutableArray *)layoutAttributesArray{ if (!_layoutAttributesArray) { _layoutAttributesArray = [NSMutableArray array]; } return _layoutAttributesArray; } - (void)prepareLayout{ [super prepareLayout]; // 每个 cell 的宽度 self.cellW = (self.collectionView.frame.size.width - (2 + self.columnCount - 1) * self.columnMargin) / self.columnCount; // 获得所有 item 的布局属性 NSInteger count = [self.collectionView numberOfItemsInSection:0]; for (int i = 0; i < count; i++) { [self.layoutAttributesArray addObject:[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]]; } // 布局完成后获得整个所有 cell 最底部 Y 值 CGFloat currentMaxY = -MAXFLOAT; for (int i = 0; i < self.columnCount; i++) { if ([self.columnBottomYArray[i] doubleValue] > currentMaxY) { currentMaxY = [self.columnBottomYArray[i] doubleValue]; } } // 计算 collectionViewContentSize self.contentSize = CGSizeMake(self.collectionView.frame.size.width, currentMaxY + self.sectionInset.bottom); // 清空 columnBottomYArray,用以滚动后重新布局时继续使用 self.columnBottomYArray = nil; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; // 利用数据源方法返回宽高比 CGFloat aspectRatio = 0; if ([self.delegate respondsToSelector:@selector(waterfallLayout:cellAspectRatioAtIndexPath:)]) { aspectRatio = [self.delegate waterfallLayout:self cellAspectRatioAtIndexPath:indexPath]; } // cell 高度 CGFloat cellH = self.cellW / aspectRatio; attributes.size = CGSizeMake(self.cellW, cellH); // 遍历 columnBottomYArray 获得当前底部最小的 Y 值和列号 int minYColumn = 0; CGFloat currentMinY = MAXFLOAT; for (int i = 0; i < self.columnCount; i++) { if ([self.columnBottomYArray[i] doubleValue] < currentMinY) { currentMinY = [self.columnBottomYArray[i] doubleValue]; minYColumn = i; } } // 根据获得数据计算 cell 中心点 CGFloat cellCenterX = self.sectionInset.left + 0.5 * self.cellW + (minYColumn % self.columnCount) * (self.cellW + self.columnMargin); CGFloat cellCenterY = currentMinY + self.rowMargin + 0.5 * cellH; attributes.center = CGPointMake(cellCenterX, cellCenterY); // 更新 columnBottomYArray CGFloat newMinY = currentMinY + self.rowMargin + cellH; self.columnBottomYArray[minYColumn] = [NSNumber numberWithDouble:newMinY]; return attributes; } - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{ return self.layoutAttributesArray; } - (CGSize)collectionViewContentSize{ return self.contentSize; } @end |
想获得去掉 5 元限制的证券账户吗?

如果您想去掉最低交易佣金 5 元限制,使用微信扫描左边小程序二维码,访问微信小程序「优财助手」,点击底部菜单「福利」,阅读文章「通过优财开证券账户无最低交易佣金 5 元限制」,按照文章步骤操作即可获得免 5 元证券账户,股票基金交易手续费率万 2.5。
请注意,一定要按照文章描述严格操作,如错误开户是无法获得免 5 元证券账户的。