• Stars
    star
    160
  • Rank 234,703 (Top 5 %)
  • Language
    Objective-C
  • Created almost 11 years ago
  • Updated over 9 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

iOS7 Safari-like tabs

WKPagesCollectionView

我尝试想做一个类似 iOS7 下的 safari tabs 页面那样的效果。

  • 有页面翻转的效果;
  • 点击一个页面变成正常的显示状态;
  • 往左划动会删除这个cell;
  • 可以在底部添加新的cell;

点击查看效果视频

效果视频]

##使用

  • 项目中添加 WKPagesCollectionView目录以及下面的WK.h, WKPagesCollectionView.h, WKPagesCollectionView.m, WKPagesCollectionViewCell.h, WKPagesCollectionViewCell.m, WKPagesCollectionViewFlowLayout.h, WKPagesCollectionViewFlowLayout.m;

  • 引用 WKPagesCollectionView;

  • 准备数据

      _array=[[NSMutableArray alloc]init];
      for (int a=0; a<=30; a++) {
          [_array addObject:[NSString stringWithFormat:@"button %d",a]];
      }
    
  • 创建collectionView

      _collectionView=[[[WKPagesCollectionView alloc]initWithPagesFlowLayoutAndFrame:CGRectMake(0.0f, 0.0f, self.view.frame.size.width, self.view.frame.size.height)] autorelease];
      _collectionView.dataSource=self;
      _collectionView.delegate=self;
      [_collectionView registerClass:[WKPagesCollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
      [self.view addSubview:_collectionView];
      _collectionView.maskShow=YES;
    
  • 完成WKPagesCollectionViewDataSource(继承自UICollectionViewDataSource) 和 WKPagesCollectionViewDelegate(继承自UICollectionViewDelegate), 除了要完成UICollectionViewDataSource中提供数据的方法外,还要完成追加数据的方法-(void)willAppendItemInCollectionView:(WKPagesCollectionView *)collectionView 和删除数据的方法 -(void)collectionView:(WKPagesCollectionView *)collectionView willRemoveCellAtNSIndexPath:(NSIndexPath *)indexPath

      #pragma mark - UICollectionViewDataSource and UICollectionViewDelegate
      -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
          return _array.count;
      }
      -(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
      //    NSLog(@"cellForItemAtIndexPath:%d",indexPath.row);
          static NSString* identity=@"cell";
          WKPagesCollectionViewCell* cell=(WKPagesCollectionViewCell*)[collectionView dequeueReusableCellWithReuseIdentifier:identity forIndexPath:indexPath];
          cell.collectionView=collectionView;
          UIImageView* imageView=[[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"image-0"]] autorelease];
          imageView.frame=self.view.bounds;
          [cell.cellContentView addSubview:imageView];
          UIButton* button=[UIButton buttonWithType:UIButtonTypeCustom];
          button.frame=CGRectMake(0, (indexPath.row+1)*10+100, 320, 50.0f);
          button.backgroundColor=[UIColor whiteColor];
          [button setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
          [button setTitle:_array[indexPath.row] forState:UIControlStateNormal];
          [button addTarget:self action:@selector(onButtonTitle:) forControlEvents:UIControlEventTouchUpInside];
          [cell.cellContentView addSubview:button];
          return cell;
      }
      #pragma mark WKPagesCollectionViewDataSource
      ///追加数据
      -(void)willAppendItemInCollectionView:(WKPagesCollectionView *)collectionView{
          [_array addObject:@"new button"];
      }
      ///删除数据
      -(void)collectionView:(WKPagesCollectionView *)collectionView willRemoveCellAtNSIndexPath:(NSIndexPath *)indexPath{
          [_array removeObjectAtIndex:indexPath.row];
      }
    
  • 添加一个页面时的操作

      -(IBAction)onButtonAdd:(id)sender{
          [_collectionView appendItem];
      }
    
  • 展开显示一个具体的页面,这个在点击页面的时候会自动实现,也可以像下面这样代码调用;

      [_collectionView showCellToHighLightAtIndexPath:indexPath completion:^(BOOL finished) {
          NSLog(@"highlight completed");
      }];
    
  • 停止展开,回到普通的滚动模式

      -(IBAction)onButtonTitle:(id)sender{
          NSLog(@"button");
          [_collectionView dismissFromHightLightWithCompletion:^(BOOL finished) {
              NSLog(@"dismiss completed");
          }];
      }
    

##TODO

  • bug 每滚动几个时候顶上的那一个就会先看不见然后又突然出现了,还没想到原因 这个问题已经解决,@Nikolay Abelyashev 修复了这个bug,原因是由于WKPagesCollectionView的高度太小,而UICollectionView会把屏幕外的cell不在显示,在WKPagesCollectionView中,屏幕外的3个cell会被取消显示,解决的办法是修改WKPagesCollectionView的frame,使他高出window(这里添加了 topOfScreen:120.0f),然后把frame.origin.y也往上移动了那么多距离,这样WKPagesCollectionView其实就更大;

##实现的方式 ###实现滚动

我使用了UICollectionView来实现,本质上是一个垂直的列表,而主要的工作是来创造一个CollectionViewLayout, (我定义为WKPagesCollectionViewFlowLayout)每一个cell其实是和当前屏幕一样大小的,也就是说其实就是有一堆和屏幕一样大的cell错开折叠在一起,他们之间的间隔设置为self.minimumLineSpacing=-1*(self.itemSize.height-160.0f);不翻转cell时

为了实现翻转的效果,我在WKPagesCollectionViewFlowLayout中的 -(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect 中修改了transform3D。

看上去貌似比较简单,而如果所有的页面都是固定的角度的话,的确也没问题,但是我想像safari里那样在滚动时有点视差的效果,所以就为每个页面设置了不同的翻转角度了,其实就是在layoutAttributesForElementsInRect 中根据每个cell的位置来计算角度使得他们在滚动条滚动时有点不同的角度,下面这个是设置角度的方法:

	-(void)makeRotateTransformForAttributes:(UICollectionViewLayoutAttributes*)attributes{
	    attributes.zIndex=attributes.indexPath.row;///要设置zIndex,否则遮挡顺序会有编号
	    CGFloat distance=attributes.frame.origin.y-self.collectionView.contentOffset.y;
	    CGFloat normalizedDistance = distance / self.collectionView.frame.size.height;
	    normalizedDistance=fmaxf(normalizedDistance, 0.0f);
	    CGFloat rotate=RotateDegree+20.0f*normalizedDistance;
	    //CGFloat rotate=RotateDegree;
	    NSLog(@"makeRotateTransformForAttributes:row:%d,normalizedDistance:%f,rotate:%f",
	          attributes.indexPath.row,normalizedDistance,rotate);
	    ///角度大的会和角度小的cell交叉,即使设置zIndex也没有用,这里设置底部的cell角度越来越大
	    CATransform3D rotateTransform=WKFlipCATransform3DPerspectSimpleWithRotate(rotate);
	    attributes.transform3D=rotateTransform;
	    
	}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    NSLog(@"layoutAttributesForItemAtIndexPath:%d",path.row);
    UICollectionViewLayoutAttributes* attributes=[super layoutAttributesForItemAtIndexPath:path];
    [self makeRotateTransformForAttributes:attributes];
    return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSLog(@"layoutAttributesForElementsInRect:%@",NSStringFromCGRect(rect));
    NSArray* array = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* attributes in array) {
        [self makeRotateTransformForAttributes:attributes];
    }
    return array;
}

