动画二
隔了好多天,因为在忙别的事。。。
那我们现在要做什么,上一篇讲到一个滚动大屏图,巨丑。
趁着回忆的时间,先添两行代码
//隐藏滚动条
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.showsHorizontalScrollIndicator = NO;
开始正题
让item变小
毫无疑问,这是一个很简单的事情。我们首先应该去设置itemSize,然后设置行(这里是纵向)间距,其次可以做一个组边距(sectionInset)使其两边留白。
当然简单的事情就要考虑的稍微多一点,这个size要怎么设置(200,300),这样可能在不同大小的屏幕上会有很明显的差别,那有人会说可以像Masonry(一个layout的框架)设置与其他UI的关系位置,这是一种策略,但是不见得好,因为它不是一个纯色的东西。根据其他位置拉伸变化(即便可以保持图片不拉伸)也不见得好,所以可以看很多代码会有一堆屏幕大小的宏,然后根据一个(200,300)针对某一尺寸的屏幕,对于不同的屏幕,按屏幕比等比扩大的代码。这个可以自行实现,我做简单易懂一点
#define ItemHeight (ScreenHeight * (5.0 / 9))
#define ItemWidth (ScreenWidth * (3.0 / 5))
然后相应的代码变成
self.itemSize = CGSizeMake(ItemWidth, ItemHeight);
self.scrollDirection = UICollectionViewScrollDirectionHorizontal; //水平方向
self.sectionInset = UIEdgeInsetsMake(0, ScreenWidth/2 - ItemWidth/2, 0, ScreenWidth/2 - ItemWidth/2); //设置组边距
self.minimumLineSpacing = (ScreenWidth - ItemWidth)/4;
这里忘了提一个东西,我在这次加了一个pch(全局不需要自己导入的.h - -),当然里面放太多的东西会影响效率,至于添加方法自行百度,也可以做个.h自行导入,差不多
cell居中设置
现在我们的跑一下我们的代码,当你停止滑动的时候,它会停在一个当前的位置,但是这个位置可能是两个卡片(cell)的中间,或者偏一点的地方,我们需要的是对卡片进行操作,所以需要让它居中。所以引入一个问题,如果只是不小心的触碰到屏幕,稍稍的滑了一下,需要切换吗?显然是不需要的,把这个一下量化一下(设置为distance)以后,变成<distance的距离不需要切换,而>distance的距离就切换卡片。在这样的思路确定以后,我们开始实现这个逻辑
代理也是继承的,所以这样的效果应该在scrollView的代理里实现
//开始拖拽collectionView(scrollView的子类)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
//停止拖拽collectionView
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
在这两个方法里面可以拿到起始的位置,和结束的位置,他们到底是怎么样的,可以在里面打印输出一下
//向左滑
2018-01-29 19:37:03.265 卡片转场1[31481:1202165] 开始拖动0.000000
2018-01-29 19:37:04.624 卡片转场1[31481:1202165] 停止拖动105.000000
2018-01-29 19:37:06.905 卡片转场1[31481:1202165] 开始拖动105.000000
2018-01-29 19:37:07.729 卡片转场1[31481:1202165] 停止拖动124.000000
2018-01-29 19:37:10.943 卡片转场1[31481:1202165] 开始拖动124.000000
//向右滑
2018-01-29 19:38:26.746 卡片转场1[31481:1202165] 开始拖动1449.000000
2018-01-29 19:38:28.164 卡片转场1[31481:1202165] 停止拖动1404.000000
2018-01-29 19:38:30.113 卡片转场1[31481:1202165] 开始拖动1404.000000
2018-01-29 19:38:30.547 卡片转场1[31481:1202165] 停止拖动1371.333333
2018-01-29 19:38:32.806 卡片转场1[31481:1202165] 开始拖动1371.333333
明白这样的变化以后,我们声明几个类内的变量
CGFloat _startDragX; //开始移动的位置
CGFloat _endDragX; //停止移动的位置
CGFloat _dragMiniDistance; //最小移动的临界值
NSInteger _currentIndex; //当前切换到的卡片索引位置
NSInteger _maxIndex; //最大的索引位置
//在开始拖动的方法里获取到 _startDragX
_startDragX = scrollView.contentOffset.x;
//在停止拖动的方法里获取到_endDragX
_endDragX = scrollView.contentOffset.x;
然后根据上面的逻辑进行一系列的简单运算判断:
CGFloat delta = _startDragX - _endDragX;
if (delta >= _dragMiniDistance) {
//向右滑动
_currentIndex -= 1;
} else if (delta * -1 >= _dragMiniDistance) {
//向左滑动
_currentIndex += 1;
}
_currentIndex = _currentIndex <= 0 ? 0 : _currentIndex;
_currentIndex = _currentIndex >= _maxIndex ? _maxIndex : _currentIndex;
这样我们就可以调用scrollToItemAtIndexPath: atScrollPosition这个方法实现滑动了,但是这里要注意一点,就是所有tableView,collectionView这些数据源方法的特殊性,如果你同步执行的话,他们的数据源方法可能还没有在内部算清,所以就会出Bug,所以对于这个方法,我们需要用主队列异步去执行,很多代码里面就把类似上面的一堆运算也扔在异步队列里,其实不用,我们只是要把UI更新放进去.
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0]
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
});
//顺带利用这个_currentIndex的索引,可以把背后的蒙版也换一下
self.BgView.image = [UIImage imageNamed:self.imageArray[_currentIndex].cardPicName];
调整布局属性
现在有个问题需要我们考虑,就是每个卡片的大小都是一样,看示例gif明显当前屏幕中间的大,两边的小。所以我们也要这样做,调整cell的大小,在UICollectionView里面,他们的大小(还有透明度这些属性)是归于布局属性负责的,所以我们要调整布局属性,在哪里调整,当然是代理方法了,一个UICollectionViewFlowLayout的父类代理UICollectionViewLayout里面有layoutAttributesForElementsInRect:(CGRect)rect这样一个方法,是这样介绍的
//Implement -layoutAttributesForElementsInRect:
//to return layout attributes for for supplementary or decoration views, or to perform layout in an as-needed-on-screen fashion.
翻译过来就是:返回 追加的 或者 装饰视图 布局属性 或者去适应屏幕上的布局。
还有一个这样的方法:
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
很明显是对每个具体cell的布局进行调整
这里我们的需求其实是对两边cell的大小进行统一的调整,其实也不用具体到张三,李四cell这个具体的cell上,只要是在两边的就进行缩放就好了,所以选择第一个方法去实现
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
//这样就拿到了所有的布局属性
NSArray<UICollectionViewLayoutAttributes *> *arr = [super layoutAttributesForElementsInRect:rect].copy;
return arr;
}
这里同样有个问题需要说明,就是苹果规定,我们不能直接去拿系统生成这些原有的布局属性,对这些原有的布局进行更改,直接更改会报错(我们不展开去解释这个问题),所以我用了.copy,其实这样用也不好,在你运行的时候,还是会给你一堆警告
Logging only once for UICollectionViewFlowLayout cache mismatched frame
UICollectionViewFlowLayout has cached frame mismatch for index path <NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0} - cached value: {{82.666666666666686, 163.66667320702987}, {248.39999999999998, 408.88887580816248}}; expected value: {{82.666666666666671, 163.66666666666666}, {248.39999999999998, 408.88888888888891}}
This is likely occurring because the flow layout subclass CardFlowLayout is modifying attributes returned by UICollectionViewFlowLayout without copying them
就是告诉你这样不好,但是通常好像也不会出错,但是为了避免这样,我们可以干脆做一个拷贝的方法
- (NSArray *)getCopyOfAttributes:(NSArray *)attributes {
NSMutableArray *copyArr = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *attribute in attributes) {
[copyArr addObject:[attribute copy]];
}
return copyArr;
}
这样就ok了。
接着写我们的代码,这里单独说一点collectionView的位置不是屏幕大小的位置,它是内部展开的位置
//获取屏幕中线
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width / 2.0;
//拿到每个属性
for (UICollectionViewLayoutAttributes *attr in arr) {
//布局属性与中线的距离
CGFloat distance = fabs(attr.center.x - centerX);
//距离与屏幕宽的比例(为了计算缩放的比例)
CGFloat disScale = distance / self.collectionView.bounds.size.width;
//确定缩放的大小
CGFloat scale = fabs(cos(disScale * M_PI / 4));
//对布局属性进行缩放变换
//如果想要3D倾斜的变化,也可以在这里指定为3D变化
attr.transform = CGAffineTransformMakeScale(1.0, scale);
//同时也利用这个比例对透明度进行一下更改,显得自然
attr.alpha = scale;
}
3月-12-2018 17-00-40.gif但是这样运行以后我们又发现一个问题,就是他们好像是一顺缩小的,越来越小,一看我们的代码,是啊,我们设置的就是把属性缩小了,缩小的属性的后面的属性就变得更小了。所以我们要调整代码,让他们下次恢复之前的约束,然后再进行变化,刚好有这样一个方法:- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//判定为布局需要被无效化并重新计算的时候,布局对象会被询问以提供新的布局
所以我们只需要在这里返回YES就好了。
这样这篇文章就可以完了
再下面就要对cell内部进行复杂操作了。。。