zoukankan      html  css  js  c++  java
  • PSCollectionView瀑布流实现

     

    PSCollectionView是一个实现较简洁的仿Pinterest瀑布流iOS版实现,使用UIScrollView做容器,每列列宽固定,高度可变,使用方式类似UITableView。
    其效果如图:

    一.基本原理

    其基本实现原理为:

    1. 列数固定,根据列数每列存储一个当前列的高度值。
    2. 每次插入数据块时,在当前最小高度的列里插入,然后更新当前列的高度为原有高度加上当前数据模块高度
    3. 重复2直到所有数据模块插入完毕
    4. 调整容器(UIScrollView)的高度为各列最大的高度值。

    二.具体实现

    1. 相关数据结构

    公共属性

    @property (nonatomic, retain) UIView *headerView;
    @property (nonatomic, retain) UIView *footerView;
    @property (nonatomic, retain) UIView *emptyView;
    @property (nonatomic, retain) UIView *loadingView;
    
    @property (nonatomic, assign, readonly) CGFloat colWidth;
    @property (nonatomic, assign, readonly) NSInteger numCols;
    @property (nonatomic, assign) NSInteger numColsLandscape;
    @property (nonatomic, assign) NSInteger numColsPortrait;
    @property (nonatomic, assign) id  collectionViewDelegate;
    @property (nonatomic, assign) id  collectionViewDataSource;
    • headerView,footerView,emptyView,loadingView分别对应列表头部,尾部,空白时,正在加载时要显示的视图。
      numColsLandscape,numColsPortrait为横屏和竖屏时的列数。
    • colWidth,numCols为只读属性,根据当前的视图方向,视图总大小,横屏和竖屏时的列数计算得出。
    • collectionViewDelegate,collectionViewDataSource为Delegate和数据源。

    私有属性

    @property (nonatomic, assign, readwrite) CGFloat colWidth;
    @property (nonatomic, assign, readwrite) NSInteger numCols;
    @property (nonatomic, assign) UIInterfaceOrientation orientation;
    
    @property (nonatomic, retain) NSMutableSet *reuseableViews;
    @property (nonatomic, retain) NSMutableDictionary *visibleViews;
    @property (nonatomic, retain) NSMutableArray *viewKeysToRemove;
    @property (nonatomic, retain) NSMutableDictionary *indexToRectMap;
    • 私有属性将colWidth,numCols定义为readwrite,便于内部赋值操作。
    • orientation为当前视图的方向,从UIApplicationstatusBarOrientation属性获取。
    • reuseableViews数据集存储可重用的的数据块视图,在数据块移除可见范围时将其放入reuseableViews中,当DataSource调用dequeueReusableView时,从reuseableViews取出一个返回。
    • visibleViews字典存储当前可见的数据块视图,key为数据块索引,当容器滚动时,将移除可见范围的数据块视图从visibleViews中移除,并放入reuseableViews中;当存在应该显示的数据块视图,但还未放入容器视图时,则从DataSource获取新的数据块视图,加入到容器视图中,同时将其加入到visibleViews中。
    • viewKeysToRemove数组在遍历visibleViews时存储应该移除的数据块视图Key。
    • indexToRectMap数据字典存储每个数据块(不管可不可见)在容器中的位置,将CGRect转换为NSString(NSStringFromCGRect)作为Value存储,Key为数据块的索引。



      2.视图更新方式

      • 在reloadData或视图方向发生变化时,需要重新计算所有数据块的位置并重新加载,见relayoutViews方法。
      • 当滑动容器时,UIScrollView会调用其layoutSubviews方法,若方向未变化,则不需要重新加载所有数据块,仅仅需要移除非可见的数据块,载入进入可见范围的数据块,见removeAndAddCellsIfNecessary方法.
      #pragma mark - DataSource
      
      - (void)reloadData {
          [self relayoutViews];
      }
      
      #pragma mark - View
      
      - (void)layoutSubviews {
          [super layoutSubviews];
      
          UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
          if (self.orientation != orientation) {
              self.orientation = orientation;
              [self relayoutViews];
          } else {
              [self removeAndAddCellsIfNecessary];
          }
      }

      3.relayoutViews方法

      relayoutViews会将现有的所有数据块视图清除,重新从DataSource获取数据,重新计算所有数据块视图的位置,容器的高度等。

      1. 首先遍历可见数据块视图字典visibleViews,将所有数据块视图放入reuseableViews中,并清空visibleViews,indexToRectMap。
      2. 将emptyView,loadingView从容器视图中移除。
      3. 从DataSource获取数据块的个数numViews。
      4. 若headerView不为nil,则将headerView视图加入到容器,更新top索引。
      5. 若numViews不为0,则依次计算每个数据块的位置。使用colOffsets存储每一列的当前高度,每次增加数据块时将其添加到高度最小的列中,所处的列确定后,其orig坐标就确定了,宽度固定,再从DataSource获取此数据块的高度,那么当前数据块的frame位置就确定了,将其转换为NSString(使用setObject:NSStringFromCGRect)存储到indexToRectMap字典中,以数控块索引为key;同时将当前列的高度更新,再继续处理下一数据块,还是加入到高度最小的列中,直至所有数据块处理完毕。
      6. 这时的总高度即最高列的高度。
      7. 若numViews为0,则将emptyView增加到容器中,总高度则为添加emptyView的高度。
      8. 若footerView不为nil,则将footerView加入到容器中.
      9. 这时的总高度totalHeight即为最终容器内容的总高度,将其赋值的UIScrollView的contentSize属性。
      10. 这时headerView和footView已加入到容器中,但所有的数据块只是计算了其应该处于的位置,并未实际放入容器中,调用removeAndAddCellsIfNecessary将当前可见的数据块视图加入到容器中。
      - (void)relayoutViews {
          self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape;
      
          // Reset all state
          [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
              PSCollectionViewCell *view = (PSCollectionViewCell *)obj;
              [self enqueueReusableView:view];
          }];
          [self.visibleViews removeAllObjects];
          [self.viewKeysToRemove removeAllObjects];
          [self.indexToRectMap removeAllObjects];
      
          if (self.emptyView) {
              [self.emptyView removeFromSuperview];
          }
          [self.loadingView removeFromSuperview];
      
          // This is where we should layout the entire grid first
          NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self];
      
          CGFloat totalHeight = 0.0;
          CGFloat top = kMargin;
      
          // Add headerView if it exists
          if (self.headerView) {
              self.headerView.top = kMargin;
              top = self.headerView.top;
              [self addSubview:self.headerView];
              top += self.headerView.height;
              top += kMargin;
          }
      
          if (numViews > 0) {
              // This array determines the last height offset on a column
              NSMutableArray *colOffsets = [NSMutableArray arrayWithCapacity:self.numCols];
              for (int i = 0; i < self.numCols; i++) {
                  [colOffsets addObject:[NSNumber numberWithFloat:top]];
              }
      
              // Calculate index to rect mapping
              self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols);
              for (NSInteger i = 0; i < numViews; i++) {
                  NSString *key = PSCollectionKeyForIndex(i);
      
                  // Find the shortest column
                  NSInteger col = 0;
                  CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue];
                  for (int i = 1; i < [colOffsets count]; i++) {
                      CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue];
      
                      if (colHeight < minHeight) {
                          col = i;
                          minHeight = colHeight;
                      }
                  }
      
                  CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth);
                  CGFloat top = [[colOffsets objectAtIndex:col] floatValue];
                  CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:i];
                  if (colHeight == 0) {
                      colHeight = self.colWidth;
                  }
      
                  if (top != top) {
                      // NaN
                  }
      
                  CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight);
      
                  // Add to index rect map
                  [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key];
      
                  // Update the last height offset for this column
                  CGFloat test = top + colHeight + kMargin;
      
                  if (test != test) {
                      // NaN
                  }
                  [colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]];
              }
      
              for (NSNumber *colHeight in colOffsets) {
                  totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight;
              }
          } else {
              totalHeight = self.height;
      
              // If we have an empty view, show it
              if (self.emptyView) {
                  self.emptyView.frame = CGRectMake(kMargin, top, self.width - kMargin * 2, self.height - top - kMargin);
                  [self addSubview:self.emptyView];
              }
          }
      
          // Add footerView if exists
          if (self.footerView) {
              self.footerView.top = totalHeight;
              [self addSubview:self.footerView];
              totalHeight += self.footerView.height;
              totalHeight += kMargin;
          }
      
          self.contentSize = CGSizeMake(self.width, totalHeight);
      
          [self removeAndAddCellsIfNecessary];
      }

      4.removeAndAddCellsIfNecessary方法

      removeAndAddCellsIfNecessary根据当前容器UIScrollViewcontentOffset,将用户不可见的数据块视图从容器中移除,将用户可见的数据块视图加入到容器中。

      1. 获得当前容器的可见部分。
        CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height);
      2. 逐个遍历visibleViews中的视图,使用CGRectIntersectsRect方法判断其frame与容器可见部分visibleRect是否有交集,若没有,则将其从visibleViews中去除,并添加到reuseableViews中。
      3. 对visibleViews剩余的数据块视图排序,获得其最小索引(topIndex)和最大索引(bottomIndex)。
      4. 将topIndex和bottomIndex分别向上和向下扩充bufferViewFactor*numCols个数据块索引。
      5. 从topIndex开始到bottomIndex判断索引对应的数据块视图的位置是否在容器的visibleRect范围内,以及其是否在visibleViews中。若其应该显示,而且不在visibleViews中,则向DataSource请求一个新的数据块视图,加到容器视图中,同时添加到visibleViews中。

      这样新的ScrollView可见区域就可以被数据块填充满。

      - (void)removeAndAddCellsIfNecessary {
          static NSInteger bufferViewFactor = 5;
          static NSInteger topIndex = 0;
          static NSInteger bottomIndex = 0;
      
          NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self];
      
          if (numViews == 0) return;
      
          // Find out what rows are visible
          CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height);
      
          // Remove all rows that are not inside the visible rect
          [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
              PSCollectionViewCell *view = (PSCollectionViewCell *)obj;
              CGRect viewRect = view.frame;
              if (!CGRectIntersectsRect(visibleRect, viewRect)) {
                  [self enqueueReusableView:view];
                  [self.viewKeysToRemove addObject:key];
              }
          }];
      
          [self.visibleViews removeObjectsForKeys:self.viewKeysToRemove];
          [self.viewKeysToRemove removeAllObjects];
      
          if ([self.visibleViews count] == 0) {
              topIndex = 0;
              bottomIndex = numViews;
          } else {
              NSArray *sortedKeys = [[self.visibleViews allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) {
                  if ([obj1 integerValue] < [obj2 integerValue]) {
                      return (NSComparisonResult)NSOrderedAscending;
                  } else if ([obj1 integerValue] > [obj2 integerValue]) {
                      return (NSComparisonResult)NSOrderedDescending;
                  } else {
                      return (NSComparisonResult)NSOrderedSame;
                  }
              }];
              topIndex = [[sortedKeys objectAtIndex:0] integerValue];
              bottomIndex = [[sortedKeys lastObject] integerValue];
      
              topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols));
              bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols));
          }
          //    NSLog(@"topIndex: %d, bottomIndex: %d", topIndex, bottomIndex);
      
          // Add views
          for (NSInteger i = topIndex; i < bottomIndex; i++) {
              NSString *key = PSCollectionKeyForIndex(i);
              CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]);
      
              // If view is within visible rect and is not already shown
              if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) {
                  // Only add views if not visible
                  PSCollectionViewCell *newView = [self.collectionViewDataSource collectionView:self viewAtIndex:i];
                  newView.frame = CGRectFromString([self.indexToRectMap objectForKey:key]);
                  [self addSubview:newView];
      
                  // Setup gesture recognizer
                  if ([newView.gestureRecognizers count] == 0) {
                      PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease];
                      gr.delegate = self;
                      [newView addGestureRecognizer:gr];
                      newView.userInteractionEnabled = YES;
                  }
      
                  [self.visibleViews setObject:newView forKey:key];
              }
          }
      }

      5.select方法

      其定义了一个UITapGestureRecognizer的子类PSCollectionViewTapGestureRecognizer来检测每个数据块的点击操作。
      从DataSource获取到一个新的数据块视图时,会检测里面是否已包含gesture recognizer对象,若没有则新创建一个PSCollectionViewTapGestureRecognizer对象放入,将delegate设为自身。

                  // Setup gesture recognizer
                  if ([newView.gestureRecognizers count] == 0) {
                      PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease];
                      gr.delegate = self;
                      [newView addGestureRecognizer:gr];
                      newView.userInteractionEnabled = YES;
                  }

      手势识别检测到点击时会向Delegate询问此点是否可接受(gestureRecognizer:shouldReceiveTouch:),若手势识别对象是PSCollectionViewTapGestureRecognizer类型,则是我们添加进去的。若该点所属的数据块视图可见,则接受此点,若不可见,则忽略。若手势识别对象不是PSCollectionViewTapGestureRecognizer对象,就不是我们放入的,则一直返回YES。

      - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
          if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES;
      
          NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
          NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
          NSString *key = [matchingKeys lastObject];
      
          if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
              return YES;
          } else {
              return NO;
          }
      }

      当检测到点击操作时,调用didSelectView:方法,在其中调用delegate的collectionView:didSelectView:atIndex:方法,传递参数为self对象,选择的数据块视图以及选择的数据块索引;

      - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer {
          NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
          NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
          NSString *key = [matchingKeys lastObject];
          if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
              if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) {
                  NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]);
                  [self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex];
              }
          }
      }

      这种方式还存在各种问题

      1. 若DataSource返回的数据块视图中已加入自己的UITapGestureRecognizer对象,则[newView.gestureRecognizers count]就不为0,在判断时PSCollectionView内部定义的PSCollectionViewTapGestureRecognizer就不会加入, 这样选择数据块视图的操作就不会触发。
      2. 实现的gestureRecognizer:shouldReceiveTouch:方法对非PSCollectionViewTapGestureRecognizer的对象直接返回YES。这样,如果子类化PSCollectionView重写gestureRecognizer:shouldReceiveTouch:方法时,如果调用super的此方法,则会直接返回,不会执行自己的定制化操作;若不调用super的此方法,则选择功能就会出差错。

      6.重用数据块视图机制

      NSMutableSet *reuseableViews;中存储可复用的数据块视图。dequeueReusableView从reuseableViews中任取一个视图返回,enqueueReusableView将数据块视图放入reuseableViews中。

      #pragma mark - Reusing Views
      
      - (PSCollectionViewCell *)dequeueReusableView {
          PSCollectionViewCell *view = [self.reuseableViews anyObject];
          if (view) {
              // Found a reusable view, remove it from the set
              [view retain];
              [self.reuseableViews removeObject:view];
              [view autorelease];
          }
      
          return view;
      }
      
      - (void)enqueueReusableView:(PSCollectionViewCell *)view {
          if ([view respondsToSelector:@selector(prepareForReuse)]) {
              [view performSelector:@selector(prepareForReuse)];
          }
          view.frame = CGRectZero;
          [self.reuseableViews addObject:view];
          [view removeFromSuperview];
      }

      代码:
      PSCollectionView.h
      PSCollectionView.m
      PSCollectionViewCell.h
      PSCollectionViewCell.m

      git工程:
      https://github.com/ptshih/PSCollectionView

      三.使用方法

      创建PSCollectionView对象

      self.collectionView = [[[PSCollectionView alloc] initWithFrame:self.view.bounds] autorelease];
      self.collectionView.delegate = self;
      self.collectionView.collectionViewDelegate = self;
      self.collectionView.collectionViewDataSource = self;
      self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

      设置列数

      // Specify number of columns for both iPhone and iPad
      if (isDeviceIPad()) {
          self.collectionView.numColsPortrait = 4;
          self.collectionView.numColsLandscape = 5;
      } else {
          self.collectionView.numColsPortrait = 2;
          self.collectionView.numColsLandscape = 3;
      }

      添加header,footer,empty,loader等视图

      UIView *loadingLabel = ...
      self.collectionView.loadingView = loadingLabel;
      UIView *emptyView = ...
      self.collectionView.emptyView = emptyView;
      UIView *headerView = ...
      self.collectionView.headerView = headerView;
      UIView *footerView = ...
      self.collectionView.footerView = footerView;

      实现Delegate和DataSource

      - (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndex:(NSInteger)index {
          NSDictionary *item = [self.items objectAtIndex:index];
      
          // You should probably subclass PSCollectionViewCell
          PSCollectionViewCell *v = (PSCollectionViewCell *)[self.collectionView dequeueReusableView];
          if (!v) {
              v = [[[PSCollectionViewCell alloc] initWithFrame:CGRectZero] autorelease];
          }
      
          [v fillViewWithObject:item]
      
          return v;
      }
      
      - (CGFloat)heightForViewAtIndex:(NSInteger)index {
          NSDictionary *item = [self.items objectAtIndex:index];
      
          // You should probably subclass PSCollectionViewCell
          return [PSCollectionViewCell heightForViewWithObject:item inColumnWidth:self.collectionView.colWidth];
      }
      
      - (void)collectionView:(PSCollectionView *)collectionView didSelectView:(PSCollectionViewCell *)view atIndex:(NSInteger)index {
          // Do something with the tap
      }

      四.其他瀑布流实现

      1.WaterflowView
      2.上拉刷新瀑布流PSCollectionViewEGOTableViewPullRefresh结合,增加上拉/下拉刷新效果。
      3.瀑布效果,不同的实现方式

      参考:
      PSCollectionView
      When does layoutSubviews get called?
      Overriding layoutSubviews when rotating UIView
      iPhone开发笔记 – 瀑布流布局
      瀑布流布局浅析
      说说瀑布流式网站里那些可人的小细节
      EGOTableViewPullRefresh

    本文出自 清风徐来,水波不兴 的博客,转载时请注明出处及相应链接。

    From: http://www.winddisk.com/2012/07/28/pscollectionview%E7%80%91%E5%B8%83%E6%B5%81%E5%A

  • 相关阅读:
    算法训练 表达式计算
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    New ways to verify that Multipath TCP works through your network
    TCP的拥塞控制 (Tahoe Reno NewReno SACK)
    Multipath TCP Port for Android 4.1.2
    How to enable ping response in windows 7?
    NS3
    Multipath TCP Port for Android
  • 原文地址:https://www.cnblogs.com/langtianya/p/3903570.html
Copyright © 2011-2022 走看看