现在运行的时候,滚动起来就和想要的效果差不多了,虽然不如safari那么细节完美,大概的意思是达到了。

##实现删除

后面我想做出safari那样按住其中一个cell往左滑动就删除的效果,在每个cell里面添加一个scrollView,然后scrollViewDidEndDragging 中达到一定距离的时候就触发删除好了,而UICollectionView中的performBatchUpdates就可以很好的完成删除的动画了。

删除时的效果

	-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
	    if (self.showingState==WKPagesCollectionViewCellShowingStateNormal){
	        if (scrollView.contentOffset.x>=90.0f){
	            NSIndexPath* indexPath=[self.collectionView indexPathForCell:self];
	            NSLog(@"delete cell at %d",indexPath.row);
	            //self.alpha=0.0f;
	            ///删除数据
	            id<WKPagesCollectionViewDataSource> pagesDataSource=(id<WKPagesCollectionViewDataSource>)self.collectionView.dataSource;
	            [pagesDataSource collectionView:(WKPagesCollectionView*)self.collectionView willRemoveCellAtNSIndexPath:indexPath];
	            ///动画
	            [self.collectionView performBatchUpdates:^{
	                [self.collectionView deleteItemsAtIndexPaths:@[indexPath,]];
	            } completion:^(BOOL finished) {
	                
	            }];
	        }
	    }
	}

