你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> SwipeTableView:搞定半糖首頁列表布局效果

SwipeTableView:搞定半糖首頁列表布局效果

編輯:IOS開發基礎

logo.jpg

這是一個實現類似半糖首頁、QQ音樂列表、美麗說首頁、格瓦斯電影詳情頁效果(既能上下滾動,同時又能左右滑動)的控件。

項目地址GitHub:https://github.com/Roylee-ML/SwipeTableView

說起這個項目,還是得談一下一開始寫這個項目的緣由。前一陣子,公司項目首頁改版,要求作出半糖首頁的效果。看了一眼半糖之後,心中一萬只草泥馬奔過,怎麼會做這種設計?後來,想了一天的時間,終於把大概的實現原理捋順出來,又花了幾天的時間來一步步的實現,解決bug。最後終於可以實現上下與左後滑動同時兼容的效果了。由於只是首頁改版,所以只是首頁實現了效果,也並沒有單獨寫一個控件,可是,一周之後,悲劇的事發生了:新的產品設計中有幾個頁面都是這樣的設計。好吧,還是乖乖的寫個控件吧。於是,就有了SwipeTableView。

先來幾張預覽吧:

screenshot2.gif

screenshot3.gif

再來說一下實現原理:

最初的設計並沒有考慮兼容第三方下拉刷新的問題,所以最初的設計更簡潔,擴展性也更好。下面是原理的結構圖——

SwipeTableViewStruct1.jpg

首先,為了實現左右滑動的功能,需要一個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物理引擎的方式解決了這個問題。參考文章 英文博客

最初開源這個項目並沒有想會很多人使用,而後期,實際用到人越來越多,大家也都反映項目不能支持下拉刷新的問題,所以,便有了第二種設計方式——

SwipeTableViewStruct2.jpg

在這個版本中,跟上面的結構原理是差不多的。由於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;
}

總結

這是我的第一個比較完善的開源項目,在這個項目中,非常感謝使用者的認同與支持。通過這個項目,自己確實學到了很多東西。個人覺得,對於開發者而言,能夠參與一個開源項目,並去不斷的完善解決問題,從中得到的受益將是非常大的。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved