本開源庫基於iOS8中PhotoKit框架製作,所以暫不支援iOS8以下版本,請諒解。另,在iOS10中,使用photoKit框架的應用可能會出現crash,但這個問題相信很快會被官方修復,請無需擔心。
效果圖
實際截圖
以下是原Idea作者的Dribble:Photo Picker Interaction 地址和本人的GitHub:CBImagePicker地址,希望大家點選支援,再次感謝。
下面我們來仔細的分析,做這個庫的完整過程。
第一部分 Category
整個庫的製作用到了大量的座標計算,所以我們很有必要寫一個category來簡化這一部分操作,我針對UIView的UIView+CBAddition類庫和針對UIImage的UIImage+CBAddition都是為了簡化這一部分內容而做的工作。
1 2 3 4 |
// Getter - (CGFloat)originLeft { return self.frame.origin.x; } |
1 2 3 4 5 6 |
// Setter - (void)setOriginLeft:(CGFloat)originLeft { if (!isnan(originLeft)) { self.frame = CGRectMake(originLeft, self.originUp, self.sizeWidth, self.sizeHeight); } } |
大都以設定屬性後自定義Getter和Setter的方式來進行,更詳細程式碼請點選檢視。
第二部分 ImagePicker(圖片選擇)
這個部分我們分小節來講。
TitleView
TitleView分為兩個View,第一個是Label,第二個則是UIImageView,這裡使用了蘋果原生NSLayoutConstraint來新增約束去限制兩個View之間的位置和尺寸的關係,使用方法如下:
1 |
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c; |
引數說明:
view1:設定的目標檢視 attr1:設定的目標檢視的屬性 relation:目的檢視和參照檢視之間的關係view2:設定的參照檢視 attr2:參照檢視的參照屬性 multiplier:目標檢視屬性和參照檢視屬性倍值 c:目標檢視屬性和參照檢視屬性差異值
CollctionView
相簿獲取
collection這個部分,主要是圖片資料的申請,這裡首先使用了PhotoKit來申請所有的相簿列表,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil]; PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil]; [smartAlbums enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL *stop) { PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:nil]; if (fetchResult.count > 0) { [self.assetsGroupArray addObject:collection]; } }]; [topLevelUserCollections enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL *stop) { PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:nil]; if (fetchResult.count > 0) { [self.assetsGroupArray addObject:collection]; } }]; |
這裡使用了一個陣列儲存所取到的所有相簿列表,相簿資訊儲存物件為PHAssetCollection。
相簿圖片獲取
而當我們取到相簿資訊之後,要從對應相簿中取到我們所需要顯示的圖片,那麼這裡我們根據index從上面的陣列中取出我們所需要的對應相簿,遍歷改相簿,再利用方法requestImageForAsset來請求圖片資訊,方法如下:
1 |
- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler; |
引數說明:
asset:單個的圖片資料 targetSize:請求的圖片尺寸 cotentMode:尺寸拉伸方式 options:附加資訊resultHandler:結果回撥
此處自定義了一個頭部檢視,新增方法如下:
註冊registerClass
1 |
[_imageCollectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionReusableView"]; |
設定dataSource
1 2 3 4 5 6 7 8 9 10 11 12 |
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { UICollectionReusableView *collectionHeardView; if (kind == UICollectionElementKindSectionHeader){ collectionHeardView = (UICollectionReusableView *)[collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionReusableView" forIndexPath:indexPath]; collectionHeardView.backgroundColor = [UIColor clearColor]; [collectionHeardView addSubview:_horizontalScrollView]; } return collectionHeardView; } |
當進行圖片選擇時,頭部檢視會向下滑動,而當開始取消選擇圖片,且選擇圖片為空時,頭部檢視向上滑動,這一部分操作,我使用了UIView動畫來製作,方法內容如下:
1 |
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0); |
引數說明:
duration:持續時間 delay:延時時間 dampingRatio:spring係數,從0-1,數值越小,效果越強velocity:初始速度 options:動畫選項 animations:動畫block completion:動畫結束後的回撥
在animations block中設定了collectionView的originUp,sizeHeight和contentOffset,作為最終狀態,而上面那個方法則會自動完成中間狀態的計算和設定。
CollectionViewCell
在cell裡面,同樣使用了約束和UIView動畫,在動畫中使用了View的transform屬性,點選時設定cell的transform為CGAffineTransformMakeScale(0.9, 0.9),即縮小狀態,取消點選時設定cell的transform為CGAffineTransformMakeScale(1, 1),即正常大小,形成一種強烈的選中效果。
AlbumTableView
這裡自定義了一個下拉選單,整個下拉選單的製作並沒有困難的地方,同樣使用了UIView動畫來進行frame上的位移設定。使用了requestImageForAsset:的方法進行縮圖請求。
第三部分 ImageBrowser(圖片瀏覽)
動畫顯示
這個瀏覽器,我寫了這樣的一個present的方法。我們從這個方法入手。
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Present the browser. * * @param fromView the fromeView. * @param toContainer the view which will contain the browser. * @param animated animated bool. * @param completion completion block. */ - (void)presentFromImageView:(UIView *)fromView toContainer:(UIView *)toContainer animated:(BOOL)animated completion:(void (^)(void))completion; |
引數設定:
fromView:通過該View的絕對位置,我們取到初始變化的Bounds toContainer:瀏覽器所add上去的檢視 animated:動畫選擇 completion完成回撥
下面的兩個動畫方法,分別用來做瀏覽器的顯示和隱藏動效,結合fromView的bounds值,可以做到檢視從所點選處的View變化而來的效果。點選檢視更多程式碼。
顯示動畫:
1 2 3 |
- (void)showWithAnimated:(BOOL)animated cell:(CBImageScrollViewCell *)cell completion:(void (^)(void))completion; |
隱藏動畫:
1 2 |
- (void)dismissAnimated:(BOOL)animated completion:(void (^)(void))completion; |
第一層ScrollView
這個瀏覽器使用了兩層的ScrollView,第一層是用來盛放第二層的ScrollView,起到了滑動選擇不同圖片的效果,第一層的ScrollView的contentSize通過所傳入的圖片陣列動態改變。
第二層ScrollView
第二層的ScrollView起到了作為類似cell的作用,我們簡單的通過官方的一個方法:
1 |
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView; |
傳入一個ImageView即可自動完成二指縮放的效果,非常方便。但我們還是需要自己處理單擊,雙擊,長按和雙擊事件,下面我們來看這一部分內容。
單擊,雙擊,長按和雙擊手勢的處理
我們先依次新增手勢,記得要新增[singleTap requireGestureRecognizerToFail:doubleTap];保證雙擊手勢和單擊手勢之間的手勢衝突得到解決。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; doubleTap.delegate = self; doubleTap.numberOfTapsRequired = 2; [doubleTap requireGestureRecognizerToFail:doubleTap]; [self addGestureRecognizer:doubleTap]; UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismiss:)]; singleTap.delegate = self; [singleTap requireGestureRecognizerToFail:doubleTap]; [self addGestureRecognizer:singleTap]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]; longPress.delegate = self; [self addGestureRecognizer:longPress]; UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)]; [self addGestureRecognizer:panGesture]; |
單擊手勢我們直接使用block來執行dismiss方法即可,這裡不表。雙擊手勢呢,我們用下面的方法來進行縮放,主要是計算rect和scale。
1 2 3 4 5 6 7 8 9 |
CGPoint touchPoint = [sender locationInView:imageCell.imageView]; CGFloat newZoomScale = imageCell.maximumZoomScale; CGFloat xsize = imageCell.sizeWidth / newZoomScale; CGFloat ysize = imageCell.sizeHeight / newZoomScale; [imageCell zoomToRect:CGRectMake(touchPoint.x - xsize / 2, touchPoint.y - ysize / 2, xsize, ysize) animated:YES]; |
長按手勢我是直接呼叫系統的popoverPresentationController:方法,可以實現分享和儲存等功能。
1 2 3 4 5 6 7 |
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[imageCell.imageView.image] applicationActivities:nil]; if ([activityViewController respondsToSelector:@selector(popoverPresentationController)]) { activityViewController.popoverPresentationController.sourceView = self; } [self.viewController presentViewController:activityViewController animated:YES completion:nil]; |
Pan手勢相對複雜,利用velocityInView:來取得滑動速度,利用locationInView:來取得滑動的初始位置和最終位置。主要的處理邏輯是進行初始位置和最終位置進行對比,如果數值較大,就執行動畫並進行dismiss,而如果數值較小就取消滑動,恢復初始位置。而一旦初始速度很大的時候,就直接執行動畫並dismiss,判斷使用者的行為,並作出操作。
這一部分內容較多,請直接點選檢視。
1 |
- (void)panGesture:(UIPanGestureRecognizer *)sender; |
第四部分 總結
這個庫的製作過程基本如上所述,但仍有很多細節難以細講,請諒解,請有意向的朋友下載demo檢視,如有疑問或者BUG,歡迎留言或者提出issue,謝謝。