为了添加和删除的时候动画的好看一点,我们还得修改 (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath-(UICollectionViewLayoutAttributes*)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath

我在WKPagesCollectionViewFlowLayout 中添加了insertIndexPaths 和 deleteIndexPaths 来记录用来添加和删除的位置,因为这个两个回调在添加和删除时会被调用,而且不仅仅是针对正在添加或者删除的NSIndexPath,其他行也会被调用,而我们这里只要处理正在添加和删除的NSIndexPath;

	-(UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath{
	    UICollectionViewLayoutAttributes* attributes=[super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
	    NSLog(@"initialLayoutAttributesForAppearingItemAtIndexPath:%d",itemIndexPath.row);
	    if ([self.insertIndexPaths containsObject:itemIndexPath]){
	        if (!attributes)
	            attributes=[self layoutAttributesForItemAtIndexPath:itemIndexPath];
	        CATransform3D rotateTransform=WKFlipCATransform3DPerspectSimpleWithRotate(-90.0f);
	        attributes.transform3D=rotateTransform;
	    }
	    return attributes;
	}
	-(UICollectionViewLayoutAttributes*)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath{
	    NSLog(@"finalLayoutAttributesForDisappearingItemAtIndexPath:%d",itemIndexPath.row);
	    UICollectionViewLayoutAttributes* attributes=[super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
	    if ([self.deleteIndexPaths containsObject:itemIndexPath]){
	        if (!attributes){
	            attributes=[self layoutAttributesForItemAtIndexPath:itemIndexPath];
	        }
	        CATransform3D moveTransform=CATransform3DMakeTranslation(-320.0f, 0.0f, 0.0f);
	        attributes.transform3D=CATransform3DConcat(attributes.transform3D, moveTransform);
	    }    
	    return attributes;
	    
	}

开始的时候会发生一些意想不到的动画效果,滚动到底部,然后在模拟器下按下command+t打开慢速动画的时候就看的很清楚了,往左滑动来删除最后两个cell,在删除时一开始的效果是正常的,而几乎在完成之后,会看到一闪,出现了两个cell在很奇怪的位置,然后又动画慢慢回到预订的位置。

现在的问题就是,如果我删除带有button-0,button-1,button-2,button-3这样的cell,动画是正常的,但是如果我删除最后几个cell,带有button-6,button-7,button-8的,就会出现意想不到的动画。

而且我发现如果我的cell的翻转角度如果是全部固定的,那在删除cell时是不会发生奇怪的动画的,我的makeRotateTransformForAttributes2中是指定了一个固定的角度。

####Fixed

后来终于知道问题在哪里了,只是由于contentSize计算错误导致内容区域不够所以在删除cell后又自动滚动时产生了奇怪的行为,所以-(CGSize)collectionViewContentSize中的高度一定要正确。

###实现添加页面

现在页面有两种状态,一种就是这种普通的滚动页面的状态,当点击某一个页面的时候,他会翻转到全屏显示,这时我称作是highlight。如果只是在普通的滚动状态下,会先滚动到屏幕地步,然后添加一个页面,之后又会把这个页面展开到highLight显示状态。而如果现在整个collectionView本身就已经有一个页面在hightLight了,那应该先退回到普通状态,再重复之前的添加页面过程。

下面是在WKPagesCollectionView中的添加页面的方法;

	///追加一个页面
	-(void)appendItem{
	    if (self.isHighLight){
	        [self dismissFromHightLightWithCompletion:^(BOOL finished) {
	            [self _addNewPage];
	        }];
	    }
	    else{
	        [self _addNewPage];
	    }
	}
	///添加一页
	-(void)_addNewPage{
	    int total=[self numberOfItemsInSection:0];
	    [self scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:total-1 inSection:0] atScrollPosition:UICollectionViewScrollPositionBottom animated:YES];
	    double delayInSeconds = 0.3f;
	    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
	    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
	        ///添加数据
	        [(id<WKPagesCollectionViewDataSource>)self.dataSource willAppendItemInCollectionView:self];
	        int lastRow=total;
	        NSIndexPath* insertIndexPath=[NSIndexPath indexPathForItem:lastRow inSection:0];
	        [self performBatchUpdates:^{
	            [self insertItemsAtIndexPaths:@[insertIndexPath]];
	        } completion:^(BOOL finished) {
	            double delayInSeconds = 0.3f;
	            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
	            dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
	                [self showCellToHighLightAtIndexPath:insertIndexPath completion:^(BOOL finished) {
	                    
	                }];
	            });
	            
	        }];
	    });
	}