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

  • 相关阅读:
    SQLSERVER FUNCTIONS
    批处理命令学习
    java回调
    android布局中容易混淆的几个属性
    UML类图关系
    android面试试题
    Handler基础
    Android中自定义属性的使用
    php中钩子函数&实现
    Handler高级
  • 原文地址:https://www.cnblogs.com/langtianya/p/3903570.html
Copyright © 2011-2022 走看看