本开源库基于iOS8中PhotoKit框架制作,所以暂不支持iOS8以下版本,请谅解。另,在iOS10中,使用photoKit框架的应用可能会出现crash,但这个问题相信很快会被官方修复,请无需担心。
效果图
实际截图
以下是原Idea作者的Dribble:Photo Picker Interaction 地址和本人的GitHub:CBImagePicker 地址,希望大家点击支持,再次感谢。
下面我们来仔细的分析,做这个库的完整过程。
第一部分 Category
<span id="jump_1">整个库的制作用到了大量的坐标计算,所以我们很有必要写一个category来简化这一部分操作,我针对UIView的<u>UIView+CBAddition</u>类库和针对UIImage的<u>UIImage+CBAddition</u>都是为了简化这一部分内容而做的工作。</span>
// Getter
- (CGFloat)originLeft {
return self.frame.origin.x;
}
// 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之间的位置和尺寸的关系,使用方法如下:
+(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来申请所有的相册列表,代码如下:
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来请求图片信息,方法如下:
- (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
[_imageCollectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionReusableView"];
设置dataSource
- (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动画来制作,方法内容如下:
+ (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的方法。我们从这个方法入手。
/**
* 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变化而来的效果。点击查看更多代码。
显示动画:
- (void)showWithAnimated:(BOOL)animated
cell:(CBImageScrollViewCell *)cell
completion:(void (^)(void))completion;
隐藏动画:
- (void)dismissAnimated:(BOOL)animated
completion:(void (^)(void))completion;
第一层ScrollView
这个浏览器使用了两层的ScrollView,第一层是用来盛放第二层的ScrollView,起到了滑动选择不同图片的效果,第一层的ScrollView的contentSize通过所传入的图片数组动态改变。
第二层ScrollView
第二层的ScrollView起到了作为类似cell的作用,我们简单的通过官方的一个方法:
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
传入一个ImageView即可自动完成二指缩放的效果,非常方便。但我们还是需要自己处理单击,双击,长按和双击事件,下面我们来看这一部分内容。
单击,双击,长按和双击手势的处理
我们先依次添加手势,记得要添加[singleTap requireGestureRecognizerToFail:doubleTap];保证双击手势和单击手势之间的手势冲突得到解决。
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。
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:方法,可以实现分享和保存等功能。
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,判断用户的行为,并作出操作。
这一部分内容较多,请直接点击查看。
- (void)panGesture:(UIPanGestureRecognizer *)sender;
第四部分 总结
这个库的制作过程基本如上所述,但仍有很多细节难以细讲,请谅解,请有意向的朋友下载demo查看,如有疑问或者BUG,欢迎留言或者提出issue,谢谢。