本以为很简单的一个功能,深入学习过后才发现细节满满,首先看看iPhone手机的手势多选的gif,根据gif整理出给个细节后再去仿出这样的效果。
一:梳理逻辑(如图)
- 1.1,获取起始点:获取起始点可以通过手势的状态获取:
panGesture.state == UIGestureRecognizerStateBegan
,这个状态下获取到CGPoint即是起始点的坐标。
if (panGesture.state == UIGestureRecognizerStateBegan) {
// 获取起始点
self.panSelectStartPoint = [panGesture locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:self.panSelectStartPoint];
}
- 1.2,获取结束点:同样我们也是根据手势的
state
来获取对应的结束点,这里有个细节需要注意,即需要判断当前手指是否在self.collectionView
的item
上,如果不在就不允许手势滑动,这样做的目的是避免和self.collectionView的手势冲突,也是更好的用户体验,更重要的是我们添加的手势是添加到self.view
上的,而不是self.collectionView
,原因是会和self.collectionView
的UIScrollView
产生手势冲突,导致不能上下滑动。如下代码:
if (panGesture.state == UIGestureRecognizerStateChanged) {
NSIndexPath *firstIndexPath = [self.collectionView indexPathForItemAtPoint:self.panSelectStartPoint];
if (!firstIndexPath) {// 起始点不在cell上直接不可滑动选择
return;
}
NSIndexPath *lastIndexPath = [self.collectionView indexPathForItemAtPoint:currentPoint];
if (!lastIndexPath) {
return;
}
}
- 1.3,手势的方向:手势的方向可以根据结束点对应cell的
minX
、maxX
、minY
、maxY
来获取对应最新选中的item。
CGPoint currentPoint = [panGesture locationInView:self.collectionView];
NSInteger firstLine = 0;
NSInteger lastLine = 0;
NSIndexPath *firstIndexPath = [self.collectionView indexPathForItemAtPoint:self.panSelectStartPoint];
if (!firstIndexPath) {
// 起始点不在cell上直接不可滑动选择
return;
}
NSIndexPath *lastIndexPath = [self.collectionView indexPathForItemAtPoint:currentPoint];
if (!lastIndexPath) {
return;
}
NSInteger rowCount = 3;
if ((firstIndexPath.item + 1) % rowCount == 0) {
firstLine = (firstIndexPath.item + 1) / rowCount;
}else {
firstLine = (firstIndexPath.item + 1) / rowCount + 1;
}
if ((lastIndexPath.item + 1) % rowCount == 0) {
lastLine = (lastIndexPath.item + 1) / rowCount;
}else {
lastLine = (lastIndexPath.item + 1) / rowCount + 1;
}
NSMutableArray *indexPaths = [NSMutableArray array];
CGFloat startX;
CGFloat maxX;
BOOL xReverse = NO;
if (currentPoint.x > self.panSelectStartPoint.x) {
// 向右
maxX = [self panSelectGetMaxXWithPoint:currentPoint];
startX = [self panSelectGetMinXWithPoint:self.panSelectStartPoint];
}else {
// 向左
xReverse = YES;
maxX = [self panSelectGetMaxXWithPoint:self.panSelectStartPoint];
startX = [self panSelectGetMinXWithPoint:currentPoint];
}
CGFloat maxY;
CGFloat startY;
BOOL yReverse = NO;
if (currentPoint.y > self.panSelectStartPoint.y) {
// 向下
maxY = [self panSelectGetMaxYWithPoint:currentPoint];
startY = [self panSelectGetMinYWithPoint:self.panSelectStartPoint];
}else {
// 向上
yReverse = YES;
maxY = [self panSelectGetMaxYWithPoint:self.panSelectStartPoint];
startY = [self panSelectGetMinYWithPoint:currentPoint];
}
NSInteger distanceW = self.layout.minimumInteritemSpacing + self.layout.itemSize.width;
NSInteger distanceH = self.layout.minimumInteritemSpacing + self.layout.itemSize.height;
NSIndexPath *sIndexPath = [self.collectionView indexPathForItemAtPoint:self.panSelectStartPoint];
if (sIndexPath && ![indexPaths containsObject:sIndexPath]) {
[indexPaths addObject:sIndexPath];
}
while (yReverse ? maxY > startY : startY < maxY) {
CGFloat tempStartX = startX;
CGFloat tempMaxX = maxX;
NSInteger currentLine = 0;
NSIndexPath *currentIndexPath;
if (yReverse) {
currentIndexPath = [self.collectionView indexPathForItemAtPoint:CGPointMake(tempMaxX - 1, maxY - 1)];
}else {
currentIndexPath = [self.collectionView indexPathForItemAtPoint:CGPointMake(tempStartX + 1, startY + 1)];
}
if ((currentIndexPath.item + 1) % rowCount == 0) {
currentLine = (currentIndexPath.item + 1) / rowCount;
}else {
currentLine = (currentIndexPath.item + 1) / rowCount + 1;
}
if (currentLine == firstLine) {
if (lastLine != firstLine) {
if (yReverse) {
tempMaxX = [self panSelectGetMaxXWithPoint:self.panSelectStartPoint];
tempStartX = 2;
}else {
if (xReverse) {
tempStartX = [self panSelectGetMinXWithPoint:self.panSelectStartPoint];
}
tempMaxX = SCREEN_WIDTH - 2;
}
}
}else if (currentLine == lastLine) {
if (yReverse) {
tempStartX = [self panSelectGetMinXWithPoint:currentPoint];
tempMaxX = SCREEN_WIDTH - 2;
}else {
tempStartX = 2;
if (xReverse) {
tempMaxX = [self panSelectGetMaxXWithPoint:currentPoint];
}
}
}else if (currentLine != firstLine && currentLine != lastLine) {
tempStartX = 2;
tempMaxX = SCREEN_WIDTH - 2;
}
while (yReverse ? tempMaxX > tempStartX : tempStartX < tempMaxX) {
NSIndexPath *indexPath;
if (yReverse) {
indexPath = [self panSelectCurrentIndexPathWithPoint:CGPointMake(tempMaxX, maxY) indexPaths:indexPaths];
}else {
indexPath = [self panSelectCurrentIndexPathWithPoint:CGPointMake(tempStartX, startY) indexPaths:indexPaths];
}
if (indexPath) {
[indexPaths addObject:indexPath];
}
if (yReverse) {
tempMaxX -= distanceW / 2;
}else {
tempStartX += distanceW / 2;
}
}
if (yReverse) {
maxY -= distanceH / 2;
}else {
startY += distanceH / 2;
}
}
NSIndexPath *eIndexPath = [self.collectionView indexPathForItemAtPoint:currentPoint];
if (eIndexPath && ![indexPaths containsObject:eIndexPath]) {
QYPhotoModel *model = self.sources[eIndexPath.item];
if (self.currentPanSelectType == 0) {
if (model.selected) {
[indexPaths addObject:eIndexPath];
}
}else if (self.currentPanSelectType == 1) {
if (!model.selected) {
[indexPaths addObject:eIndexPath];
}
}
}
if (self.currentPanSelectType == -1) {
NSIndexPath *firstIndexPath = indexPaths.firstObject;
QYPhotoModel *firstModel;
if (firstIndexPath) {
firstModel = self.sources[firstIndexPath.item];
self.currentPanSelectType = !firstModel.selected;
}
}
NSMutableArray *reloadSelectArray = [NSMutableArray array];
for (NSIndexPath *indexPath in indexPaths) {
QYPhotoModel *model = self.sources[indexPath.item];
if (self.currentPanSelectType == 0) {
// 取消选择
if (model.selected) {
[self.manager beforeSelectedListdeletePhotoModel:model];
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}
}else if (self.currentPanSelectType == 1) {
// 选择
if (!model.selected) {
[self.manager beforeSelectedListAddPhotoModel:model];
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}else {
QYCollectionViewCell *cell = (QYCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
if (![cell.selectBtn.currentTitle isEqualToString:model.selectIndexStr] || !cell.selectBtn.selected) {
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}
}
}
}
NSPredicate * filterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",indexPaths];
NSArray * filterArray = [self.panSelectIndexPaths filteredArrayUsingPredicate:filterPredicate];
for (NSIndexPath *indexPath in filterArray) {
QYPhotoModel *model = self.sources[indexPath.item];
if (self.currentPanSelectType == 0) {
if (!model.selected) {
[self.manager beforeSelectedListAddPhotoModel:model];
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}
}else if (self.currentPanSelectType == 1) {
if (model.selected) {
[self.manager beforeSelectedListdeletePhotoModel:model];
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}
}
}
if (self.currentPanSelectType == 0) {
for (QYPhotoModel *model in [self.manager selectedArray]) {
if (!model.dateCellIsVisible) {
continue;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self dateItem:model] inSection:0];
QYCollectionViewCell *cell = (QYCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
if (cell && ![cell.selectBtn.currentTitle isEqualToString:model.selectIndexStr]) {
if (![reloadSelectArray containsObject:indexPath]) {
[reloadSelectArray addObject:indexPath];
}
}
}
}
[reloadSelectArray addObjectsFromArray:[self getVisibleVideoCellIndexPathsWithCurrentIndexPaths:reloadSelectArray]];
if (reloadSelectArray.count) {
[self.collectionView reloadItemsAtIndexPaths:reloadSelectArray];
}
self.panSelectIndexPaths = indexPaths;
for (NSIndexPath *item in reloadSelectArray) {
NSLog(@"当前的刷新的item: %ld", item.item);
}
}else if (panGesture.state == UIGestureRecognizerStateEnded ||
panGesture.state == UIGestureRecognizerStateCancelled) {
self.panSelectIndexPaths = nil;
self.currentPanSelectType = -1;
}
#pragma mark - private
- (CGFloat)panSelectGetMaxXWithPoint:(CGPoint)point {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
return CGRectGetMaxX(cell.frame) - 2;
}
- (CGFloat)panSelectGetMinXWithPoint:(CGPoint)point {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
return cell.qy_x + 2;
}
- (CGFloat)panSelectGetMaxYWithPoint:(CGPoint)point {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
return CGRectGetMaxY(cell.frame) - 2;
}
- (CGFloat)panSelectGetMinYWithPoint:(CGPoint)point {
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
return cell.qy_y + 2;
}
注意点:如上gif,手势在滑动过程中有选中和取消选中,可以在model中自定一个一个
isSelected的BOOL值来记录item是否已被选中
。我的思路是新增一个manager管理类,用于记录当前手势是选中还是取消选中,最后给选中的item做个标记,便于设置item选中时设置一个白色蒙版(设置的标记和item的row是一样就选中或者取消选中)具体详情见demo即可。
- 1.4, manager管理类进行选中的标记
- (void)beforeSelectedDeleteItemPhotoModel:(QYPhotoModel *)model {
if (![self.selectedList containsObject:model]) {
return;
}
model.selected = NO;
model.selectIndexStr = @"";
model.selectedIndex = 0;
[self.selectedList removeObject:model];
int i = 0;
for (QYPhotoModel *model in self.selectedList) {
model.selectIndexStr = [NSString stringWithFormat:@"%d",i + 1];
i++;
}
}
- (void)beforeSelectedAddItemPhotoModel:(QYPhotoModel *)model {
[self.selectedList addObject:model];
model.selected = YES;
model.selectedIndex = [self.selectedList indexOfObject:model];
model.selectIndexStr = [NSString stringWithFormat:@"%ld",model.selectedIndex + 1];
// NSLog(@"添加的图片: %@", model.selectIndexStr );
}
- (NSArray *)selectedArray {
return self.selectedList;
}
二:最后效果图如下:
总结
功能整体思路大体就是这样,具体在计算手势方向,item标记等逻辑如上述代码展示,可细细理解。由于公司项目需要,需要深入学习和使用OpenGL ES的知识。接下来的目标是学习OpenGL ES、和音视频相关知识,也会不断将笔记更新,继续督促自己进步。