一.框架介绍
由于项目需求,然后也没有找到完全合适的第三方,所以自己写了一个图片浏览器,后来公司几个项目都用到了这个图片浏览器,为了适应各种产品需求,较之前添加了些功能,主要支持:
1.支持模仿微博的直接动画放大功能;
2.支持模仿微信的当下载中的时候,居中显示,下载完成放大,如果已经下载,直接动画放大。
3.支持本地图片和网络图片的混搭,比如说聊天页面有自己发送的本地图片和别人发过来的网络图片,这个框架会自动判断。
4.支持图片复用,一次只加载三张图片,优化内存
5.支持长图、动态图、下载进度显示
二.效果图
当时项目需求如图所示,由于大部分第三方回去的时候会存在抖动问题,所以当时自己简单写了个浏览器,后来其他项目也用到了,所以做了扩展和优化,进行了图片复用,一次只加载三张图片,优化了内存,同时支持长图和动态图,以及进度下载进度显示。
1.UIScrollView 效果图:
2.UICollectionView 模仿微博模式 效果图:
3.UICollectionView 模仿微信模式 效果图:
二.实现方法
(1).创建FJPhotosView实例
FJPhotosView *photosView = [[FJPhotosView alloc] init];
// self.imageArray: 大图url数组
// selectedIndex: 当前选中图片索引
// photoViewShowType: 显示模式
[photosView setParam:self.bigImageArray selectedIndex:indexPath.row photoViewShowType:self.switchShowBtn.selected];
// 设置代理
photosView.delegate = self;
// 展示图片浏览器
[photosView show];
其中:
// 显示 模式
typedef NS_ENUM(NSInteger, PhotoViewShowType){
// 模仿微博显示
PhotoViewShowTypeOfWeiBo = 0,
// 模仿微信显示
PhotoViewShowTypeOfWeiXin = 1,
};
如果
(2).实现代理方法 -- FJPhotosViewDelegate
// 返回临时占位图片(即原来的小图)
- (UIImageView *)photoBrowser:(FJPhotosView *)browser placeholderImageForIndex:(NSInteger)index {
//获取占位小图代码;
}
// 返回临时占位图片位置
-(CGRect)photoBrowser:(FJPhotosView *)browser targetRectForIndex:(NSInteger)index {
//获取占位图片位置代码;
}
三.代码解析
1.架构解析
A.FJPhotosView:
a.FJPhotosView图片浏览器入口,加载在[UIApplication sharedApplication].keyWindow上,这样可以有效的避免消失时的抖动情况。
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor blackColor];
self.frame = [UIApplication sharedApplication].keyWindow.bounds;
[[UIApplication sharedApplication].keyWindow addSubview:self];
}
return self;
}
b.FJPhotosView拥有一个最外围UIScrollView负责容纳显示视图和一个UILabel负责显示当前序号,然后根据传入的参数进行初始化
-
主要参数:
// titleLabel :展示当前页数和总页数 @property (nonatomic, strong) UILabel *titleLabel; // UIScrollView: 加载3张展示的视图 @property (nonatomic, strong) UIScrollView *scrollView; // photoView array (FJPhotoScrollView 数组) @property (nonatomic, strong) NSMutableArray *photoViewArray; // imageurl array (imageUrl 数组) @property (nonatomic, strong) NSMutableArray *photoImageUrlArray;
-
主要函数:
//初始化PhontoView(只加载三张展示图) - (void)setupPhotoViews { if (self.photoImageUrlArray.count > 0) { for (NSInteger i = 0; i < 3; i++) { FJPhotoScrollView *photoView = [[FJPhotoScrollView alloc] initWithFrame:CGRectMake(self.scrollView.width * i, 0, self.scrollView.width, self.scrollView.height)]; photoView.delegate = self; photoView.parentPhotosView = self; [self.scrollView addSubview:photoView]; [self.photoViewArray addObject:photoView]; } [self setupAvailalbePhotoView]; } }
根据点击的selectIndex加载前后的两张视图
// 初始化 可视 photoView - (void)setupAvailalbePhotoView { if (_selectedIndex >= self.photoImageUrlArray.count) { _selectedIndex = 0; } //点击第一张 if (_selectedIndex == 0) { for (NSInteger tmpIndex = 0; tmpIndex < 3; tmpIndex++) { if (tmpIndex >= self.photoImageUrlArray.count) { break; } if (tmpIndex == 0) { _transferPhotoView = [self.photoViewArray objectAtIndex:tmpIndex]; } [self configContentWithPage:tmpIndex photoView:[self.photoViewArray objectAtIndex:tmpIndex]]; } } else if (_selectedIndex == (self.photoImageUrlArray.count - 1)) {//点击最后一张 NSInteger tempIndex = 0; for (NSInteger index = (self.photoImageUrlArray.count - 1);index >= (self.photoImageUrlArray.count - 3); index--) { if (index < 0) { continue; } if (index == _selectedIndex) { _transferPhotoView = [self.photoViewArray objectAtIndex:tempIndex]; } [self configContentWithPage:index photoView:[self.photoViewArray objectAtIndex:tempIndex++]]; } } //点击中间图片 else { if (self.photoViewArray.count > 2) { _transferPhotoView = [self.photoViewArray objectAtIndex:1]; //先加载中间 [self configContentWithPage:_selectedIndex photoView:[self.photoViewArray objectAtIndex:1]]; [self configContentWithPage:_selectedIndex - 1 photoView:[self.photoViewArray objectAtIndex:0]]; [self configContentWithPage:_selectedIndex + 1 photoView:[self.photoViewArray objectAtIndex:2]]; } } if (self.photoImageUrlArray.count > 1) { self.titleLabel.text = [NSString stringWithFormat:@"%@/%ld",@(_selectedIndex+1), (long)self.photoImageUrlArray.count]; } self.scrollView.contentSize = CGSizeMake(self.photoImageUrlArray.count * FW(self.scrollView), self.scrollView.bounds.size.height); [self.scrollView scrollRectToVisible:CGRectMake(_selectedIndex * FW(self.scrollView), 0, FW(self.scrollView), FH(self.scrollView)) animated:NO]; }
将相应的参数传给FJPhotoScrollView,让FJPhotoScrollView进行图片的显示
// 配置 相关 图片 - (void)configContentWithPage:(NSInteger)page photoView:(FJPhotoScrollView*)photoView { id variable = [self.photoImageUrlArray objectAtIndex:page]; [photoView setParamWithVariable:variable currentIndex:page photoViewShowType:_photoViewShowType]; photoView.frame = CGRectMake(page * FW(_scrollView), FY(photoView), FW(photoView), FH(photoView)); photoView.backgroundColor = [UIColor blackColor]; }
通过 UIScrollViewDelegate 进行图片的缩放
// 图片 放大 缩小 - (void)scrollViewDidZoom:(FJPhotoScrollView *)photoView { if (photoView == self.scrollView){ return; } CGSize boundsSize = self.scrollView.bounds.size; CGRect contentsFrame = photoView.myImageView.frame; if (contentsFrame.size.width < boundsSize.width) { contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f; } else { contentsFrame.origin.x = 0.0f; } if (contentsFrame.size.height < boundsSize.height) { contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f; } else { contentsFrame.origin.y = 0.0f; } photoView.myImageView.frame = contentsFrame; } 图片滚动加载前后图片 // 图片 滚动 - (void)scrollViewDidScroll:(UIScrollView *)aScrollView { static NSInteger temp_lastPage = -1; CGFloat pageWidth = self.scrollView.frame.size.width; NSInteger page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1; NSInteger photoCount = [self.photoImageUrlArray count]; if (page >= photoCount) { return; } _currentSelectIndex = page; _titleLabel.text = [NSString stringWithFormat:@"%ld/%ld",(long)page+1, (long)photoCount]; if (aScrollView == self.scrollView && temp_lastPage!=page) { if (_currShowImageIndex > -1 && _currShowImageIndex < 3) { FJPhotoScrollView *subPhotoScrollView = [self.photoViewArray objectAtIndex:_currShowImageIndex]; subPhotoScrollView.zoomScale = 1; temp_lastPage = page; } } if (aScrollView != self.scrollView) { return; } if (_lastPage != page) { for (NSInteger i = 0; i<[self.photoViewArray count]; i++) { FJPhotoScrollView *tmpPhotoScrollView = [self.photoViewArray objectAtIndex:i]; if (FX(tmpPhotoScrollView) == page * FW(aScrollView)) { _currShowImageIndex = i; break; } } if (page > 0 && page < self.photoImageUrlArray.count - 1) { for (NSInteger i = 0; i < 3; i++) { UIImageView *tmpImageView = [self.photoViewArray objectAtIndex:_currShowImageIndex]; FJPhotoScrollView *tmpPhotoScrollView = [self.photoViewArray objectAtIndex:i]; //加载下一页 if ((FX(tmpImageView) - FX(tmpPhotoScrollView)) > FW(self.scrollView)) { [self configContentWithPage:page + 1 photoView:tmpPhotoScrollView]; } //加载上一页 else if ((FX(tmpPhotoScrollView) - FX(tmpImageView)) > FW(self.scrollView)) { [self configContentWithPage:page - 1 photoView:tmpPhotoScrollView]; } } } _lastPage = page; } aScrollView.userInteractionEnabled=YES; }
B.FJPhotoScrollView:
a.FJPhotoScrollView继承自FJBaseScrollView,本身是一个UIScrollView,拥有myImageView(主展示图)、originalImageView(占位小图)和progressLayer(进度条):
// 展示图
@property(nonatomic, strong) UIImageView *myImageView;
// 进度条
@property (nonatomic, strong) CAShapeLayer *progressLayer;
// 占位图
@property (nonatomic, strong) UIImageView *originalImageView;
b.FJPhotoScrollView通过设置参数函数进行图片类型判断、动画效果选择、下载进度显示。
核心函数:
// 设置相关参数
- (void)setParamWithVariable:(id)variable currentIndex:(NSInteger)currentIndex photoViewShowType:(PhotoViewShowType)photoViewShowType {
//初始位置
_variable = variable;
self.currentIndex = currentIndex;
// 获取原图位置
CGRect originalRect = [self targetRectForIndex:currentIndex];
self.myImageView.frame = originalRect;
// 获取 临时占位图
self.originalImageView = [self placeholderImageForIndex:currentIndex];
// 微博 显示 方式 (直接放大)
if (photoViewShowType == PhotoViewShowTypeOfWeiBo) {
[self showDirectlyAmplifyPhotoViewAnimation:_variable];
}
// 微信 显示 方式 (加载完成后 放大)
else if(photoViewShowType == PhotoViewShowTypeOfWeiXin && [self isImageUrl:variable]) {
// 图片未下载 先显示在 中部
NSString *tmpImageUrl = (NSString *)variable;
if ([self.myImageView exitCurrentImage:tmpImageUrl] == NO) {
[UIView animateWithDuration:FJDefaultAnimationTime animations:^{
[self setMyimageViewInTheMiddle:self.originalImageView];
} completion:^(BOOL finished) {
[self showImageViewDowningProgerss:tmpImageUrl isAnimation:YES];
}];
}
// 图片已下载 直接放大
else {
[self showDirectlyAmplifyPhotoViewAnimation:_variable];
}
}
}
variable 之所以是id类型,主要是为了做本地图片和网络图片的兼容,通过函数isImageUrl判断是否为网络图片:
// 判断 是否 为 网络 图片
- (BOOL)isImageUrl:(id)variable {
BOOL isImageUrl = NO;
// NSString 类型
if ([variable isKindOfClass:[NSString class]]) {
NSString *tmpStr = (NSString *)variable;
isImageUrl = [tmpStr isHttpUrl];
}
return isImageUrl;
}
如果非网络图片就是本地图片,本地图片就直接动画放大显示,如果是网络图片,判断是否下载,如果未下载,判断如果是微信模式,就先居中显示,然后去下载,如果是微博模式,就直接放大下载。
// 显示 直接 放大 图片 动画
- (void)showDirectlyAmplifyPhotoViewAnimation:(id)variable {
[UIView animateWithDuration:FJDefaultAnimationTime animations:^{
[self setFrameAndZoom:self.originalImageView];
} completion:^(BOOL finished) {
self.userInteractionEnabled = YES ;
// 网络 图片
if ([self isImageUrl:variable]) {
[self showImageViewDowningProgerss:(NSString *)_variable isAnimation:NO];
}
// 本地 图片
else {
//变换完动画 从网络开始加载图
self.myImageView.image = [self getImage:variable];
[self setFrameAndZoom:self.myImageView];//设置最新的网络下载后的图的frame大小
}
}];
}
网络图片显示下载进度,下载完成后动画展现
// 显示 下载进入和 下载完成 展现 动画
- (void)showImageViewDowningProgerss:(NSString *)imageUrl isAnimation:(BOOL)isAnimation{
//变换完动画 从网络开始加载图
NSString *imageUrlStr = [[imageUrl stringByReplacingOccurrencesOfString:@"\\" withString:@""] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[self.myImageView sd_setImageWithURL:[NSURL URLWithString:imageUrlStr] placeholderImage:self.myImageView.image options:SDWebImageRetryFailed|SDWebImageLowPriority progress:^(NSInteger receivedSize, NSInteger expectedSize) {
if (receivedSize > 0 && expectedSize > 0) {
CGFloat progress = receivedSize / (float)expectedSize;
progress = progress < 0.01 ? 0.01 : progress > 1 ? 1 : progress;
if (isnan(progress)) progress = 0;
self.progressLayer.hidden = NO;
self.progressLayer.strokeEnd = progress;
}
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
self.progressLayer.hidden = YES;
if (isAnimation) {
[UIView animateWithDuration:FJDefaultAnimationTime animations:^{
[self setFrameAndZoom:self.myImageView];
}];
}
else {
[self setFrameAndZoom:self.myImageView];
}
}];
}
微信模式未下载情况先显示在中间函数:
// 微信 模式 还没下载完成 显示在中间
- (void)setMyimageViewInTheMiddle:(UIImageView *)imageView {
//设置空image时的情况
//ImageView.image的大小
CGFloat imageH;
CGFloat imageW;
if(imageView == nil) {
imageH = self.myImageView.height;
imageW = self.myImageView.width;
self.myImageView.image = IMG(@"default_avatar_geren_134.png");
}
else {
imageW = imageView.width;
imageH = imageView.height;
if (imageW < 0.5 || imageH < 0.5) {
imageH = self.myImageView.height;
imageW = self.myImageView.width;
}
self.myImageView.image = imageView.image;
}
if (imageW < 0.5 || imageH < 0.5) {
imageH = SCREEN_WIDTH / 2.5;
imageW = SCREEN_WIDTH / 2.5;
}
CGFloat imageX = (SCREEN_WIDTH/2.0) - (imageW/2.0);
CGFloat imageY = (SCREEN_HEIGHT/2.0) - (imageH/2.0);
self.myImageView.frame = CGRectMake(imageX, imageY, imageW, imageH);
}
下载完成后或是本地图片直接放大以及图片缩放被说计算函数:
// 微博 模式 直接 放大
-(void)setFrameAndZoom:(UIImageView *)imageView {
//ImageView.image的大小
CGFloat imageH;
CGFloat imageW;
//设置空image时的情况
if(imageView.image == nil || imageView.image.size.width == 0 || imageView.image.size.height == 0) {
//设置主图片
imageH = SCREEN_HEIGHT;
imageW = SCREEN_WIDTH;
self.myImageView.image = IMG(@"default_avatar_geren_134.png");
}
//不空
else{
//设置主图片
imageW = imageView.image.size.width;
imageH = imageView.image.size.height;
self.myImageView.image = imageView.image;
}
//设置主图片Frame 与缩小比例
//横着
if(imageW >= (imageH * (SCREEN_WIDTH/SCREEN_HEIGHT))){
//设置居中frame
CGFloat myX_ = 0;
CGFloat myW_ = SCREEN_WIDTH;
CGFloat myH_ = myW_ *(imageH/imageW);;
CGFloat myY_ = SCREEN_HEIGHT - myH_ - ((SCREEN_HEIGHT - myH_)/2);
self.myImageView.frame = CGRectMake(myX_, myY_, myW_, myH_);
if (myH_ > SCREEN_HEIGHT) {
self.contentSize = CGSizeMake(SCREEN_WIDTH, myH_);
}
//判断原图是小图还是大图来判断,是可以缩放,还是可以放大
if (imageW > myW_) {
self.maximumZoomScale = 3*(imageW/myW_ ) ;//放大比例
}
else{
self.minimumZoomScale = (imageW/myW_);//缩小比例
}
}
//竖着
else {
CGFloat myX_ = 0;
CGFloat myY_ = 0;
CGFloat myW_ = SCREEN_WIDTH;
CGFloat myH_ = floor(imageH / (imageW / self.width));
if (myH_ > SCREEN_HEIGHT) {
self.contentSize = CGSizeMake(SCREEN_WIDTH, myH_);
}
//变换设置frame
self.myImageView.frame = CGRectMake(myX_, myY_, myW_, myH_);
//判断原图是小图还是大图来判断,是可以缩放,还是可以放大
if (imageH > myH_) {
self.maximumZoomScale = 3*(imageH/myH_ ) ;//放大比例
}
else {
self.minimumZoomScale = (imageH/myH_);//缩小比例
}
}
}
实现单击,如果当前滚动到的图片在原主界面可视范围内,就动态返回原主界面当前滚动到的图片位置,如果不在当前可视范围内,就动画消失函数:
// tap事件
- (void)singleTap {
CGRect originalRect = [self targetRectForIndex:_currentIndex];
self.userInteractionEnabled = NO;
self.zoomScale = 1;
[UIView animateWithDuration:0.5 animations:^{
if (CGRectEqualToRect(originalRect, CGRectZero)) {
self.alpha = 0;
}else{
self.myImageView.frame = originalRect;
}
self.superview.superview.backgroundColor = [UIColor clearColor];
self.superview.backgroundColor = [UIColor clearColor];
self.backgroundColor = [UIColor clearColor];
} completion:^(BOOL finished) {
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewDidClick)]) {
[self.delegate performSelector:@selector(scrollViewDidClick)];
}
}];
}
C.FJBaseScrollView
a.FJBaseScrollView是FJPhotoScrollView的基类,主要实现单击返回原主界面,双击将图片放大的效果,
通过touchesBegan函数来识别单击和双击:
// touch begin 标识双击和单击
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
NSTimeInterval delaytime = 0.3;
_point = [[touches anyObject] locationInView:self];
switch (touch.tapCount) {
case 1:
break;
case 2: {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTap) object:nil];
[self performSelector:@selector(doubleTap) withObject:nil afterDelay:delaytime];
break;
default:
break;
}
}
}
// touch end
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
switch (touch.tapCount) {
case 1:
[self performSelector:@selector(singleTap) withObject:nil afterDelay:.2];
break;
default:
break;
}
}
双击调用缩放方法:
// 双击
-(void)doubleTap {
if(_isScaled == YES){
[self zoomToPointInRootView:_point atScale:1];
_isScaled = NO;
}else{
[self zoomToPointInRootView:_point atScale:2];
_isScaled = YES;
}
}
设置UIScrollView缩放效果:
// 放大 或 缩小
- (void)zoomToPointInRootView:(CGPoint)center atScale:(float)scale {
CGRect zoomRect;
zoomRect.size.height = self.frame.size.height / scale;
zoomRect.size.width = self.frame.size.width / scale;
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
[self zoomToRect:zoomRect animated:YES];
}
单击调用代理动态返回原来界面:
// 单击
-(void)singleTap {
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewDidClick)]) {
[self.delegate performSelector:@selector(scrollViewDidClick)];
}
}
四.最后
送给大家一张很喜欢的图:
这是gitHub链接地址,大家有兴趣可以看一下,如果觉得不错,麻烦给个喜欢或star,如果有问题请及时反馈,谢谢!