這是一個實現類似半糖首頁、QQ音樂列表、美麗說首頁、格瓦斯電影詳情頁效果(既能上下滾動,同時又能左右滑動)的控件。
項目地址GitHub:https://github.com/Roylee-ML/SwipeTableView
說起這個項目,還是得談一下一開始寫這個項目的緣由。前一陣子,公司項目首頁改版,要求作出半糖首頁的效果。看了一眼半糖之後,心中一萬只草泥馬奔過,怎麼會做這種設計?後來,想了一天的時間,終於把大概的實現原理捋順出來,又花了幾天的時間來一步步的實現,解決bug。最後終於可以實現上下與左後滑動同時兼容的效果了。由於只是首頁改版,所以只是首頁實現了效果,也並沒有單獨寫一個控件,可是,一周之後,悲劇的事發生了:新的產品設計中有幾個頁面都是這樣的設計。好吧,還是乖乖的寫個控件吧。於是,就有了SwipeTableView。
先來幾張預覽吧:
再來說一下實現原理:
最初的設計並沒有考慮兼容第三方下拉刷新的問題,所以最初的設計更簡潔,擴展性也更好。下面是原理的結構圖——
首先,為了實現左右滑動的功能,需要一個view來作為所有單一item的父視圖載體,並提供左右滑動的功能,這裡沒有比UICollectionView再合適的了。所有,我用一個CollectionView作為contentView。
有了CollectionView作為載體之後,就可以把每一個ScrollView作為CollectionView的item平鋪了。之後,最關鍵的問題,也是最難處理的問題,就是在滑動CollectionView的時候,怎樣保證前後item能夠對齊呢(因為懸停,只有對齊才行)?最後想到的解決辦法,就是在cellForItem的方法中,對前後兩個item的contentOffset進行adjust一致性調整。這樣能實現前後兩個item的位置是合理的。
最後一個主要的問題就是,多個item共用一個header與bar的問題。既然共用,最後想到,那就讓header與bar與CollectionView一樣作為SwipeTableView的子視圖,並別在圖層最上面。由於是獨立的view,之後就要解決當前item滑動同時滾動對header與bar做跟隨處理的問題了。在這裡,對當前的item進行KVO,在item的contentOffset發生變化的時候,同樣改變header與bar的位置,使之總是跟隨scrollview的滾動,並在懸停的位置做判斷,固定bar的frame的y值。
這樣,基本的實現就完成了。同時,SwipeTableView允許自適應contentSize,就是在item的內容很少的時候,也能自適應的調整contentSize,保整至少能夠滾動到頂端。
後期,用戶的反饋header不能滑動,最後通過UIKitDynamic物理引擎的方式解決了這個問題。參考文章 英文博客
最初開源這個項目並沒有想會很多人使用,而後期,實際用到人越來越多,大家也都反映項目不能支持下拉刷新的問題,所以,便有了第二種設計方式——
在這個版本中,跟上面的結構原理是差不多的。由於header與bar是獨立的,那麼每個item就要為header與bar的空間留出空白。而在第一個設計中,是通過修改增加每個item contentInset的top值來留出頂部的留白。而幾乎所有的下拉刷新空間都是不考慮inset,直接設置在content的頂部的,而contentInset是不算內容中的(一般下拉刷新控件rame的y值都是自身高度的負值)。所以,在添加了下拉刷新之後,下拉刷新組件其實是藏在header與bar的下面的,底部也正好跟bar對齊(這裡可以通過調整下拉刷新組件的frame,減小y值,來顯露下拉組件即可)。
為了方便使用,並且兼容第三方下拉刷新,最後采用,每個item頂部的留白由tableHeaderVeiw代替(CollectionView方面要繼承STCollectionView設置collectionHeaderView)。不過這樣,item的tableHeaderView就是占用的了,由於考慮項目的簡潔性,並沒有自定義UIScrollView支持scrollview設置headerview。這樣用戶完全可以只是通過設置一個宏就可以支持下拉刷新,更加方便
@define ST_PULLTOREFRESH_HEADER_HEIGHT xx
其中xx是指下拉刷新組件RefreshHeader的高度,也就是完全顯露RefreshHeader開始刷新的高度(如:MJRefresh 為 MJRefreshHeaderHeight,SVPullToRefresh 為 SVPullToRefreshViewHeight)。
最後在混合模式的時候出現了問題,最終發現,原來UICollectionView不支持通過contentSize屬性來改變contentSize。最後只好自定義STCollectionView,即支持設置collectionHeaderView,又可以實現自適應contentSize。關於STCollectionView的使用可以詳細看github示例。
最後貼上使用示例:
初始化並設置header與bar
self.swipeTableView = [[SwipeTableView alloc]initWithFrame:[UIScreen mainScreen].bounds]; _swipeTableView.delegate = self; _swipeTableView.dataSource = self; _swipeTableView.shouldAdjustContentSize = YES; _swipeTableView.swipeHeaderView = self.tableViewHeader; _swipeTableView.swipeHeaderBar = self.segmentBar;
實現數據源代理:
- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView { return 4; } - (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view { UITableView * tableView = view; if (nil == tableView) { UITableView * tableView = [[UITableView alloc]initWithFrame:swipeView.bounds style:UITableViewStylePlain]; tableView.backgroundColor = [UIColor whiteColor]; ... } // 這裡刷新每個item的數據 [tableVeiw refreshWithData:dataArray]; ... return tableView; }
STCollectionView使用方法:
MyCollectionView.h @interface MyCollectionView : STCollectionView @property (nonatomic, assign) NSInteger numberOfItems; @property (nonatomic, assign) BOOL isWaterFlow; @end MyCollectionView.m - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { STCollectionViewFlowLayout * layout = self.st_collectionViewLayout; layout.minimumInteritemSpacing = 5; layout.minimumLineSpacing = 5; layout.sectionInset = UIEdgeInsetsMake(5, 5, 5, 5); self.stDelegate = self; self.stDataSource = self; [self registerClass:UICollectionViewCell.class forCellWithReuseIdentifier:@"item"]; [self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header"]; [self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer"]; } return self; } - (NSInteger)collectionView:(UICollectionView *)collectionView layout:(STCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section { return _numberOfColumns; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return CGSizeMake(0, 100); } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { return CGSizeMake(kScreenWidth, 35); } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { return CGSizeMake(kScreenWidth, 35); } - (UICollectionReusableView *)stCollectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { UICollectionReusableView * reusableView = nil; if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header" forIndexPath:indexPath]; // custom UI...... }else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer" forIndexPath:indexPath]; // custom UI...... } return reusableView; } - (NSInteger)numberOfSectionsInStCollectionView:(UICollectionView *)collectionView { return _numberOfSections; } - (NSInteger)stCollectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return _numberOfItems; } - (UICollectionViewCell *)stCollectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"item" forIndexPath:indexPath]; // do something ....... return cell; }
總結
這是我的第一個比較完善的開源項目,在這個項目中,非常感謝使用者的認同與支持。通過這個項目,自己確實學到了很多東西。個人覺得,對於開發者而言,能夠參與一個開源項目,並去不斷的完善解決問題,從中得到的受益將是非常大的。