一直在500px上看照片,發照片。以前看它的首頁圖片展示就只是覺得好看,洋氣,也沒想過自己在iOS上實現一下。昨天不知怎麼的就開始想其中的算法了,現在我把思考的過程在這裡貼出來分享一下,如果你有更好的算法歡迎探討。
最終我做出的效果是這樣的:
垂直滾動
水平滾動
算法總體思路
先說一下總體上的思路。既然圖片的大小、位置各不一樣,我們很自然地會想到需要算出每個item的frame,然後把這些frame賦值給當前item的UICollectionViewLayoutAttributes。
自定義UICollectionViewLayout的關鍵兩步是先後重載下面兩個方法:
- (void)prepareLayout;
和
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
所以我們的思路是在- (void)prepareLayout;方法中算出所有item的frame,並賦值給當前item的 UICollectionViewLayoutAttributes。用圖片的形式比較直觀:
接下來問題就化歸到了如何求每個item的frame。
這裡我們抽象出一個 列 的概念:
除此之外,我們還需要維護一個存儲高度的數組COLUMNSHEIGHTS。數組中有n個元素,n表示所有列數,如上圖,n = 3。緩存的值表示當前列的高度,上圖的例子中,COLUMNSHEIGHTS = [104,123,89]。
然後我們把item逐個放入列中,以這樣的規則:從左到右,item優先放入COLUMNSHEIGHTS中最短的列。
打個比方,下一個item就應該放入最短的列,也就是第三列:
以此規則,循環下去,直到所有item都放置完畢。
細節
item.frame.origin:
用自然語言描述,
坐標x應該是這樣的:(最短列的編號-1)x 列寬
坐標y應該是這樣的:從COLUMNSHEIGHTS中取出最短列對應的高度
所以我們需要一個算法來找出當前COLUMNSHEIGHTS中的最短的列,最直接的方法就是0(n)時間復雜度的循環比較,這裡還好因為數據量比較少,如果遇到數據量大的情況可能就需要考慮分治法了。
//尋找此時高度最短的列.第一列為0 -(NSUInteger)findShortestColumn{ NSUInteger shortestIndex = 0; CGFloat shortestValue = MAXFLOAT; NSUInteger index=0;//游標 for (NSNumber *columnHeight in self.COLUMNSHEIGHTS) { if ([columnHeight floatValue] < shortestValue) { shortestValue = [columnHeight floatValue]; shortestIndex = index; } index++; } return shortestIndex; }
找到了最短列,表達出item的x坐標和y坐標就很容易了:
NSUInteger origin_x = [self findShortestColumn] * [self columnWidth]; NSUInteger origin_y = [self.COLUMNSHEIGHTS[shtIndex] integerValue] ;
item.frame.size.width:
由於列數是有用戶決定的,所以是個變量,由此可以獲得列寬columnWidth = self.collectionView.bounds.size.width / self.columnsCount
然後我們規定,默認情況下item的寬度等於columnWidth。當滿足當前列和下一列(如上圖紅色方塊,就是屬於當前列位於列2,下一列列3)高度相等時,可以橫跨兩欄。(再看紅色方塊,因為在它放進去之前,第二列高度為0,第三列高度也為0,滿足橫跨的條件)
但是!
如果出現了下面的這種情況:
也就是說,單單滿足當前列和下一列高度相等還不夠,因為只要一旦滿足這個條件,接下去將會一直是橫跨的狀態。所以我們還需要再加一個條件來篩選這些即使滿足當前列和下一列高度相等的item。我們可以用隨機數:
NSUInteger randomOfWhetherDouble = arc4random() % 100;//隨機數標記是否要雙行
arc4random() % 100;會隨機生成一個0~100的整數。然後我們設定一個阈值,比如40.只有當同時滿足 當前列和下一列高度相等 和 randomOfWhetherDouble < 40 的item才能真正實現跨行。換句話說,即使當前列和下一列高度相等,也只有百分之40的幾率出現跨行的item,這樣就很好的保證了寬度不一的item隨機出現!
所以寬度的代碼是:
if (shtIndex < self.columnsCount - 1 && [self.COLUMNSHEIGHTS[shtIndex] floatValue] == [self.COLUMNSHEIGHTS[shtIndex+1] floatValue] && randomOfWhetherDouble < 40) { size_width = 2*[self columnWidth]; }else{ size_width = [self columnWidth]; }
item.frame.size.height:
這個可以自由規定,因為在豎直方向高度沒有特別的限制。比如我規定:
1.如果是橫跨的,高度 = 寬度 * (0.75~1隨機)
float extraRandomHeight = arc4random() % 25; retVal = 0.75 + (extraRandomHeight / 100); size_height = size_width * retVal;
2.如果是單列的,高度 = 寬度 * (0.75~1.25隨機)
float extraRandomHeight = arc4random() % 50; retVal = 0.75 + (extraRandomHeight / 100); size_height = size_width * retVal; // 高度為寬度的0.75~1.25倍
補充:
實際測試中發現,即使把出現橫跨item的阈值調成0,也就是只要滿足 當前列和下一列高度相等,100%出現橫跨的情況,出現橫跨的情況也是少之又少。為什麼呢?原因出在了數據類型上,之前我的用的數據類型全是CGFloat或者float的浮點類型,兩個浮點數要相等的概率可想而知。改成NSUInteger之後就好多了。除此之外,為了增加橫跨情況出現的概率,我還用到了四捨五入。拿圖舉個例子:
我們讓item的高度對某個整數取余,比如以40為單位,讓高度對40取余,再讓item的高度剪掉這些余數。剩下的高度肯定是40的整數倍。
代碼很簡單:
size_height = size_height - (size_height % 40);
這可以把某個范圍內的高度都歸約到用一個高度,也就讓左右兩列高度相等的概率增加了,出現橫跨item的可能性也變大了。
然後,在循環的過程中把每個item的frame賦值給對應的attributes,並把這些attributes保存到一個數組裡。
//給attributes.frame 賦值,並存入 self.itemsAttributes NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0]; UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; attributes.frame = CGRectMake(origin_x, origin_y, size_width, size_height); [self.itemsAttributes addObject:attributes];
然後在layoutAttributesForElementsInRect方法中返回:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{ return self.itemsAttributes; }
最後
為了能讓collectionView滑起來,我們還需要設置它的ContentSize.其實只要讓ContentSize的高度變成COLUMNSHEIGHTS中最長列的高度即可。至於這個求數組中最長列的算法,和前面的求最短列類似。
-(CGSize)collectionViewContentSize{ CGSize size = self.collectionView.bounds.size; NSUInteger longstIndex = [self findLongestColumn]; float columnMax = [self.COLUMNSHEIGHTS[longstIndex] floatValue]; size.height = columnMax; return size; }
結語
如果你有興趣,還可以試試
讓圖片在豎屏和橫屏時擁有不同的列數,並以過渡動畫切換。
實現contentView的水平滾動並實現上面的不規則布局。
以上兩個功能我已經實現並進行了封裝,你可以像普通的UICollectionViewLayout一樣使用。可以在這裡使用和學習。