首次提交

This commit is contained in:
启星
2025-09-22 18:48:29 +08:00
parent 28ae935e93
commit ae9be0b58e
8941 changed files with 999209 additions and 2 deletions

21
Pods/YBImageBrowser/LICENSE generated Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 杨波
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

229
Pods/YBImageBrowser/README.md generated Normal file
View File

@@ -0,0 +1,229 @@
![](https://github.com/indulgeIn/YBImageBrowser/blob/master/Images/banner.png)
[![CocoaPods](https://img.shields.io/cocoapods/v/YBImageBrowser.svg)](https://cocoapods.org/pods/YBImageBrowser) 
[![CocoaPods](https://img.shields.io/cocoapods/p/YBImageBrowser.svg)](https://github.com/indulgeIn/YBImageBrowser) 
[![License](https://img.shields.io/github/license/indulgeIn/YBImageBrowser.svg)](https://github.com/indulgeIn/YBImageBrowser) 
**iOS 图片浏览器,功能强大,易于拓展,性能优化和内存控制让其运行更加的流畅和稳健。**
##### 相关文章:
##### [YBImageBrowser 重构心得:如何优化架构、性能、内存?](https://www.jianshu.com/p/ef53d0094437)
##### [避免 iOS 组件依赖冲突的小技巧](https://www.jianshu.com/p/0e3283275300)
## 注意事项
#### 关于 3.x 版本 (使用 2.x 版本请切换到 store_2.x 分支)
为了彻底解决 2.x 版本的设计缺陷和代码漏洞,特花费大量业余时间进行了 3.x 深度重构,所以没办法做到向下兼容,希望社区朋友们能体谅,根据情况进行版本迁移。
3.x 版本有着更科学的架构,更极致的性能提升,更严格的内存控制,使用起来会更得心应手,也便于将来的迭代优化。
#### 提问须知
考虑到笔者的精力问题,遇到问题请先查看 API、效仿 Demo、阅读 README、搜索 Issues。请不要提出与组件无关的问题比如 CocoaPods 的错误,如果是 BUG 或 Feature 最好是提 Issue。
# 目录
* [预览](#预览)
* [特性](#特性)
* [安装](#安装)
* [用法](#用法)
* [常见问题](#常见问题)
# 预览
![](https://github.com/indulgeIn/YBImageBrowser/blob/master/Images/preview.gif)
# 特性
- 支持 GIFAPNGWebP 等本地和网络图片类型(由 YYImage、SDWebImage 提供支持)。
- 支持系统相册图片和视频。
- 支持简单的视频播放。
- 支持高清图浏览。
- 支持图片预处理(比如添加水印)。
- 支持根据图片的大小判断是否需要预先解码(精确控制内存)。
- 支持图片压缩、裁剪的界限设定。
- 支持修改下载图片的 NSURLRequest。
- 支持主动旋转或跟随控制器旋转。
- 支持自定义图标。
- 支持自定义 Toast/Loading。
- 支持自定义文案(默认提供中文和英文)。
- 支持自定义工具视图(比如查看原图功能)。
- 支持自定义 Cell比如添加一个广告模块
- 支持添加到其它父视图上使用(比如加到控制器上)。
- 支持转场动效、图片布局等深度定制。
- 支持数据重载、局部更新。
- 支持低粒度的内存控制和性能调优。
- 极致的性能优化和严格的内存控制让其运行更加的流畅和稳健。
# 安装
## CocoaPods
支持分库导入,核心部分就是图片浏览功能,视频播放作为拓展功能按需导入。
1. 在 Podfile 中添加:
```
pod 'YBImageBrowser'
pod 'YBImageBrowser/Video' //视频功能需添加
```
2. 执行 `pod install``pod update`
3. 导入 `<YBImageBrowser/YBImageBrowser.h>`,视频功能需导入`<YBImageBrowser/YBIBVideoData.h>`
4. 注意:如果你需要支持 WebP可以在 Podfile 中添加 `pod 'YYImage/WebP'`
若搜索不到库,可执行`pod repo update`,或使用 `rm ~/Library/Caches/CocoaPods/search_index.json` 移除本地索引然后再执行安装,或更新一下 CocoaPods 版本。
#### 去除 SDWebImage 的依赖(版本需 >= 3.0.4
Podfile 相应的配置变为:
```
pod 'YBImageBrowser/NOSD'
pod 'YBImageBrowser/VideoNOSD' //视频功能需添加
```
这时你必须定义一个类实现`YBIBWebImageMediator`协议,并赋值给`YBImageBrowser`类的`webImageMediator`属性(可以参考 `YBIBDefaultWebImageMediator`的实现)。
## 手动导入
1. 下载 YBImageBrowser 文件夹所有内容并且拖入你的工程中,视频功能还需下载 Video 文件夹所有内容。
2. 链接以下 frameworks
* SDWebImage
* YYImage
3. 导入 `YBImageBrowser.h`,视频功能需导入`YBIBVideoData.h`
4. 注意:如果你需要支持 WebP可以在 Podfile 中添加 `pod 'YYImage/WebP'`,或者到手动下载 [YYImage 仓库](https://github.com/ibireme/YYImage) 的 webP 支持文件。
# 用法
初始化`YBImageBrowser`并且赋值数据源`id<YBIBDataProtocol>`,默认提供`YBIBImageData` (图片) 和`YBIBVideoData` (视频) 两种数据源。
图片处理是组件的核心,笔者精力有限,视频播放做得很轻量,若有更高的要求最好是自定义 Cell望体谅。
Demo 中提供了很多示例代码,演示较复杂的拓展方式,所以若需要深度定制最好是下载 Demo 查看。
建议不对`YBImageBrowser`进行复用,目前还存在一些逻辑漏洞。
## 基本使用
```
// 本地图片
YBIBImageData *data0 = [YBIBImageData new];
data0.imageName = ...;
data0.projectiveView = ...;
// 网络图片
YBIBImageData *data1 = [YBIBImageData new];
data1.imageURL = ...;
data1.projectiveView = ...;
// 视频
YBIBVideoData *data2 = [YBIBVideoData new];
data2.videoURL = ...;
data2.projectiveView = ...;
YBImageBrowser *browser = [YBImageBrowser new];
browser.dataSourceArray = @[data0, data1, data2];
browser.currentPage = ...;
[browser show];
```
## 设置支持的旋转方向
当图片浏览器依托的 UIViewController 仅支持一个方向:
这种情况通过`YBImageBrowser.new.supportedOrientations`设置图片浏览器支持的旋转方向。
否则:
上面的属性将失效,图片浏览器会跟随控制器的旋转而旋转,由于各种原因这种情况的旋转过渡有瑕疵,建议不使用这种方式。
## 自定义图标
修改`YBIBIconManager.sharedManager`实例的属性。
## 自定义文案
修改`YBIBCopywriter.sharedCopywriter`实例的属性。
## 自定义 Toast / Loading
实现`YBIBAuxiliaryViewHandler`协议,并且赋值给`YBImageBrowser.new.auxiliaryViewHandler`属性,可参考和协议同名的默认实现类。
## 自定义工具视图ToolView
默认实现的`YBImageBrowser.new.defaultToolViewHandler`处理器可以做一些属性配置,当满足不了业务需求时,最好是进行自定义,参考默认实现或 Demo 中“查看原图”功能实现。
定义一个或多个类实现`YBIBToolViewHandler`协议,并且装入`YBImageBrowser.new.toolViewHandlers`数组属性。建议使用一个中介者来实现这个协议,然后所有的工具视图都由这个中介者来管理,当然也可以让每一个自定义的工具 UIView 都实现`YBIBToolViewHandler`协议,请根据具体需求取舍。
## 自定义 Cell
当默认提供的`YBIBImageData` (图片) 和`YBIBVideoData` (视频) 满足不了需求时,可自定义拓展 Cell参考默认实现或 Demo 中的示例代码。
定义一个实现`YBIBCellProtocol`协议的`UICollectionViewCell`类和一个实现`YBIBDataProtocol`协议的数据类,当要求不高时实现必选协议方法就能跑起来了,若对交互有要求就相对比较复杂,最好是参考默认的交互动效实现。
在某些场景下,甚至可以直接继承项目中的 Cell 来做自定义。
# 常见问题
## SDWebImage Pods 版本兼容问题
SDWebImage 有两种情况会出现兼容问题:该库对 SDWebImage 采用模糊向上依赖,但将来 SDWebImage 可能没做好向下兼容;当其它库依赖 SDWebImage 更低或更高 API 不兼容版本。对于这种情况,可以尝试以下方式解决:
- Podfile 中采用去除 SDWebImage 依赖的方式导入,只需要实现一个中介者(见[安装](#安装)部分)。
- 更改其它库对 SDWebImage 的依赖版本。
- 手动导入 YBImageBrowser然后修改`YBIBDefaultWebImageMediator`文件。
为什么不去除依赖 SDWebImage 自己实现?时间成本太高。
为什么不拖入 SDWebImage 修改类名?会扩大组件的体积,若外部有 SDWebImage 就存在一份多余代码。
## 依赖的 YYImage 与项目依赖的 YYKit 冲突
实际上 YYKit 有把各个组件拆分出来,建议项目中分开导入:
```
pod 'YYModel'
pod 'YYCache'
pod 'YYImage'
pod 'YYWebImage'
pod 'YYText'
...
```
而且这样更灵活便于取舍。
## 低内存设备 OOM 问题
组件内部会降低在低内存设备上的性能,减小内存占用,但若高清图过多,可能需要手动去控制(以下是硬件消耗很低的状态):
```
YBIBImageData *data = YBIBImageData.new;
// 取消预解码
data.shouldPreDecodeAsync = NO;
// 直接设大触发裁剪比例,绘制更小的裁剪区域压力更小,不过可能会让用户感觉奇怪,放很大才开始裁剪显示高清局部(这个属性很多时候不需要显式设置,内部会动态计算)
data.cuttingZoomScale = 10;
YBImageBrowser *browser = YBImageBrowser.new;
// 调低图片的缓存数量
browser.ybib_imageCache.imageCacheCountLimit = 1;
// 预加载数量设为 0
browser.preloadCount = 0;
```
## 视频播放功能简陋
关于大家提的关于视频的需求,有些成本过高,笔者精力有限望体谅。若组件默认的视频播放器满足不了需求,就自定义一个 Cell 吧,把成熟的播放器集成到组件中肯定更加的稳定。
## 关于 Swift 版本
考虑时间成本,目前没有写 Swift 版本的计划。

View File

@@ -0,0 +1,44 @@
//
// YBIBVideoActionBar.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/11.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class YBIBVideoActionBar;
@protocol YBIBVideoActionBarDelegate <NSObject>
@required
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar clickPlayButton:(UIButton *)playButton;
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar clickPauseButton:(UIButton *)pauseButton;
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar changeValue:(float)value;
@end
@interface YBIBVideoActionBar : UIView
@property (nonatomic, weak) id<YBIBVideoActionBarDelegate> delegate;
- (void)setMaxValue:(float)value;
- (void)setCurrentValue:(float)value;
- (void)pause;
- (void)play;
+ (CGFloat)defaultHeight;
@property (nonatomic, assign, readonly) BOOL isTouchInside;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,191 @@
//
// YBIBVideoActionBar.m
// YBImageBrowserDemo
//
// Created by on 2019/7/11.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBVideoActionBar.h"
#import "YBIBIconManager.h"
@interface YBVideoBrowseActionSlider : UISlider
@end
@implementation YBVideoBrowseActionSlider
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setThumbImage:YBIBIconManager.sharedManager.videoDragCircleImage() forState:UIControlStateNormal];
self.minimumTrackTintColor = UIColor.whiteColor;
self.maximumTrackTintColor = [UIColor.whiteColor colorWithAlphaComponent:0.5];
self.layer.shadowColor = UIColor.darkGrayColor.CGColor;
self.layer.shadowOffset = CGSizeMake(0, 1);
self.layer.shadowOpacity = 1;
self.layer.shadowRadius = 4;
}
return self;
}
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect frame = [super trackRectForBounds:bounds];
return CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 2);
}
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value {
CGRect frame = [super thumbRectForBounds:bounds trackRect:rect value:value];
return CGRectMake(frame.origin.x - 10, frame.origin.y - 10, frame.size.width + 20, frame.size.height + 20);
}
@end
@interface YBIBVideoActionBar ()
@property (nonatomic, strong) UIButton *playButton;
@property (nonatomic, strong) UILabel *preTimeLabel;
@property (nonatomic, strong) UILabel *sufTimeLabel;
@property (nonatomic, strong) YBVideoBrowseActionSlider *slider;
@end
@implementation YBIBVideoActionBar {
BOOL _dragging;
}
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_dragging = NO;
[self addSubview:self.playButton];
[self addSubview:self.preTimeLabel];
[self addSubview:self.sufTimeLabel];
[self addSubview:self.slider];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.bounds.size.width, height = self.bounds.size.height, labelWidth = 55, buttonWidth = 44, labelOffset = 10;
CGFloat imageWidth = YBIBIconManager.sharedManager.videoPlayImage().size.width;
CGFloat offset = (buttonWidth - imageWidth) * 0.5;
self.playButton.frame = CGRectMake(10, 0, buttonWidth, height);
self.preTimeLabel.frame = CGRectMake(CGRectGetMaxX(self.playButton.frame) + labelOffset - offset, 0, labelWidth, height);
self.sufTimeLabel.frame = CGRectMake(width - labelWidth - labelOffset, 0, labelWidth, height);
self.slider.frame = CGRectMake(CGRectGetMaxX(self.preTimeLabel.frame), 0, CGRectGetMinX(self.sufTimeLabel.frame) - CGRectGetMaxX(self.preTimeLabel.frame), height);
}
#pragma mark - public
+ (CGFloat)defaultHeight {
return 44;
}
- (void)setMaxValue:(float)value {
self.slider.maximumValue = value;
self.sufTimeLabel.attributedText = [self.class timeformatFromSeconds:value];
}
- (void)setCurrentValue:(float)value {
if (!_dragging) {
[self.slider setValue:value animated:YES];
}
self.preTimeLabel.attributedText = [self.class timeformatFromSeconds:value];
}
- (void)pause {
self.playButton.selected = NO;
}
- (void)play {
_dragging = NO;
self.playButton.selected = YES;
self.slider.userInteractionEnabled = YES;
}
#pragma mark - private
+ (NSAttributedString *)timeformatFromSeconds:(NSInteger)seconds {
NSInteger hour = seconds / 3600, min = (seconds % 3600) / 60, sec = seconds % 60;
NSString *text = seconds > 3600 ? [NSString stringWithFormat:@"%02ld:%02ld:%02ld", (long)hour, (long)min, (long)sec] : [NSString stringWithFormat:@"%02ld:%02ld", (long)min, (long)sec];
NSShadow *shadow = [NSShadow new];
shadow.shadowBlurRadius = 4;
shadow.shadowOffset = CGSizeMake(0, 1);
shadow.shadowColor = UIColor.darkGrayColor;
NSAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:text attributes:@{NSShadowAttributeName:shadow, NSFontAttributeName:[UIFont boldSystemFontOfSize:11]}];
return attr;
}
#pragma mark - touch event
- (void)clickPlayButton:(UIButton *)button {
button.userInteractionEnabled = NO;
if (button.selected) {
[self.delegate yb_videoActionBar:self clickPauseButton:button];
} else {
[self.delegate yb_videoActionBar:self clickPlayButton:button];
}
button.userInteractionEnabled = YES;
}
- (void)respondsToSliderTouchFinished:(UISlider *)slider {
[self.delegate yb_videoActionBar:self changeValue:slider.value];
}
- (void)respondsToSliderTouchDown:(UISlider *)slider {
_dragging = YES;
slider.userInteractionEnabled = NO;
}
#pragma mark - getters
- (UIButton *)playButton {
if (!_playButton) {
_playButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_playButton setImage:YBIBIconManager.sharedManager.videoPlayImage() forState:UIControlStateNormal];
[_playButton setImage:YBIBIconManager.sharedManager.videoPauseImage() forState:UIControlStateSelected];
[_playButton addTarget:self action:@selector(clickPlayButton:) forControlEvents:UIControlEventTouchUpInside];
_playButton.layer.shadowColor = UIColor.darkGrayColor.CGColor;
_playButton.layer.shadowOffset = CGSizeMake(0, 1);
_playButton.layer.shadowOpacity = 1;
_playButton.layer.shadowRadius = 4;
}
return _playButton;
}
- (UILabel *)preTimeLabel {
if (!_preTimeLabel) {
_preTimeLabel = [UILabel new];
_preTimeLabel.attributedText = [self.class timeformatFromSeconds:0];
_preTimeLabel.adjustsFontSizeToFitWidth = YES;
_preTimeLabel.textAlignment = NSTextAlignmentCenter;
_preTimeLabel.textColor = [UIColor.whiteColor colorWithAlphaComponent:0.9];
}
return _preTimeLabel;
}
- (UILabel *)sufTimeLabel {
if (!_sufTimeLabel) {
_sufTimeLabel = [UILabel new];
_sufTimeLabel.attributedText = [self.class timeformatFromSeconds:0];
_sufTimeLabel.adjustsFontSizeToFitWidth = YES;
_sufTimeLabel.textAlignment = NSTextAlignmentCenter;
_sufTimeLabel.textColor = [UIColor.whiteColor colorWithAlphaComponent:0.9];
}
return _sufTimeLabel;
}
- (YBVideoBrowseActionSlider *)slider {
if (!_slider) {
_slider = [YBVideoBrowseActionSlider new];
[_slider addTarget:self action:@selector(respondsToSliderTouchFinished:) forControlEvents:UIControlEventTouchUpInside|UIControlEventTouchCancel|UIControlEventTouchUpOutside];
[_slider addTarget:self action:@selector(respondsToSliderTouchDown:) forControlEvents:UIControlEventTouchDown];
}
return _slider;
}
- (BOOL)isTouchInside {
return self.slider.isTouchInside;
}
@end

View File

@@ -0,0 +1,20 @@
//
// YBIBVideoCell+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/12/23.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBVideoCell.h"
#import "YBIBVideoView.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBVideoCell ()
@property (nonatomic, strong) YBIBVideoView *videoView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,17 @@
//
// YBIBVideoCell.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/10.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBCellProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBVideoCell : UICollectionViewCell <YBIBCellProtocol>
@end
NS_ASSUME_NONNULL_END

448
Pods/YBImageBrowser/Video/YBIBVideoCell.m generated Normal file
View File

@@ -0,0 +1,448 @@
//
// YBIBVideoCell.m
// YBImageBrowserDemo
//
// Created by on 2019/7/10.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBVideoCell.h"
#import "YBIBVideoData.h"
#import "YBIBVideoData+Internal.h"
#import "YBIBCopywriter.h"
#import "YBIBIconManager.h"
#import <objc/runtime.h>
#import "YBIBVideoCell+Internal.h"
@interface NSObject (YBIBVideoPlayingRecord)
- (void)ybib_videoPlayingAdd:(NSObject *)obj;
- (void)ybib_videoPlayingRemove:(NSObject *)obj;
- (BOOL)ybib_noVideoPlaying;
@end
@implementation NSObject (YBIBVideoPlayingRecord)
- (NSMutableSet *)ybib_videoPlayingSet {
static void *kRecordKey = &kRecordKey;
NSMutableSet *set = objc_getAssociatedObject(self, kRecordKey);
if (!set) {
set = [NSMutableSet set];
objc_setAssociatedObject(self, kRecordKey, set, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return set;
}
- (void)ybib_videoPlayingAdd:(NSObject *)obj {
[[self ybib_videoPlayingSet] addObject:[NSString stringWithFormat:@"%p", obj]];
}
- (void)ybib_videoPlayingRemove:(NSObject *)obj {
[[self ybib_videoPlayingSet] removeObject:[NSString stringWithFormat:@"%p", obj]];
}
- (BOOL)ybib_noVideoPlaying {
return [self ybib_videoPlayingSet].count == 0;
}
@end
@interface YBIBVideoCell () <YBIBVideoDataDelegate, YBIBVideoViewDelegate, UIGestureRecognizerDelegate>
@end
@implementation YBIBVideoCell {
CGPoint _interactStartPoint;
BOOL _interacting;
}
#pragma mark - life cycle
- (void)dealloc {
[self.yb_backView ybib_videoPlayingRemove:self];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initValue];
[self.contentView addSubview:self.videoView];
[self addGesture];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.videoView.frame = self.bounds;
}
- (void)initValue {
_interactStartPoint = CGPointZero;
_interacting = NO;
}
- (void)prepareForReuse {
((YBIBVideoData *)self.yb_cellData).delegate = nil;
self.videoView.thumbImageView.image = nil;
[self hideAuxiliaryView];
[self.videoView reset];
self.videoView.asset = nil;
[super prepareForReuse];
}
#pragma mark - private
- (void)hideAuxiliaryView {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_hideToastWithContainer:self];
}
- (void)updateImageLayoutWithOrientation:(UIDeviceOrientation)orientation previousImageSize:(CGSize)previousImageSize {
YBIBVideoData *data = self.yb_cellData;
UIImage *image = self.videoView.thumbImageView.image;
CGSize imageSize = image.size;
CGRect imageViewFrame = [data yb_imageViewFrameWithContainerSize:self.yb_containerSize(orientation) imageSize:imageSize orientation:orientation];
CGFloat scale;
if (previousImageSize.width > 0 && previousImageSize.height > 0) {
scale = imageSize.width / imageSize.height - previousImageSize.width / previousImageSize.height;
} else {
scale = 0;
}
// '0.001' is admissible error.
if (ABS(scale) <= 0.001) {
self.videoView.thumbImageView.frame = imageViewFrame;
} else {
[UIView animateWithDuration:0.25 animations:^{
self.videoView.thumbImageView.frame = imageViewFrame;
}];
}
}
- (void)hideBrowser {
((YBIBVideoData *)self.yb_cellData).delegate = nil;
self.videoView.thumbImageView.hidden = NO;
self.videoView.autoPlayCount = 0;
[self.videoView reset];
[self.videoView hideToolBar:YES];
[self.videoView hidePlayButton];
self.yb_hideBrowser();
_interacting = NO;
}
- (void)hideToolViews:(BOOL)hide {
if (hide) {
self.yb_hideToolViews(YES);
} else {
if ([self.yb_backView ybib_noVideoPlaying]) {
self.yb_hideToolViews(NO);
}
}
}
#pragma mark - <YBIBCellProtocol>
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_backView = _yb_backView;
@synthesize yb_collectionView = _yb_collectionView;
@synthesize yb_isTransitioning = _yb_isTransitioning;
@synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
@synthesize yb_hideStatusBar = _yb_hideStatusBar;
@synthesize yb_hideBrowser = _yb_hideBrowser;
@synthesize yb_hideToolViews = _yb_hideToolViews;
@synthesize yb_cellData = _yb_cellData;
@synthesize yb_currentPage = _yb_currentPage;
@synthesize yb_selfPage = _yb_selfPage;
@synthesize yb_cellIsInCenter = _yb_cellIsInCenter;
@synthesize yb_isRotating = _yb_isRotating;
- (void)setYb_cellData:(id<YBIBDataProtocol>)yb_cellData {
_yb_cellData = yb_cellData;
YBIBVideoData *data = (YBIBVideoData *)yb_cellData;
data.delegate = self;
UIDeviceOrientation orientation = self.yb_currentOrientation();
CGSize containerSize = self.yb_containerSize(orientation);
[self.videoView updateLayoutWithExpectOrientation:orientation containerSize:containerSize];
self.videoView.autoPlayCount = data.autoPlayCount;
self.videoView.topBar.cancelButton.hidden = data.shouldHideForkButton;
}
- (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation {
if (_interacting) [self restoreGestureInteractionWithDuration:0];
}
- (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation {
[self updateImageLayoutWithOrientation:orientation previousImageSize:self.videoView.thumbImageView.image.size];
CGSize containerSize = self.yb_containerSize(orientation);
[self.videoView updateLayoutWithExpectOrientation:orientation containerSize:containerSize];
}
- (UIView *)yb_foregroundView {
return self.videoView.thumbImageView;
}
- (void)yb_pageChanged {
if (self.yb_currentPage() != self.yb_selfPage()) {
[self.videoView reset];
[self hideToolViews:NO];
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
if (_interacting) [self restoreGestureInteractionWithDuration:0];
self.videoView.needAutoPlay = NO;
} else {
self.videoView.needAutoPlay = YES;
}
}
#pragma mark - <YBIBVideoDataDelegate>
- (void)yb_startLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data {}
- (void)yb_finishLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data {}
- (void)yb_startLoadingFirstFrameForData:(YBIBVideoData *)data {
if (!self.videoView.thumbImageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
- (void)yb_finishLoadingFirstFrameForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
}
- (void)yb_videoData:(YBIBVideoData *)data downloadingWithProgress:(CGFloat)progress {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self progress:progress];
}
- (void)yb_finishDownloadingForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
}
- (void)yb_videoData:(YBIBVideoData *)data readyForAVAsset:(AVAsset *)asset {
self.videoView.asset = asset;
}
- (void)yb_videoData:(YBIBVideoData *)data readyForThumbImage:(UIImage *)image {
if (!self.videoView.isPlaying) {
self.videoView.thumbImageView.hidden = NO;
}
if (!self.videoView.thumbImageView.image) {
CGSize previousSize = self.videoView.thumbImageView.image.size;
self.videoView.thumbImageView.image = image;
[self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:previousSize];
}
}
- (void)yb_videoIsInvalidForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
NSString *imageIsInvalid = [YBIBCopywriter sharedCopywriter].videoIsInvalid;
if (self.videoView.thumbImageView.image) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:imageIsInvalid];
} else {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:imageIsInvalid];
}
}
#pragma mark - <YBIBVideoViewDelegate>
- (BOOL)yb_isFreezingForVideoView:(YBIBVideoView *)view {
return self.yb_isTransitioning();
}
- (void)yb_preparePlayForVideoView:(YBIBVideoView *)view {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!view.isPlaying && !view.isPlayFailed && self.yb_selfPage() == self.yb_currentPage()) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
});
}
- (void)yb_startPlayForVideoView:(YBIBVideoView *)view {
self.videoView.thumbImageView.hidden = YES;
[self.yb_backView ybib_videoPlayingAdd:self];
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self hideToolViews:YES];
}
- (void)yb_didPlayToEndTimeForVideoView:(YBIBVideoView *)view {
YBIBVideoData *data = (YBIBVideoData *)self.yb_cellData;
if (data.repeatPlayCount == NSUIntegerMax) {
[view preparPlay];
} else if (data.repeatPlayCount > 0) {
--data.repeatPlayCount;
[view preparPlay];
} else {
[self hideToolViews:NO];
}
}
- (void)yb_finishPlayForVideoView:(YBIBVideoView *)view {
[self.yb_backView ybib_videoPlayingRemove:self];
[self hideToolViews:NO];
}
- (void)yb_playFailedForVideoView:(YBIBVideoView *)view {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:YBIBCopywriter.sharedCopywriter.videoError];
}
- (void)yb_respondsToTapGestureForVideoView:(YBIBVideoView *)view {
if (self.yb_isRotating()) return;
YBIBVideoData *data = self.yb_cellData;
if (data.singleTouchBlock) {
data.singleTouchBlock(data);
} else {
[self hideBrowser];
}
}
- (void)yb_cancelledForVideoView:(YBIBVideoView *)view {
if (self.yb_isRotating()) return;
[self hideBrowser];
}
- (CGSize)yb_containerSizeForVideoView:(YBIBVideoView *)view {
return self.yb_containerSize(self.yb_currentOrientation());
}
- (void)yb_autoPlayCountChanged:(NSUInteger)count {
YBIBVideoData *data = (YBIBVideoData *)self.yb_cellData;
data.autoPlayCount = count;
}
#pragma mark - <UIGestureRecognizerDelegate>
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
#pragma mark - gesture
- (void)addGesture {
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToPanGesture:)];
panGesture.cancelsTouchesInView = NO;
panGesture.delegate = self;
[self.videoView.tapGesture requireGestureRecognizerToFail:panGesture];
[self.videoView addGestureRecognizer:panGesture];
}
- (void)respondsToPanGesture:(UIPanGestureRecognizer *)pan {
if (self.yb_isRotating()) return;
if ((!self.videoView.thumbImageView.image && !self.videoView.isPlaying)) return;
YBIBInteractionProfile *profile = ((YBIBVideoData *)self.yb_cellData).interactionProfile;
if (profile.disable) return;
CGPoint point = [pan locationInView:self];
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
if (pan.state == UIGestureRecognizerStateBegan) {
_interactStartPoint = point;
} else if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateRecognized || pan.state == UIGestureRecognizerStateFailed) {
// End
if (_interacting) {
CGPoint velocity = [pan velocityInView:self.videoView];
BOOL velocityArrive = ABS(velocity.y) > profile.dismissVelocityY;
BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > containerSize.height * profile.dismissScale;
BOOL shouldDismiss = distanceArrive || velocityArrive;
if (shouldDismiss) {
[self hideBrowser];
} else {
[self restoreGestureInteractionWithDuration:profile.restoreDuration];
}
}
} else if (pan.state == UIGestureRecognizerStateChanged) {
if (_interacting) {
// Change
self.videoView.center = point;
CGFloat scale = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 1.2);
if (scale > 1) scale = 1;
if (scale < 0.35) scale = 0.35;
self.videoView.transform = CGAffineTransformMakeScale(scale, scale);
CGFloat alpha = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 0.7);
if (alpha > 1) alpha = 1;
if (alpha < 0) alpha = 0;
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:alpha];
} else {
// Start
if (CGPointEqualToPoint(_interactStartPoint, CGPointZero) || self.yb_currentPage() != self.yb_selfPage() || !self.yb_cellIsInCenter() || self.videoView.actionBar.isTouchInside) return;
CGPoint velocityPoint = [pan velocityInView:self.videoView];
CGFloat triggerDistance = profile.triggerDistance;
BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > triggerDistance && (ABS(point.x - _interactStartPoint.x) < triggerDistance && ABS(velocityPoint.x) < 500);
BOOL shouldStart = distanceArrive;
if (!shouldStart) return;
[self.videoView hideToolBar:YES];
_interactStartPoint = point;
CGRect startFrame = self.videoView.bounds;
CGFloat anchorX = (point.x - startFrame.origin.x) / startFrame.size.width,
anchorY = (point.y - startFrame.origin.y) / startFrame.size.height;
self.videoView.layer.anchorPoint = CGPointMake(anchorX, anchorY);
self.videoView.userInteractionEnabled = NO;
self.videoView.center = point;
[self hideToolViews:YES];
self.yb_hideStatusBar(NO);
self.yb_collectionView().scrollEnabled = NO;
_interacting = YES;
}
}
}
- (void)restoreGestureInteractionWithDuration:(NSTimeInterval)duration {
[self.videoView hideToolBar:NO];
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
void (^animations)(void) = ^{
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:1];
CGPoint anchorPoint = self.videoView.layer.anchorPoint;
self.videoView.center = CGPointMake(containerSize.width * anchorPoint.x, containerSize.height * anchorPoint.y);
self.videoView.transform = CGAffineTransformIdentity;
};
void (^completion)(BOOL finished) = ^(BOOL finished){
self.videoView.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.videoView.center = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
self.videoView.userInteractionEnabled = YES;
self.yb_hideStatusBar(YES);
self.yb_collectionView().scrollEnabled = YES;
if (!self.videoView.isPlaying) [self hideToolViews:NO];;
self->_interactStartPoint = CGPointZero;
self->_interacting = NO;
};
if (duration <= 0) {
animations();
completion(NO);
} else {
[UIView animateWithDuration:duration animations:animations completion:completion];
}
}
#pragma mark - getters & setters
- (YBIBVideoView *)videoView {
if (!_videoView) {
_videoView = [YBIBVideoView new];
_videoView.delegate = self;
}
return _videoView;
}
@end

View File

@@ -0,0 +1,50 @@
//
// YBIBVideoData+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/11.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBVideoData.h"
NS_ASSUME_NONNULL_BEGIN
@class YBIBVideoData;
@protocol YBIBVideoDataDelegate <NSObject>
@required
- (void)yb_startLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data;
- (void)yb_finishLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data;
- (void)yb_startLoadingFirstFrameForData:(YBIBVideoData *)data;
- (void)yb_finishLoadingFirstFrameForData:(YBIBVideoData *)data;
- (void)yb_videoData:(YBIBVideoData *)data downloadingWithProgress:(CGFloat)progress;
- (void)yb_finishDownloadingForData:(YBIBVideoData *)data;
- (void)yb_videoData:(YBIBVideoData *)data readyForThumbImage:(UIImage *)image;
- (void)yb_videoData:(YBIBVideoData *)data readyForAVAsset:(AVAsset *)asset;
- (void)yb_videoIsInvalidForData:(YBIBVideoData *)data;
@end
@interface YBIBVideoData ()
@property (nonatomic, assign, getter=isLoadingAVAssetFromPHAsset) BOOL loadingAVAssetFromPHAsset;
@property (nonatomic, assign, getter=isLoadingFirstFrame) BOOL loadingFirstFrame;
@property (nonatomic, assign, getter=isDownloading) BOOL downloading;
@property (nonatomic, weak) id<YBIBVideoDataDelegate> delegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,64 @@
//
// YBIBVideoData.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/10.
// Copyright © 2019 杨波. All rights reserved.
//
#import <Photos/Photos.h>
#import "YBIBDataProtocol.h"
#import "YBIBInteractionProfile.h"
NS_ASSUME_NONNULL_BEGIN
@class YBIBVideoData;
/// 单击事件的处理闭包
typedef void (^YBIBVideoSingleTouchBlock)(YBIBVideoData *videoData);
/**
图片数据类,承担配置数据和处理数据的责任
*/
@interface YBIBVideoData : NSObject <YBIBDataProtocol>
/// 视频 URL
@property (nonatomic, copy, nullable) NSURL *videoURL;
/// 相册视频资源
@property (nonatomic, strong, nullable) PHAsset *videoPHAsset;
/// 视频 AVAsset (通常使用 AVURLAsset)
@property (nonatomic, strong, nullable) AVAsset *videoAVAsset;
/// 投影视图,当前数据模型对应外界业务的 UIView (通常为 UIImageView),做转场动效用
@property (nonatomic, weak, nullable) __kindof UIView *projectiveView;
/// 预览图/缩约图,若 projectiveView 存在且是 UIImageView 类型将会自动获取缩约图
@property (nonatomic, strong, nullable) UIImage *thumbImage;
/// 是否允许保存到相册
@property (nonatomic, assign) BOOL allowSaveToPhotoAlbum;
/// 自动播放次数,默认为 0NSUIntegerMax 表示无限次
@property (nonatomic, assign) NSUInteger autoPlayCount;
/// 重复播放次数,默认为 0NSUIntegerMax 表示无限次
@property (nonatomic, assign) NSUInteger repeatPlayCount;
/// 预留属性可随意使用
@property (nonatomic, strong, nullable) id extraData;
/// 手势交互动效配置文件
@property (nonatomic, strong) YBIBInteractionProfile *interactionProfile;
/// 单击的处理(视频未播放时),默认是退出图片浏览器
@property (nonatomic, copy, nullable) YBIBVideoSingleTouchBlock singleTouchBlock;
/// 是否要隐藏播放时的叉叉(取消)按钮
@property (nonatomic, assign) BOOL shouldHideForkButton;
@end
NS_ASSUME_NONNULL_END

296
Pods/YBImageBrowser/Video/YBIBVideoData.m generated Normal file
View File

@@ -0,0 +1,296 @@
//
// YBIBVideoData.m
// YBImageBrowserDemo
//
// Created by on 2019/7/10.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBVideoData.h"
#import "YBIBVideoCell.h"
#import "YBIBVideoData+Internal.h"
#import "YBIBUtilities.h"
#import "YBIBPhotoAlbumManager.h"
#import "YBIBCopywriter.h"
extern CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay);
@interface YBIBVideoData () <NSURLSessionDelegate>
@end
@implementation YBIBVideoData {
NSURLSessionDownloadTask *_downloadTask;
}
#pragma mark - life cycle
- (instancetype)init {
self = [super init];
if (self) {
[self initValue];
}
return self;
}
- (void)initValue {
_loadingFirstFrame = NO;
_loadingAVAssetFromPHAsset = NO;
_downloading = NO;
_interactionProfile = [YBIBInteractionProfile new];
_repeatPlayCount = 0;
_autoPlayCount = 0;
_shouldHideForkButton = NO;
_allowSaveToPhotoAlbum = YES;
}
#pragma mark - load data
- (void)loadData {
// Always load 'thumbImage'.
[self loadThumbImage];
if (self.videoAVAsset) {
[self.delegate yb_videoData:self readyForAVAsset:self.videoAVAsset];
} else if (self.videoPHAsset) {
[self loadAVAssetFromPHAsset];
} else {
[self.delegate yb_videoIsInvalidForData:self];
}
}
- (void)loadAVAssetFromPHAsset {
if (!self.videoPHAsset) return;
if (self.isLoadingAVAssetFromPHAsset) {
self.loadingAVAssetFromPHAsset = YES;
return;
}
self.loadingAVAssetFromPHAsset = YES;
[YBIBPhotoAlbumManager getAVAssetWithPHAsset:self.videoPHAsset completion:^(AVAsset * _Nullable asset) {
YBIB_DISPATCH_ASYNC_MAIN(^{
self.loadingAVAssetFromPHAsset = NO;
self.videoAVAsset = asset;
[self.delegate yb_videoData:self readyForAVAsset:self.videoAVAsset];
[self loadThumbImage];
})
}];
}
- (void)loadThumbImage {
if (self.thumbImage) {
[self.delegate yb_videoData:self readyForThumbImage:self.thumbImage];
} else if (self.projectiveView && [self.projectiveView isKindOfClass:UIImageView.self] && ((UIImageView *)self.projectiveView).image) {
self.thumbImage = ((UIImageView *)self.projectiveView).image;
[self.delegate yb_videoData:self readyForThumbImage:self.thumbImage];
} else {
[self loadThumbImage_firstFrame];
}
}
- (void)loadThumbImage_firstFrame {
if (!self.videoAVAsset) return;
if (self.isLoadingFirstFrame) {
self.loadingFirstFrame = YES;
return;
}
self.loadingFirstFrame = YES;
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
CGSize maximumSize = containerSize;
__weak typeof(self) wSelf = self;
YBIB_DISPATCH_ASYNC(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.videoAVAsset];
generator.appliesPreferredTrackTransform = YES;
generator.maximumSize = maximumSize;
NSError *error = nil;
CGImageRef cgImage = [generator copyCGImageAtTime:CMTimeMake(0, 1) actualTime:NULL error:&error];
CGImageRef decodedImage = YYCGImageCreateDecodedCopy(cgImage, YES);
UIImage *resultImage = [UIImage imageWithCGImage:decodedImage];
if (cgImage) CGImageRelease(cgImage);
if (decodedImage) CGImageRelease(decodedImage);
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingFirstFrame = NO;
if (!error && resultImage) {
self.thumbImage = resultImage;
[self.delegate yb_videoData:self readyForThumbImage:self.thumbImage];
}
})
})
}
#pragma mark - <YBIBDataProtocol>
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_containerView = _yb_containerView;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_isHideTransitioning = _yb_isHideTransitioning;
@synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
- (nonnull Class)yb_classOfCell {
return YBIBVideoCell.self;
}
- (UIView *)yb_projectiveView {
return self.projectiveView;
}
- (CGRect)yb_imageViewFrameWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation {
if (containerSize.width <= 0 || containerSize.height <= 0 || imageSize.width <= 0 || imageSize.height <= 0) return CGRectZero;
CGFloat x = 0, y = 0, width = 0, height = 0;
if (imageSize.width / imageSize.height >= containerSize.width / containerSize.height) {
width = containerSize.width;
height = containerSize.width * (imageSize.height / imageSize.width);
x = 0;
y = (containerSize.height - height) / 2.0;
} else {
height = containerSize.height;
width = containerSize.height * (imageSize.width / imageSize.height);
x = (containerSize.width - width) / 2.0;
y = 0;
}
return CGRectMake(x, y, width, height);
}
- (void)yb_preload {
if (!self.delegate) {
[self loadData];
}
}
- (BOOL)yb_allowSaveToPhotoAlbum {
return self.allowSaveToPhotoAlbum;
}
- (void)yb_saveToPhotoAlbum {
void(^unableToSave)(void) = ^(){
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].unableToSave];
};
if (self.videoAVAsset && [self.videoAVAsset isKindOfClass:AVURLAsset.class]) {
AVURLAsset *asset = (AVURLAsset *)self.videoAVAsset;
NSURL *URL = asset.URL;
if ([URL.scheme isEqualToString:@"file"]) {
NSString *path = URL.path;
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path)) {
UISaveVideoAtPathToSavedPhotosAlbum(path, self, @selector(UISaveVideoAtPathToSavedPhotosAlbum_videoPath:didFinishSavingWithError:contextInfo:), nil);
} else {
unableToSave();
}
} else if ([URL.scheme containsString:@"http"]) {
[self downloadWithURL:URL];
} else {
unableToSave();
}
} else {
unableToSave();
}
}
#pragma mark - private
- (void)UISaveVideoAtPathToSavedPhotosAlbum_videoPath:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{
if (error) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbumFailed];
} else {
[self.yb_auxiliaryViewHandler() yb_showCorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbumSuccess];
}
}
- (void)downloadWithURL:(NSURL *)URL {
if (self.isDownloading) {
self.downloading = YES;
return;
}
self.downloading = YES;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
_downloadTask = [session downloadTaskWithURL:URL];
[_downloadTask resume];
}
#pragma mark - <NSURLSessionDelegate>
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
CGFloat progress = totalBytesWritten / (double)totalBytesExpectedToWrite;
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
[self.delegate yb_videoData:self downloadingWithProgress:progress];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
if (error) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].downloadFailed];
}
self.downloading = NO;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *file = [cache stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:file] error:nil];
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(file)) {
UISaveVideoAtPathToSavedPhotosAlbum(file, self, @selector(UISaveVideoAtPathToSavedPhotosAlbum_videoPath:didFinishSavingWithError:contextInfo:), nil);
} else {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbumFailed];
}
self.downloading = NO;
}
#pragma mark - getters & setters
- (void)setVideoURL:(NSURL *)videoURL{
_videoURL = [videoURL isKindOfClass:NSString.class] ? [NSURL URLWithString:(NSString *)videoURL] : videoURL;
self.videoAVAsset = [AVURLAsset URLAssetWithURL:_videoURL options:nil];
}
- (void)setDownloading:(BOOL)downloading {
_downloading = downloading;
if (downloading) {
[self.delegate yb_videoData:self downloadingWithProgress:0];
} else {
[self.delegate yb_finishDownloadingForData:self];
}
}
- (void)setLoadingAVAssetFromPHAsset:(BOOL)loadingAVAssetFromPHAsset {
_loadingAVAssetFromPHAsset = loadingAVAssetFromPHAsset;
if (loadingAVAssetFromPHAsset) {
[self.delegate yb_startLoadingAVAssetFromPHAssetForData:self];
} else {
[self.delegate yb_finishLoadingAVAssetFromPHAssetForData:self];
}
}
- (void)setLoadingFirstFrame:(BOOL)loadingFirstFrame {
_loadingFirstFrame = loadingFirstFrame;
if (loadingFirstFrame) {
[self.delegate yb_startLoadingFirstFrameForData:self];
} else {
[self.delegate yb_finishLoadingFirstFrameForData:self];
}
}
@synthesize delegate = _delegate;
- (void)setDelegate:(id<YBIBVideoDataDelegate>)delegate {
_delegate = delegate;
if (delegate) {
[self loadData];
}
}
- (id<YBIBVideoDataDelegate>)delegate {
// Stop sending data to the '_delegate' if it is transiting.
return self.yb_isHideTransitioning() ? nil : _delegate;
}
@end

View File

@@ -0,0 +1,21 @@
//
// YBIBVideoTopBar.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/11.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface YBIBVideoTopBar : UIView
@property (nonatomic, strong, readonly) UIButton *cancelButton;
+ (CGFloat)defaultHeight;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,54 @@
//
// YBIBVideoTopBar.m
// YBImageBrowserDemo
//
// Created by on 2019/7/11.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBVideoTopBar.h"
#import "YBIBIconManager.h"
@interface YBIBVideoTopBar ()
@property (nonatomic, strong) UIButton *cancelButton;
@end
@implementation YBIBVideoTopBar
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.cancelButton];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat buttonWidth = 54;
self.cancelButton.frame = CGRectMake(0, 0, buttonWidth, self.bounds.size.height);
}
#pragma mark - public
+ (CGFloat)defaultHeight {
return 50;
}
#pragma mark - getter
- (UIButton *)cancelButton {
if (!_cancelButton) {
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_cancelButton setImage:YBIBIconManager.sharedManager.videoCancelImage() forState:UIControlStateNormal];
_cancelButton.layer.shadowColor = UIColor.darkGrayColor.CGColor;
_cancelButton.layer.shadowOffset = CGSizeMake(0, 1);
_cancelButton.layer.shadowOpacity = 1;
_cancelButton.layer.shadowRadius = 4;
}
return _cancelButton;
}
@end

View File

@@ -0,0 +1,76 @@
//
// YBIBVideoView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/11.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBVideoActionBar.h"
#import "YBIBVideoTopBar.h"
NS_ASSUME_NONNULL_BEGIN
@class YBIBVideoView;
@protocol YBIBVideoViewDelegate <NSObject>
@required
- (BOOL)yb_isFreezingForVideoView:(YBIBVideoView *)view;
- (void)yb_preparePlayForVideoView:(YBIBVideoView *)view;
- (void)yb_startPlayForVideoView:(YBIBVideoView *)view;
- (void)yb_finishPlayForVideoView:(YBIBVideoView *)view;
- (void)yb_didPlayToEndTimeForVideoView:(YBIBVideoView *)view;
- (void)yb_playFailedForVideoView:(YBIBVideoView *)view;
- (void)yb_respondsToTapGestureForVideoView:(YBIBVideoView *)view;
- (void)yb_cancelledForVideoView:(YBIBVideoView *)view;
- (CGSize)yb_containerSizeForVideoView:(YBIBVideoView *)view;
- (void)yb_autoPlayCountChanged:(NSUInteger)count;
@end
@interface YBIBVideoView : UIView
@property (nonatomic, strong) UIImageView *thumbImageView;
@property (nonatomic, weak) id<YBIBVideoViewDelegate> delegate;
- (void)updateLayoutWithExpectOrientation:(UIDeviceOrientation)orientation containerSize:(CGSize)containerSize;
@property (nonatomic, strong, nullable) AVAsset *asset;
@property (nonatomic, assign, readonly, getter=isPlaying) BOOL playing;
@property (nonatomic, assign, readonly, getter=isPlayFailed) BOOL playFailed;
@property (nonatomic, assign, readonly, getter=isPreparingPlay) BOOL preparingPlay;
@property (nonatomic, strong, readonly) UITapGestureRecognizer *tapGesture;
- (void)reset;
- (void)hideToolBar:(BOOL)hide;
- (void)hidePlayButton;
- (void)preparPlay;
@property (nonatomic, assign) BOOL needAutoPlay;
@property (nonatomic, assign) NSUInteger autoPlayCount;
@property (nonatomic, strong, readonly) YBIBVideoTopBar *topBar;
@property (nonatomic, strong, readonly) YBIBVideoActionBar *actionBar;
@end
NS_ASSUME_NONNULL_END

394
Pods/YBImageBrowser/Video/YBIBVideoView.m generated Normal file
View File

@@ -0,0 +1,394 @@
//
// YBIBVideoView.m
// YBImageBrowserDemo
//
// Created by on 2019/7/11.
// Copyright © 2019 . All rights reserved.
//
#import <AVFoundation/AVFoundation.h>
#import "YBIBVideoView.h"
#import "YBIBVideoActionBar.h"
#import "YBIBVideoTopBar.h"
#import "YBIBUtilities.h"
#import "YBIBIconManager.h"
@interface YBIBVideoView () <YBIBVideoActionBarDelegate>
@property (nonatomic, strong) YBIBVideoTopBar *topBar;
@property (nonatomic, strong) YBIBVideoActionBar *actionBar;
@property (nonatomic, strong) UIButton *playButton;
@end
@implementation YBIBVideoView {
AVPlayer *_player;
AVPlayerItem *_playerItem;
AVPlayerLayer *_playerLayer;
BOOL _active;
}
#pragma mark - life cycle
- (void)dealloc {
[self removeObserverForSystem];
[self reset];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initValue];
self.backgroundColor = UIColor.clearColor;
[self addSubview:self.thumbImageView];
[self addSubview:self.topBar];
[self addSubview:self.actionBar];
[self addSubview:self.playButton];
[self addObserverForSystem];
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapGesture:)];
[self addGestureRecognizer:_tapGesture];
}
return self;
}
- (void)initValue {
_playing = NO;
_active = YES;
_needAutoPlay = NO;
_autoPlayCount = 0;
_playFailed = NO;
_preparingPlay = NO;
}
#pragma mark - public
- (void)updateLayoutWithExpectOrientation:(UIDeviceOrientation)orientation containerSize:(CGSize)containerSize {
UIEdgeInsets padding = YBIBPaddingByBrowserOrientation(orientation);
CGFloat width = containerSize.width - padding.left - padding.right, height = containerSize.height;
self.topBar.frame = CGRectMake(padding.left, padding.top, width, [YBIBVideoTopBar defaultHeight]);
self.actionBar.frame = CGRectMake(padding.left, height - [YBIBVideoActionBar defaultHeight] - padding.bottom - 10, width, [YBIBVideoActionBar defaultHeight]);
self.playButton.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
_playerLayer.frame = (CGRect){CGPointZero, containerSize};
}
- (void)reset {
[self removeObserverForPlayer];
// If set '_playerLayer.player = nil' or '_player = nil', can not cancel observeing of 'addPeriodicTimeObserverForInterval'.
[_player pause];
_playerItem = nil;
[_playerLayer removeFromSuperlayer];
_playerLayer = nil;
[self finishPlay];
}
- (void)hideToolBar:(BOOL)hide {
if (hide) {
self.actionBar.hidden = YES;
self.topBar.hidden = YES;
} else if (self.isPlaying) {
self.actionBar.hidden = NO;
self.topBar.hidden = NO;
}
}
- (void)hidePlayButton {
self.playButton.hidden = YES;
}
#pragma mark - private
- (void)videoJumpWithScale:(float)scale {
CMTime startTime = CMTimeMakeWithSeconds(scale, _player.currentTime.timescale);
AVPlayer *tmpPlayer = _player;
if (CMTIME_IS_INDEFINITE(startTime) || CMTIME_IS_INVALID(startTime)) return;
[_player seekToTime:startTime toleranceBefore:CMTimeMake(1, 1000) toleranceAfter:CMTimeMake(1, 1000) completionHandler:^(BOOL finished) {
if (finished && tmpPlayer == self->_player) {
[self startPlay];
}
}];
}
- (void)preparPlay {
_preparingPlay = YES;
_playFailed = NO;
self.playButton.hidden = YES;
[self.delegate yb_preparePlayForVideoView:self];
if (!_playerLayer) {
_playerItem = [AVPlayerItem playerItemWithAsset:self.asset];
_player = [AVPlayer playerWithPlayerItem:_playerItem];
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
_playerLayer.frame = (CGRect){CGPointZero, [self.delegate yb_containerSizeForVideoView:self]};
[self.layer insertSublayer:_playerLayer above:self.thumbImageView.layer];
[self addObserverForPlayer];
} else {
[self videoJumpWithScale:0];
}
}
- (void)startPlay {
if (_player) {
_playing = YES;
[_player play];
[self.actionBar play];
self.topBar.hidden = NO;
self.actionBar.hidden = NO;
[self.delegate yb_startPlayForVideoView:self];
}
}
- (void)finishPlay {
self.playButton.hidden = NO;
[self.actionBar setCurrentValue:0];
self.actionBar.hidden = YES;
self.topBar.hidden = YES;
_playing = NO;
[self.delegate yb_finishPlayForVideoView:self];
}
- (void)playerPause {
if (_player) {
[_player pause];
[self.actionBar pause];
}
}
- (BOOL)autoPlay {
if (self.autoPlayCount == NSUIntegerMax) {
[self preparPlay];
} else if (self.autoPlayCount > 0) {
--self.autoPlayCount;
[self.delegate yb_autoPlayCountChanged:self.autoPlayCount];
[self preparPlay];
} else {
return NO;
}
return YES;
}
#pragma mark - <YBIBVideoActionBarDelegate>
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar clickPlayButton:(UIButton *)playButton {
[self startPlay];
}
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar clickPauseButton:(UIButton *)pauseButton {
[self playerPause];
}
- (void)yb_videoActionBar:(YBIBVideoActionBar *)actionBar changeValue:(float)value {
[self videoJumpWithScale:value];
}
#pragma mark - observe
- (void)addObserverForPlayer {
[_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
__weak typeof(self) wSelf = self;
[_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
float currentTime = time.value / time.timescale;
[self.actionBar setCurrentValue:currentTime];
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didPlayToEndTime:) name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
}
- (void)removeObserverForPlayer {
[_playerItem removeObserver:self forKeyPath:@"status"];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (![self.delegate yb_isFreezingForVideoView:self]) {
if (object == _playerItem) {
if ([keyPath isEqualToString:@"status"]) {
[self playerItemStatusChanged];
}
}
}
}
- (void)didPlayToEndTime:(NSNotification *)noti {
if (noti.object == _playerItem) {
[self finishPlay];
[self.delegate yb_didPlayToEndTimeForVideoView:self];
}
}
- (void)playerItemStatusChanged {
if (!_active) return;
_preparingPlay = NO;
switch (_playerItem.status) {
case AVPlayerItemStatusReadyToPlay: {
// Delay to update UI.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startPlay];
double max = CMTimeGetSeconds(self->_playerItem.duration);
[self.actionBar setMaxValue:(isnan(max) || isinf(max)) ? 0 : max];
});
}
break;
case AVPlayerItemStatusUnknown: {
_playFailed = YES;
[self.delegate yb_playFailedForVideoView:self];
[self reset];
}
break;
case AVPlayerItemStatusFailed: {
_playFailed = YES;
[self.delegate yb_playFailedForVideoView:self];
[self reset];
}
break;
}
}
- (void)removeObserverForSystem {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
}
- (void)addObserverForSystem {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarFrame) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
}
- (void)applicationWillResignActive:(NSNotification *)notification {
_active = NO;
[self playerPause];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
_active = YES;
}
- (void)didChangeStatusBarFrame {
if ([UIApplication sharedApplication].statusBarFrame.size.height > YBIBStatusbarHeight()) {
[self playerPause];
}
}
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification {
YBIB_DISPATCH_ASYNC_MAIN(^{
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
[self playerPause];
break;
}
})
}
#pragma mark - event
- (void)respondsToTapGesture:(UITapGestureRecognizer *)tap {
if (self.isPlaying) {
self.actionBar.hidden = !self.actionBar.isHidden;
self.topBar.hidden = !self.topBar.isHidden;
} else {
[self.delegate yb_respondsToTapGestureForVideoView:self];
}
}
- (void)clickCancelButton:(UIButton *)button {
[self.delegate yb_cancelledForVideoView:self];
}
- (void)clickPlayButton:(UIButton *)button {
[self preparPlay];
}
#pragma mark - getters & setters
- (void)setNeedAutoPlay:(BOOL)needAutoPlay {
if (needAutoPlay && _asset && !self.isPlaying) {
[self autoPlay];
} else {
_needAutoPlay = needAutoPlay;
}
}
@synthesize asset = _asset;
- (void)setAsset:(AVAsset *)asset {
_asset = asset;
if (!asset) return;
if (self.needAutoPlay) {
if (![self autoPlay]) {
self.playButton.hidden = NO;
}
self.needAutoPlay = NO;
} else {
self.playButton.hidden = NO;
}
}
- (AVAsset *)asset {
if ([_asset isKindOfClass:AVURLAsset.class]) {
_asset = [AVURLAsset assetWithURL:((AVURLAsset *)_asset).URL];
}
return _asset;
}
- (YBIBVideoTopBar *)topBar {
if (!_topBar) {
_topBar = [YBIBVideoTopBar new];
[_topBar.cancelButton addTarget:self action:@selector(clickCancelButton:) forControlEvents:UIControlEventTouchUpInside];
_topBar.hidden = YES;
}
return _topBar;
}
- (YBIBVideoActionBar *)actionBar {
if (!_actionBar) {
_actionBar = [YBIBVideoActionBar new];
_actionBar.delegate = self;
_actionBar.hidden = YES;
}
return _actionBar;
}
- (UIButton *)playButton {
if (!_playButton) {
_playButton = [UIButton buttonWithType:UIButtonTypeCustom];
_playButton.bounds = CGRectMake(0, 0, 100, 100);
[_playButton setImage:YBIBIconManager.sharedManager.videoBigPlayImage() forState:UIControlStateNormal];
[_playButton addTarget:self action:@selector(clickPlayButton:) forControlEvents:UIControlEventTouchUpInside];
_playButton.hidden = YES;
_playButton.layer.shadowColor = UIColor.darkGrayColor.CGColor;
_playButton.layer.shadowOffset = CGSizeMake(0, 1);
_playButton.layer.shadowOpacity = 1;
_playButton.layer.shadowRadius = 4;
}
return _playButton;
}
- (UIImageView *)thumbImageView {
if (!_thumbImageView) {
_thumbImageView = [UIImageView new];
_thumbImageView.contentMode = UIViewContentModeScaleAspectFit;
_thumbImageView.layer.masksToBounds = YES;
}
return _thumbImageView;
}
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,39 @@
//
// YBIBAuxiliaryViewHandler.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/27.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBAuxiliaryViewHandler <NSObject>
@required
/// 展示正确情况的提示
- (void)yb_showCorrectToastWithContainer:(UIView *)container text:(NSString *)text;
/// 展示错误情况的提示
- (void)yb_showIncorrectToastWithContainer:(UIView *)container text:(NSString *)text;
/// 隐藏所有提示
- (void)yb_hideToastWithContainer:(UIView *)container;
/// 展示加载视图
- (void)yb_showLoadingWithContainer:(UIView *)container;
/// 展示带进度的加载视图
- (void)yb_showLoadingWithContainer:(UIView *)container progress:(CGFloat)progress;
/// 展示带文字的视图
- (void)yb_showLoadingWithContainer:(UIView *)container text:(NSString *)text;
/// 隐藏所有视图
- (void)yb_hideLoadingWithContainer:(UIView *)container;
@end
@interface YBIBAuxiliaryViewHandler : NSObject <YBIBAuxiliaryViewHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,45 @@
//
// YBIBAuxiliaryViewHandler.m
// YBImageBrowserDemo
//
// Created by on 2019/6/27.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBAuxiliaryViewHandler.h"
#import "YBIBToastView.h"
#import "YBIBLoadingView.h"
@implementation YBIBAuxiliaryViewHandler
#pragma mark - <YBIBAuxiliaryViewHandler>
- (void)yb_showCorrectToastWithContainer:(UIView *)container text:(NSString *)text {
[container ybib_showHookToast:text];
}
- (void)yb_showIncorrectToastWithContainer:(UIView *)container text:(NSString *)text {
[container ybib_showForkToast:text];
}
- (void)yb_hideToastWithContainer:(UIView *)container {
[container ybib_hideToast];
}
- (void)yb_showLoadingWithContainer:(UIView *)container {
[container ybib_showLoading];
}
- (void)yb_showLoadingWithContainer:(UIView *)container progress:(CGFloat)progress {
[container ybib_showLoadingWithProgress:progress];
}
- (void)yb_showLoadingWithContainer:(UIView *)container text:(NSString *)text {
[container ybib_showLoadingWithText:text click:nil];
}
- (void)yb_hideLoadingWithContainer:(UIView *)container {
[container ybib_hideLoading];
}
@end

View File

@@ -0,0 +1,36 @@
//
// YBIBLoadingView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/9/1.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (YBIBLoading)
- (void)ybib_showLoading;
- (void)ybib_showLoadingWithProgress:(CGFloat)progress;
- (void)ybib_showLoadingWithText:(NSString *)text click:(nullable void(^)(void))click;
- (void)ybib_hideLoading;
@end
@interface YBIBLoadingView : UIView
- (void)show;
- (void)showProgress:(CGFloat)progress;
- (void)showText:(NSString *)text click:(void(^)(void))click;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,258 @@
//
// YBIBLoadingView.m
// YBImageBrowserDemo
//
// Created by on 2018/9/1.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBLoadingView.h"
#import "YBIBIconManager.h"
#import <objc/runtime.h>
@interface UIView ()
@property (nonatomic, strong, readonly) YBIBLoadingView *ybib_loading;
@end
@implementation UIView (YBIBLoading)
- (void)ybib_showLoadingWithProgress:(CGFloat)progress {
[self ybib_addLoadingView];
[self.ybib_loading showProgress:progress];
}
- (void)ybib_showLoading {
[self ybib_addLoadingView];
[self.ybib_loading show];
}
- (void)ybib_showLoadingWithText:(NSString *)text click:(void (^)(void))click {
[self ybib_addLoadingView];
[self.ybib_loading showText:text click:click];
}
- (void)ybib_addLoadingView {
YBIBLoadingView *loading = self.ybib_loading;
if (!loading.superview) {
[self addSubview:loading];
loading.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layA = [NSLayoutConstraint constraintWithItem:loading attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
NSLayoutConstraint *layB = [NSLayoutConstraint constraintWithItem:loading attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:0];
NSLayoutConstraint *layC = [NSLayoutConstraint constraintWithItem:loading attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
NSLayoutConstraint *layD = [NSLayoutConstraint constraintWithItem:loading attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1 constant:0];
[self addConstraints:@[layA, layB, layC, layD]];
}
}
- (void)ybib_hideLoading {
YBIBLoadingView *loading = self.ybib_loading;
if (loading && loading.superview) {
[loading removeFromSuperview];
}
}
static void *YBIBLoadingKey = &YBIBLoadingKey;
- (void)setYbib_loading:(YBIBLoadingView *)ybib_loading {
objc_setAssociatedObject(self, YBIBLoadingKey, ybib_loading, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (YBIBLoadingView *)ybib_loading {
YBIBLoadingView *loading = objc_getAssociatedObject(self, YBIBLoadingKey);
if (!loading) {
loading = [YBIBLoadingView new];
self.ybib_loading = loading;
}
return loading;
}
@end
@interface YBIBProgressDrawView : UIView
@property (nonatomic, assign) CGFloat progress;
@end
@implementation YBIBProgressDrawView
- (void)drawRect:(CGRect)rect {
if (self.isHidden) return;
CGFloat progress = (isnan(_progress) || isinf(_progress) || _progress < 0) ? 0 : _progress;
CGFloat radius = 17;
CGFloat strokeWidth = 3;
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[[UIColor lightGrayColor] setStroke];
UIBezierPath *bottomPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
bottomPath.lineWidth = 4.0;
bottomPath.lineCapStyle = kCGLineCapRound;
bottomPath.lineJoinStyle = kCGLineCapRound;
[bottomPath stroke];
[[UIColor whiteColor] setStroke];
UIBezierPath *activePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:-M_PI / 2.0 endAngle:M_PI * 2 * progress - M_PI / 2.0 clockwise:true];
activePath.lineWidth = strokeWidth;
activePath.lineCapStyle = kCGLineCapRound;
activePath.lineJoinStyle = kCGLineCapRound;
[activePath stroke];
NSShadow *shadow = [NSShadow new];
shadow.shadowBlurRadius = 4;
shadow.shadowOffset = CGSizeMake(0, 1);
shadow.shadowColor = UIColor.darkGrayColor;
NSString *string = [NSString stringWithFormat:@"%.0lf%@", progress * 100, @"%"];
NSMutableAttributedString *atts = [[NSMutableAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName:[UIFont boldSystemFontOfSize:10], NSForegroundColorAttributeName:[UIColor whiteColor], NSShadowAttributeName:shadow}];
CGSize size = atts.size;
[atts drawAtPoint:CGPointMake(center.x - size.width / 2.0, center.y - size.height / 2.0)];
}
@end
typedef NS_ENUM(NSUInteger, YBImageBrowserProgressType) {
YBImageBrowserProgressTypeProgress,
YBImageBrowserProgressTypeLoad,
YBImageBrowserProgressTypeText
};
@interface YBIBLoadingView () {
YBImageBrowserProgressType _type;
}
@property (nonatomic, strong) UILabel *textLabel;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) YBIBProgressDrawView *drawView;
@property (nonatomic, copy) void(^clickTextLabelBlock)(void);
@end
@implementation YBIBLoadingView
#pragma mark life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0];
self.userInteractionEnabled = NO;
[self addSubview:self.drawView];
[self addSubview:self.textLabel];
[self addSubview:self.imageView];
}
return self;
}
- (void)updateConstraints {
self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layA = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:20];
NSLayoutConstraint *layB = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:-20];
NSLayoutConstraint *layC = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layE = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSLayoutConstraint *layF = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
self.drawView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layG = [NSLayoutConstraint constraintWithItem:self.drawView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSLayoutConstraint *layH = [NSLayoutConstraint constraintWithItem:self.drawView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
NSLayoutConstraint *layI = [NSLayoutConstraint constraintWithItem:self.drawView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:50];
NSLayoutConstraint *layJ = [NSLayoutConstraint constraintWithItem:self.drawView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:50];
[self addConstraints:@[layA, layB, layC, layE, layF, layG, layH, layI, layJ]];
[super updateConstraints];
}
#pragma mark public
- (void)showProgress:(CGFloat)progress {
self.userInteractionEnabled = NO;
_type = YBImageBrowserProgressTypeProgress;
self.drawView.hidden = NO;
self.textLabel.hidden = YES;
self.imageView.hidden = YES;
[self stopImageViewAnimation];
self.drawView.progress = progress;
[self.drawView setNeedsDisplay];
}
- (void)show {
self.userInteractionEnabled = NO;
_type = YBImageBrowserProgressTypeLoad;
self.drawView.hidden = YES;
self.textLabel.hidden = YES;
self.imageView.hidden = NO;
[self startImageViewAnimation];
[self.drawView setNeedsDisplay];
}
- (void)startImageViewAnimation {
CABasicAnimation *ra = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
ra.toValue = [NSNumber numberWithFloat:M_PI * 2];
ra.duration = 1;
ra.cumulative = YES;
ra.repeatCount = HUGE_VALF;
ra.removedOnCompletion = NO;
ra.fillMode = kCAFillModeForwards;
[self.imageView.layer addAnimation:ra forKey:@"ra"];
}
- (void)stopImageViewAnimation {
[self.imageView.layer removeAllAnimations];
}
- (void)showText:(NSString *)text click:(void (^)(void))click {
self.userInteractionEnabled = click ? YES : NO;
_type = YBImageBrowserProgressTypeText;
self.drawView.hidden = YES;
self.textLabel.hidden = NO;
self.imageView.hidden = YES;
[self stopImageViewAnimation];
self.textLabel.text = text;
self.clickTextLabelBlock = click;
[self.drawView setNeedsDisplay];
}
#pragma mark - touch event
- (void)respondsToTapTextlabel {
if (self.clickTextLabelBlock) {
self.clickTextLabelBlock();
}
}
#pragma mark - getter
- (YBIBProgressDrawView *)drawView {
if (!_drawView) {
_drawView = [YBIBProgressDrawView new];
_drawView.backgroundColor = [UIColor clearColor];
}
return _drawView;
}
- (UILabel *)textLabel {
if (!_textLabel) {
_textLabel = [UILabel new];
_textLabel.textColor = [UIColor whiteColor];
_textLabel.numberOfLines = 0;
_textLabel.font = [UIFont systemFontOfSize:14];
_textLabel.textAlignment = NSTextAlignmentCenter;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapTextlabel)];
[_textLabel addGestureRecognizer:tapGesture];
_textLabel.userInteractionEnabled = YES;
}
return _textLabel;
}
- (UIImageView *)imageView {
if (!_imageView) {
_imageView = [UIImageView new];
_imageView.image = [YBIBIconManager sharedManager].loadingImage();
_imageView.layer.shadowColor = UIColor.darkGrayColor.CGColor;
_imageView.layer.shadowOffset = CGSizeMake(0, 1);
_imageView.layer.shadowOpacity = 1;
_imageView.layer.shadowRadius = 4;
}
return _imageView;
}
@end

View File

@@ -0,0 +1,36 @@
//
// YBIBToastView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/20.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (YBIBToast)
- (void)ybib_showHookToast:(NSString *)text;
- (void)ybib_showForkToast:(NSString *)text;
- (void)ybib_hideToast;
@end
typedef NS_ENUM(NSInteger, YBIBToastType) {
YBIBToastTypeNone,
YBIBToastTypeHook,
YBIBToastTypeFork
};
@interface YBIBToastView : UIView
- (void)showWithText:(NSString *)text type:(YBIBToastType)type;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,179 @@
//
// YBIBToastView.m
// YBImageBrowserDemo
//
// Created by on 2019/6/20.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBToastView.h"
#import <objc/runtime.h>
@interface UIView ()
@property (nonatomic, strong, readonly) YBIBToastView *ybib_toast;
@end
@implementation UIView (YBIBToast)
- (void)ybib_showHookToast:(NSString *)text {
[self ybib_showToastWithText:text type:YBIBToastTypeHook hideAfterDelay:1.7];
}
- (void)ybib_showForkToast:(NSString *)text {
[self ybib_showToastWithText:text type:YBIBToastTypeFork hideAfterDelay:1.7];
}
- (void)ybib_showToastWithText:(NSString *)text type:(YBIBToastType)type hideAfterDelay:(NSTimeInterval)delay {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(ybib_hideToast) object:nil];
YBIBToastView *toast = self.ybib_toast;
if (!toast.superview) {
[self addSubview:toast];
toast.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layA = [NSLayoutConstraint constraintWithItem:toast attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSLayoutConstraint *layB = [NSLayoutConstraint constraintWithItem:toast attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
NSLayoutConstraint *layC = [NSLayoutConstraint constraintWithItem:toast attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:40];
NSLayoutConstraint *layD = [NSLayoutConstraint constraintWithItem:toast attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationLessThanOrEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:-40];
[self addConstraints:@[layA, layB, layC, layD]];
}
[toast showWithText:text type:type];
[self performSelector:@selector(ybib_hideToast) withObject:nil afterDelay:delay];
}
- (void)ybib_hideToast {
YBIBToastView *toast = self.ybib_toast;
if (toast && toast.superview) {
[UIView animateWithDuration:0.25 animations:^{
toast.alpha = 0;
} completion:^(BOOL finished) {
[toast removeFromSuperview];
toast.alpha = 1;
}];
}
}
static void *YBIBToastKey = &YBIBToastKey;
- (void)setYbib_toast:(YBIBToastView *)ybib_toast {
objc_setAssociatedObject(self, YBIBToastKey, ybib_toast, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (YBIBToastView *)ybib_toast {
YBIBToastView *toast = objc_getAssociatedObject(self, YBIBToastKey);
if (!toast) {
toast = [YBIBToastView new];
self.ybib_toast = toast;
}
return toast;
}
@end
@interface YBIBToastView () {
YBIBToastType _type;
CAShapeLayer *_shapeLayer;
}
@property (nonatomic, strong) UILabel *textLabel;
@end
@implementation YBIBToastView
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];
self.userInteractionEnabled = NO;
self.layer.cornerRadius = 7;
[self addSubview:self.textLabel];
}
return self;
}
- (void)updateConstraints {
self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *layA = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:20];
NSLayoutConstraint *layB = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:-20];
NSLayoutConstraint *layC = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1 constant:-15];
NSLayoutConstraint *layD = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1 constant:70];
NSLayoutConstraint *layE = [NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:60];
[self addConstraints:@[layA, layB, layC, layD, layE]];
[super updateConstraints];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self startAnimation];
}
#pragma mark - animation
- (void)showWithText:(NSString *)text type:(YBIBToastType)type {
self.textLabel.text = text;
_type = type;
[self setNeedsLayout];
}
- (void)startAnimation {
if (_shapeLayer && _shapeLayer.superlayer) {
[_shapeLayer removeFromSuperlayer];
}
_shapeLayer = [CAShapeLayer layer];
_shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
_shapeLayer.fillColor = [UIColor clearColor].CGColor;
_shapeLayer.lineWidth = 5.0;
_shapeLayer.lineCap = @"round";
_shapeLayer.lineJoin = @"round";
_shapeLayer.strokeStart = 0.0;
_shapeLayer.strokeEnd = 0.0;
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
CGFloat r = 13.0;
CGFloat x = self.bounds.size.width / 2.0;
CGFloat y = 38.0;
switch (_type) {
case YBIBToastTypeHook: {
[bezierPath moveToPoint:CGPointMake(x - r - r / 2, y)];
[bezierPath addLineToPoint:CGPointMake(x - r / 2, y + r)];
[bezierPath addLineToPoint:CGPointMake(x + r * 2 - r / 2, y - r)];
}
break;
case YBIBToastTypeFork: {
[bezierPath moveToPoint:CGPointMake(x - r, y - r)];
[bezierPath addLineToPoint:CGPointMake(x + r, y + r)];
[bezierPath moveToPoint:CGPointMake(x - r, y + r)];
[bezierPath addLineToPoint:CGPointMake(x + r, y - r)];
}
break;
default:break;
}
CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
[baseAnimation setFromValue:@0.0];
[baseAnimation setToValue:@1.0];
[baseAnimation setDuration:0.3];
baseAnimation.removedOnCompletion = NO;
baseAnimation.fillMode = kCAFillModeBoth;
_shapeLayer.path = bezierPath.CGPath;
[self.layer addSublayer:_shapeLayer];
[_shapeLayer addAnimation:baseAnimation forKey:@"strokeEnd"];
}
#pragma mark - getter
- (UILabel *)textLabel {
if (!_textLabel) {
_textLabel = [UILabel new];
_textLabel.textColor = [UIColor whiteColor];
_textLabel.font = [UIFont systemFontOfSize:14];
_textLabel.textAlignment = NSTextAlignmentCenter;
_textLabel.numberOfLines = 0;
}
return _textLabel;
}
@end

View File

@@ -0,0 +1,19 @@
//
// NSObject+YBImageBrowser.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/9/26.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (YBImageBrowser)
@property (nonatomic, assign) CGFloat ybib_originAlpha;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
//
// NSObject+YBImageBrowser.m
// YBImageBrowserDemo
//
// Created by on 2019/9/26.
// Copyright © 2019 . All rights reserved.
//
#import "NSObject+YBImageBrowser.h"
#import <objc/runtime.h>
@implementation NSObject (YBImageBrowser)
static void *YBIBOriginAlphaKey = &YBIBOriginAlphaKey;
- (void)setYbib_originAlpha:(CGFloat)ybib_originAlpha {
objc_setAssociatedObject(self, YBIBOriginAlphaKey, @(ybib_originAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGFloat)ybib_originAlpha {
NSNumber *alpha = objc_getAssociatedObject(self, YBIBOriginAlphaKey);
return alpha ? alpha.floatValue : 1;
}
@end

View File

@@ -0,0 +1,46 @@
//
// YBIBAnimatedTransition.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/6.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBAnimatedTransition <NSObject>
@required
- (void)yb_showTransitioningWithContainer:(UIView *)container startView:(nullable __kindof UIView *)startView startImage:(nullable UIImage *)startImage endFrame:(CGRect)endFrame orientation:(UIDeviceOrientation)orientation completion:(void(^)(void))completion;
- (void)yb_hideTransitioningWithContainer:(UIView *)container startView:(nullable __kindof UIView *)startView endView:(UIView *)endView orientation:(UIDeviceOrientation)orientation completion:(void(^)(void))completion;
@end
typedef NS_ENUM(NSInteger, YBIBTransitionType) {
/// 无动效
YBIBTransitionTypeNone,
/// 渐隐
YBIBTransitionTypeFade,
/// 连贯移动
YBIBTransitionTypeCoherent
};
@interface YBIBAnimatedTransition : NSObject <YBIBAnimatedTransition>
/// 入场动效类型
@property (nonatomic, assign) YBIBTransitionType showType;
/// 出场动效类型
@property (nonatomic, assign) YBIBTransitionType hideType;
/// 入场动效持续时间
@property (nonatomic, assign) NSTimeInterval showDuration;
/// 出场动效持续时间
@property (nonatomic, assign) NSTimeInterval hideDuration;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,175 @@
//
// YBIBAnimatedTransition.m
// YBImageBrowserDemo
//
// Created by on 2019/6/6.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBAnimatedTransition.h"
extern CGFloat YBIBRotationAngle(UIDeviceOrientation startOrientation, UIDeviceOrientation endOrientation);
@implementation YBIBAnimatedTransition
#pragma mark - life cycle
- (instancetype)init {
self = [super init];
if (self) {
_showType = _hideType = YBIBTransitionTypeCoherent;
_showDuration = _hideDuration = 0.25;
}
return self;
}
#pragma mark - <YBIBAnimationHandler>
- (void)yb_showTransitioningWithContainer:(UIView *)container startView:(__kindof UIView *)startView startImage:(UIImage *)startImage endFrame:(CGRect)endFrame orientation:(UIDeviceOrientation)orientation completion:(void (^)(void))completion {
YBIBTransitionType type = self.showType;
if (type == YBIBTransitionTypeCoherent) {
if (CGRectIsEmpty(endFrame) || !startView || orientation != (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation) {
type = YBIBTransitionTypeFade;
}
}
switch (type) {
case YBIBTransitionTypeNone: {
completion();
}
break;
case YBIBTransitionTypeFade: {
BOOL animateValid = !CGRectIsEmpty(endFrame) && startView;
UIImageView *animateImageView;
if (animateValid) {
animateImageView = [self imageViewAssimilateToView:startView];
animateImageView.frame = endFrame;
animateImageView.image = startImage;
[container addSubview:animateImageView];
}
CGFloat rawAlpha = container.alpha;
container.alpha = 0;
if (!animateValid) completion();
[UIView animateWithDuration:self.showDuration animations:^{
container.alpha = rawAlpha;
} completion:^(BOOL finished) {
if (animateValid) {
[animateImageView removeFromSuperview];
completion();
}
}];
}
break;
case YBIBTransitionTypeCoherent: {
UIImageView *animateImageView = [self imageViewAssimilateToView:startView];
animateImageView.frame = [startView convertRect:startView.bounds toView:container];
animateImageView.image = startImage;
[container addSubview:animateImageView];
UIColor *rawBackgroundColor = container.backgroundColor;
container.backgroundColor = [rawBackgroundColor colorWithAlphaComponent:0];
[UIView animateWithDuration:self.showDuration animations:^{
animateImageView.frame = endFrame;
container.backgroundColor = rawBackgroundColor;
} completion:^(BOOL finished) {
completion();
// Disappear smoothly.
[UIView animateWithDuration:0.2 animations:^{
animateImageView.alpha = 0;
} completion:^(BOOL finished) {
[animateImageView removeFromSuperview];
}];
}];
}
break;
}
}
- (void)yb_hideTransitioningWithContainer:(UIView *)container startView:(__kindof UIView *)startView endView:(UIView *)endView orientation:(UIDeviceOrientation)orientation completion:(void (^)(void))completion {
YBIBTransitionType type = self.hideType;
if (type == YBIBTransitionTypeCoherent && (!startView || !endView)) {
type = YBIBTransitionTypeFade;
}
switch (type) {
case YBIBTransitionTypeNone: {
completion();
}
break;
case YBIBTransitionTypeFade: {
CGFloat rawAlpha = container.alpha;
[UIView animateWithDuration:self.hideDuration animations:^{
container.alpha = 0;
} completion:^(BOOL finished) {
completion();
container.alpha = rawAlpha;
}];
}
break;
case YBIBTransitionTypeCoherent: {
CGRect startFrame = startView.frame;
CGRect endFrame = [endView convertRect:endView.bounds toView:startView.superview];
UIColor *rawBackgroundColor = container.backgroundColor;
[UIView animateWithDuration:self.hideDuration animations:^{
container.backgroundColor = [rawBackgroundColor colorWithAlphaComponent:0];
startView.contentMode = endView.contentMode;
CGAffineTransform transform = startView.transform;
UIDeviceOrientation statusBarOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
if (orientation != statusBarOrientation) {
transform = CGAffineTransformRotate(transform, YBIBRotationAngle(orientation, statusBarOrientation));
}
if ([startView isKindOfClass:UIImageView.self]) {
startView.frame = endFrame;
startView.transform = transform;
} else {
CGFloat scale = MAX(endFrame.size.width / startFrame.size.width, endFrame.size.height / startFrame.size.height);
startView.center = CGPointMake(endFrame.size.width * startView.layer.anchorPoint.x + endFrame.origin.x, endFrame.size.height * startView.layer.anchorPoint.y + endFrame.origin.y);
startView.transform = CGAffineTransformScale(transform, scale, scale);
}
} completion:^(BOOL finished) {
completion();
container.backgroundColor = rawBackgroundColor;
}];
}
break;
}
}
#pragma mark - private
- (UIImageView *)imageViewAssimilateToView:(nullable __kindof UIView *)view {
UIImageView *animateImageView = [UIImageView new];
if ([view isKindOfClass:UIImageView.self]) {
animateImageView.contentMode = view.contentMode;
} else {
animateImageView.contentMode = UIViewContentModeScaleAspectFill;
}
animateImageView.layer.masksToBounds = view.layer.masksToBounds;
animateImageView.layer.cornerRadius = view.layer.cornerRadius;
animateImageView.layer.backgroundColor = view.layer.backgroundColor;
return animateImageView;
}
@end

View File

@@ -0,0 +1,25 @@
//
// YBIBCollectionView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/6.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBCollectionViewLayout.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBCollectionView : UICollectionView
@property (nonatomic, strong, readonly) YBIBCollectionViewLayout *layout;
- (NSString *)reuseIdentifierForCellClass:(Class)cellClass;
- (nullable UICollectionViewCell *)centerCell;
- (void)scrollToPage:(NSInteger)page;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,83 @@
//
// YBIBCollectionView.m
// YBImageBrowserDemo
//
// Created by on 2019/6/6.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBCollectionView.h"
@implementation YBIBCollectionView {
NSMutableSet *_reuseSet;
}
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
_layout = [YBIBCollectionViewLayout new];
return [self initWithFrame:frame collectionViewLayout:_layout];
}
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(nonnull UICollectionViewLayout *)layout {
self = [super initWithFrame:frame collectionViewLayout:layout];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.pagingEnabled = YES;
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.alwaysBounceVertical = NO;
self.alwaysBounceHorizontal = NO;
self.decelerationRate = UIScrollViewDecelerationRateFast;
if (@available(iOS 11.0, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
_reuseSet = [NSMutableSet set];
}
return self;
}
#pragma mark - public
- (NSString *)reuseIdentifierForCellClass:(Class)cellClass {
NSString *identifier = NSStringFromClass(cellClass);
if (![_reuseSet containsObject:identifier]) {
NSString *path = [[NSBundle mainBundle] pathForResource:identifier ofType:@"nib"];
if (path) {
[self registerNib:[UINib nibWithNibName:identifier bundle:nil] forCellWithReuseIdentifier:identifier];
} else {
[self registerClass:cellClass forCellWithReuseIdentifier:identifier];
}
[_reuseSet addObject:identifier];
}
return identifier;
}
- (UICollectionViewCell *)centerCell {
NSArray<UICollectionViewCell *> *cells = [self visibleCells];
if (cells.count == 0) return nil;
UICollectionViewCell *res = cells[0];
CGFloat centerX = self.contentOffset.x + (self.bounds.size.width / 2.0);
for (int i = 1; i < cells.count; ++i) {
if (ABS(cells[i].center.x - centerX) < ABS(res.center.x - centerX)) {
res = cells[i];
}
}
return res;
}
- (void)scrollToPage:(NSInteger)page {
[self setContentOffset:CGPointMake(self.bounds.size.width * page, 0)];
}
#pragma mark - hit test
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
// When the hit-test view is 'UISlider', set '_scrollEnabled' to 'NO', avoid gesture conflicts.
self.scrollEnabled = ![view isKindOfClass:UISlider.class];
return view;
}
@end

View File

@@ -0,0 +1,16 @@
//
// YBIBCollectionViewLayout.h
// YBImageBrowserDemo
//
// Created by 杨少 on 2018/4/17.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface YBIBCollectionViewLayout : UICollectionViewFlowLayout
@property (nonatomic, assign) CGFloat distanceBetweenPages;
@end

View File

@@ -0,0 +1,44 @@
//
// YBIBCollectionViewLayout.m
// YBImageBrowserDemo
//
// Created by on 2018/4/17.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBCollectionViewLayout.h"
@implementation YBIBCollectionViewLayout
- (instancetype)init {
self = [super init];
if (self) {
self.minimumLineSpacing = 0;
self.minimumInteritemSpacing = 0;
self.sectionInset = UIEdgeInsetsZero;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_distanceBetweenPages = 20;
}
return self;
}
- (void)prepareLayout {
[super prepareLayout];
self.itemSize = self.collectionView.bounds.size;
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray<UICollectionViewLayoutAttributes *> *layoutAttsArray = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES];
CGFloat halfWidth = self.collectionView.bounds.size.width / 2.0;
CGFloat centerX = self.collectionView.contentOffset.x + halfWidth;
[layoutAttsArray enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.center = CGPointMake(obj.center.x + (obj.center.x - centerX) / halfWidth * self.distanceBetweenPages / 2, obj.center.y);
}];
return layoutAttsArray;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
@end

View File

@@ -0,0 +1,17 @@
//
// YBIBContainerView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/11.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface YBIBContainerView : UIView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
//
// YBIBContainerView.m
// YBImageBrowserDemo
//
// Created by on 2019/7/11.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBContainerView.h"
@implementation YBIBContainerView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *originView = [super hitTest:point withEvent:event];
if ([originView isKindOfClass:self.class]) {
// Continue hit-testing if the view is kind of 'self.class'.
return nil;
}
return originView;
}
@end

View File

@@ -0,0 +1,31 @@
//
// YBIBDataMediator.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/6.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBImageBrowser.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBDataMediator : NSObject
- (instancetype)initWithBrowser:(YBImageBrowser *)browser;
@property (nonatomic, assign) NSUInteger dataCacheCountLimit;
- (NSInteger)numberOfCells;
- (id<YBIBDataProtocol>)dataForCellAtIndex:(NSInteger)index;
- (void)clear;
@property (nonatomic, assign) NSUInteger preloadCount;
- (void)preloadWithPage:(NSInteger)page;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,69 @@
//
// YBIBDataMediator.m
// YBImageBrowserDemo
//
// Created by on 2019/6/6.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBDataMediator.h"
#import "YBImageBrowser+Internal.h"
@implementation YBIBDataMediator {
__weak YBImageBrowser *_browser;
NSCache<NSNumber *, id<YBIBDataProtocol>> *_dataCache;
}
#pragma mark - life cycle
- (instancetype)initWithBrowser:(YBImageBrowser *)browser {
if (self = [super init]) {
_browser = browser;
_dataCache = [NSCache new];
}
return self;
}
#pragma mark - public
- (NSInteger)numberOfCells {
return _browser.dataSource ? [_browser.dataSource yb_numberOfCellsInImageBrowser:_browser] : _browser.dataSourceArray.count;
}
- (id<YBIBDataProtocol>)dataForCellAtIndex:(NSInteger)index {
if (index < 0 || index > self.numberOfCells - 1) return nil;
id<YBIBDataProtocol> data = [_dataCache objectForKey:@(index)];
if (!data) {
data = _browser.dataSource ? [_browser.dataSource yb_imageBrowser:_browser dataForCellAtIndex:index] : _browser.dataSourceArray[index];
[_dataCache setObject:data forKey:@(index)];
[_browser implementGetBaseInfoProtocol:data];
}
return data;
}
- (void)clear {
[_dataCache removeAllObjects];
}
- (void)preloadWithPage:(NSInteger)page {
if (_preloadCount == 0) return;
NSInteger left = -(_preloadCount / 2), right = _preloadCount - ABS(left);
for (NSInteger i = left; i <= right; ++i) {
if (i == 0) continue;
id<YBIBDataProtocol> targetData = [self dataForCellAtIndex:page + i];
if ([targetData respondsToSelector:@selector(yb_preload)]) {
[targetData yb_preload];
}
}
}
#pragma mark - getters & setters
- (void)setDataCacheCountLimit:(NSUInteger)dataCacheCountLimit {
_dataCacheCountLimit = dataCacheCountLimit;
_dataCache.countLimit = dataCacheCountLimit;
}
@end

View File

@@ -0,0 +1,37 @@
//
// YBIBScreenRotationHandler.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/8.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBImageBrowser.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBScreenRotationHandler : NSObject
- (instancetype)initWithBrowser:(YBImageBrowser *)browser;
- (void)startObserveStatusBarOrientation;
- (void)startObserveDeviceOrientation;
- (void)clear;
- (void)configContainerSize:(CGSize)size;
- (CGSize)containerSizeWithOrientation:(UIDeviceOrientation)orientation;
@property (nonatomic, assign, readonly, getter=isRotating) BOOL rotating;
@property (nonatomic, assign, readonly) UIDeviceOrientation currentOrientation;
@property (nonatomic, assign) UIInterfaceOrientationMask supportedOrientations;
@property (nonatomic, assign) NSTimeInterval rotationDuration;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,328 @@
//
// YBIBScreenRotationHandler.m
// YBImageBrowserDemo
//
// Created by on 2019/6/8.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBScreenRotationHandler.h"
#import "YBIBUtilities.h"
#import "YBIBCellProtocol.h"
#import "YBImageBrowser+Internal.h"
BOOL YBIBValidDeviceOrientation(UIDeviceOrientation orientation) {
static NSSet *validSet;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
validSet = [NSSet setWithObjects:@(UIDeviceOrientationPortrait), @(UIDeviceOrientationPortraitUpsideDown), @(UIDeviceOrientationLandscapeLeft), @(UIDeviceOrientationLandscapeRight), nil];
});
return [validSet containsObject:@(orientation)];
}
CGFloat YBIBRotationAngle(UIDeviceOrientation startOrientation, UIDeviceOrientation endOrientation) {
static NSDictionary<NSNumber*, NSNumber*> *angleMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
angleMap = @{@(UIDeviceOrientationPortrait):@(0), @(UIDeviceOrientationPortraitUpsideDown):@(M_PI), @(UIDeviceOrientationLandscapeLeft):@(M_PI_2), @(UIDeviceOrientationLandscapeRight): @(-M_PI_2)};
});
NSNumber *start = angleMap[@(startOrientation)], *end = angleMap[@(endOrientation)];
CGFloat res = CGFLOAT_IS_DOUBLE ? end.doubleValue - start.doubleValue : end.floatValue - start.floatValue;
if (ABS(res) > M_PI) {
return res > 0 ? res - M_PI * 2 : M_PI * 2 + res;
}
return res;
}
static NSUInteger const kMaskNull = 10000;
@interface YBIBScreenRotationHandler ()
@property (nonatomic, assign) BOOL rotating;
@property (nonatomic, assign) UIDeviceOrientation currentOrientation;
@end
@implementation YBIBScreenRotationHandler {
__weak YBImageBrowser *_browser;
CGSize _verticalContainerSize;
CGSize _horizontalContainerSize;
NSInteger _recordPage;
}
#pragma mark - life cycle
- (void)dealloc {
[self clear];
}
- (instancetype)initWithBrowser:(YBImageBrowser *)browser {
if (self = [super init]) {
_browser = browser;
_rotating = NO;
_supportedOrientations = UIInterfaceOrientationMaskAllButUpsideDown;
_rotationDuration = 0.25;
}
return self;
}
#pragma mark - public
- (void)startObserveStatusBarOrientation {
self.currentOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangedStatusBarOrientationNotification:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillChangeStatusBarOrientationNotification:) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil];
}
- (void)startObserveDeviceOrientation {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChangeNotification:) name:UIDeviceOrientationDidChangeNotification object:nil];
}
- (void)clear {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)configContainerSize:(CGSize)size {
if (UIDeviceOrientationIsLandscape(self.currentOrientation)) {
// Now is horizontal.
_verticalContainerSize = CGSizeMake(size.height, size.width);
_horizontalContainerSize = size;
} else {
// Now is vertical.
_verticalContainerSize = size;
_horizontalContainerSize = CGSizeMake(size.height, size.width);
}
}
- (CGSize)containerSizeWithOrientation:(UIDeviceOrientation)orientation {
return UIDeviceOrientationIsLandscape(orientation) ? _horizontalContainerSize : _verticalContainerSize;
}
#pragma mark - private
- (BOOL)supportedOfOrientation:(UIDeviceOrientation)orientation {
if (!YBIBValidDeviceOrientation(orientation)) return NO;
NSMutableSet *set = [NSMutableSet set];
if (_supportedOrientations & UIInterfaceOrientationMaskPortrait) [set addObject:@(UIDeviceOrientationPortrait)];
if (_supportedOrientations & UIInterfaceOrientationMaskPortraitUpsideDown) [set addObject:@(UIDeviceOrientationPortraitUpsideDown)];
if (_supportedOrientations & UIInterfaceOrientationMaskLandscapeRight) [set addObject:@(UIDeviceOrientationLandscapeLeft)];
if (_supportedOrientations & UIInterfaceOrientationMaskLandscapeLeft) [set addObject:@(UIDeviceOrientationLandscapeRight)];
return [set containsObject:@(orientation)];
}
- (BOOL)supportedOnlyOneSystemOrientation {
UIInterfaceOrientationMask mask = [self supportSystemOrientationMask];
return mask == (mask & (-mask));
}
- (void)orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
if ([centerCell respondsToSelector:@selector(yb_orientationWillChangeWithExpectOrientation:)]) {
[centerCell yb_orientationWillChangeWithExpectOrientation:orientation];
}
for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_orientationWillChangeWithExpectOrientation:)]) {
[handler yb_orientationWillChangeWithExpectOrientation:orientation];
}
}
}
- (void)orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
if ([centerCell respondsToSelector:@selector(yb_orientationChangeAnimationWithExpectOrientation:)]) {
[centerCell yb_orientationChangeAnimationWithExpectOrientation:orientation];
[centerCell layoutIfNeeded]; // Compatible with autolayout.
}
for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_orientationChangeAnimationWithExpectOrientation:)]) {
[handler yb_orientationChangeAnimationWithExpectOrientation:orientation];
}
}
}
- (void)orientationDidChangedWithOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
if ([centerCell respondsToSelector:@selector(yb_orientationDidChangedWithOrientation:)]) {
[centerCell yb_orientationDidChangedWithOrientation:orientation];
}
for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_orientationDidChangedWithOrientation:)]) {
[handler yb_orientationDidChangedWithOrientation:orientation];
}
}
}
#pragma mark - event
- (void)deviceOrientationDidChangeNotification:(NSNotification *)note {
if (![self supportedOnlyOneSystemOrientation]) return;
if (_browser.isTransitioning || self.rotating) return;
UIDeviceOrientation expectOrientation = [UIDevice currentDevice].orientation;
if (expectOrientation == self.currentOrientation || ![self supportedOfOrientation:expectOrientation]) return;
self.rotating = YES;
// Align.
[_browser.collectionView scrollToPage:_browser.currentPage];
// Record current page number before transforming.
NSInteger currentPage = _browser.currentPage;
UIDeviceOrientation statusBarOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
CGFloat angleStatusBarToExpect = YBIBRotationAngle(statusBarOrientation, expectOrientation);
CGFloat angleCurrentToExpect = YBIBRotationAngle(_currentOrientation, expectOrientation);
CGRect expectBounds = (CGRect){CGPointZero, [self containerSizeWithOrientation:expectOrientation]};
UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
// Animate smoothly if bigger rotation angle.
NSTimeInterval duration = self.rotationDuration * (ABS(angleCurrentToExpect) > M_PI_2 ? 2 : 1);
// 'collectionView' transformation.
self->_browser.collectionView.bounds = expectBounds;
self->_browser.collectionView.transform = CGAffineTransformMakeRotation(angleStatusBarToExpect);
centerCell.contentView.transform = CGAffineTransformMakeRotation(-angleCurrentToExpect);
// Reset to prevent the page number change after transforming.
[self->_browser.collectionView scrollToPage:currentPage];
[self orientationWillChangeWithExpectOrientation:expectOrientation centerCell:centerCell];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
// Maybe the internal UI need to transform.
[self orientationChangeAnimationWithExpectOrientation:expectOrientation centerCell:centerCell];
centerCell.contentView.bounds = expectBounds;
centerCell.contentView.transform = CGAffineTransformIdentity;
self->_browser.containerView.bounds = expectBounds;
self->_browser.containerView.transform = CGAffineTransformMakeRotation(angleStatusBarToExpect);
} completion:^(BOOL finished) {
self.currentOrientation = expectOrientation;
self.rotating = NO;
[self orientationDidChangedWithOrientation:expectOrientation centerCell:centerCell];
}];
}
- (void)applicationWillChangeStatusBarOrientationNotification:(NSNotification *)noti {
if ([self supportedOnlyOneSystemOrientation]) return;
self.rotating = YES;
// Record current page number before transforming.
_recordPage = _browser.currentPage;
UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
UIDeviceOrientation expectOrientation = ((NSNumber *)noti.userInfo[UIApplicationStatusBarOrientationUserInfoKey]).integerValue;
[self orientationWillChangeWithExpectOrientation:expectOrientation centerCell:centerCell];
}
- (void)applicationDidChangedStatusBarOrientationNotification:(NSNotification *)noti {
if ([self supportedOnlyOneSystemOrientation]) return;
UIDeviceOrientation expectOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
[self orientationChangeAnimationWithExpectOrientation:expectOrientation centerCell:centerCell];
CGRect expectBounds = (CGRect){CGPointZero, [self containerSizeWithOrientation:expectOrientation]};
self->_browser.collectionView.layout.itemSize = expectBounds.size;
// Reset to prevent the page number change after transforming.
[_browser.collectionView scrollToPage:_recordPage];
self.currentOrientation = expectOrientation;
self.rotating = NO;
[self orientationDidChangedWithOrientation:expectOrientation centerCell:centerCell];
}
#pragma mark - getters & setters
- (void)setRotating:(BOOL)rotating {
_rotating = rotating;
_browser.containerView.userInteractionEnabled = !rotating;
_browser.collectionView.userInteractionEnabled = !rotating;
_browser.collectionView.panGestureRecognizer.enabled = !rotating;
}
#pragma mark - calculate supported orientation of system
- (UIInterfaceOrientationMask)supportSystemOrientationMask {
UIInterfaceOrientationMask limitMask = 0;
// IphoneX series do not support UIInterfaceOrientationMaskPortraitUpsideDown, except selector '-application:supportedInterfaceOrientationsForWindow:' of '[UIApplication sharedApplication].delegate' return 0. Maybe it is BUG of Apple.
BOOL ignoreUpsideDownIfIphoneX = YES;
UIInterfaceOrientationMask delegateMask = [self maskOfApplicationDelegate];
if (delegateMask != kMaskNull) {
if (delegateMask == 0) {
// Apple do.
limitMask = UIInterfaceOrientationMaskAll;
ignoreUpsideDownIfIphoneX = NO;
} else {
limitMask = delegateMask;
}
} else {
// Lower priority.
limitMask = [self maskOfInfoPlist];
}
UIInterfaceOrientationMask supportMask = limitMask & [self maskOfViewController];
if (ignoreUpsideDownIfIphoneX && YBIBIsIphoneXSeries() && (supportMask & UIInterfaceOrientationMaskPortraitUpsideDown)) {
supportMask ^= UIInterfaceOrientationMaskPortraitUpsideDown;
}
return supportMask;
}
- (UIInterfaceOrientationMask)maskOfInfoPlist {
// 'Info.plist' will not change in a process.
static UIInterfaceOrientationMask mask = 0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:path];
NSArray *array = dict[@"UISupportedInterfaceOrientations"];
NSSet *set = [NSSet setWithArray:array];
if ([set containsObject:@"UIInterfaceOrientationPortrait"]) mask |= UIInterfaceOrientationMaskPortrait;
if ([set containsObject:@"UIInterfaceOrientationLandscapeRight"]) mask |= UIInterfaceOrientationMaskLandscapeRight;
if ([set containsObject:@"UIInterfaceOrientationLandscapeLeft"]) mask |= UIInterfaceOrientationMaskLandscapeLeft;
if ([set containsObject:@"UIInterfaceOrientationPortraitUpsideDown"]) mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
});
return mask == 0 ? kMaskNull : mask;
}
- (UIInterfaceOrientationMask)maskOfApplicationDelegate {
UIInterfaceOrientationMask mask = kMaskNull;
id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
if ([delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) {
mask = [delegate application:[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:_browser.window];
}
return mask;
}
- (UIInterfaceOrientationMask)maskOfViewController {
UIInterfaceOrientationMask mask = kMaskNull;
// Find the UIViewController whitch 'browser' followed.
UIViewController *target = nil;
id next = _browser;
while (next) {
if ([next isKindOfClass:UIViewController.self]) {
target = next;
break;
}
if ([next isKindOfClass:UIWindow.self]) {
target = YBIBTopControllerByWindow(next);
break;
}
next = [next nextResponder];
}
// Cover directly.
if (target.tabBarController) {
mask = target.tabBarController.shouldAutorotate ? target.tabBarController.supportedInterfaceOrientations : 0;
} else if (target.navigationController) {
mask = target.navigationController.shouldAutorotate ? target.navigationController.supportedInterfaceOrientations : 0;
} else {
mask = target.shouldAutorotate ? target.supportedInterfaceOrientations : 0;
}
return mask;
}
@end

View File

@@ -0,0 +1,24 @@
//
// YBImageBrowser+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/1.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBImageBrowser.h"
#import "YBIBContainerView.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBImageBrowser ()
@property (nonatomic, strong) YBIBContainerView *containerView;
- (void)implementGetBaseInfoProtocol:(id<YBIBGetBaseInfoProtocol>)obj;
@property (nonatomic, weak, nullable) NSObject *hiddenProjectiveView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,57 @@
//
// YBIBCopywriter.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/9/13.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, YBIBCopywriterType) {
/// 简体中文
YBIBCopywriterTypeSimplifiedChinese,
/// 英文
YBIBCopywriterTypeEnglish
};
/**
文案管理类
*/
@interface YBIBCopywriter : NSObject
/**
唯一有效单例
*/
+ (instancetype)sharedCopywriter;
/// 语言类型
@property (nonatomic, assign) YBIBCopywriterType type;
#pragma - 以下文案可更改
@property (nonatomic, copy) NSString *videoIsInvalid;
@property (nonatomic, copy) NSString *videoError;
@property (nonatomic, copy) NSString *unableToSave;
@property (nonatomic, copy) NSString *imageIsInvalid;
@property (nonatomic, copy) NSString *downloadFailed;
@property (nonatomic, copy) NSString *getPhotoAlbumAuthorizationFailed;
@property (nonatomic, copy) NSString *saveToPhotoAlbumSuccess;
@property (nonatomic, copy) NSString *saveToPhotoAlbumFailed;
@property (nonatomic, copy) NSString *saveToPhotoAlbum;
@property (nonatomic, copy) NSString *cancel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,65 @@
//
// YBIBCopywriter.m
// YBImageBrowserDemo
//
// Created by on 2018/9/13.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBCopywriter.h"
@implementation YBIBCopywriter
#pragma mark - life cycle
+ (instancetype)sharedCopywriter {
static YBIBCopywriter *copywriter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
copywriter = [YBIBCopywriter new];
});
return copywriter;
}
- (instancetype)init {
self = [super init];
if (self) {
_type = YBIBCopywriterTypeSimplifiedChinese;
NSArray *appleLanguages = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
if (appleLanguages && appleLanguages.count > 0) {
NSString *languages = appleLanguages[0];
if (![languages hasPrefix:@"zh-Hans"]) {
_type = YBIBCopywriterTypeEnglish;
}
}
[self initCopy];
}
return self;
}
#pragma mark - private
- (void)initCopy {
BOOL en = self.type == YBIBCopywriterTypeEnglish;
self.videoIsInvalid = en ? @"Video is invalid" : @"视频无效";
self.videoError = en ? @"Video error" : @"视频错误";
self.unableToSave = en ? @"Unable to save" : @"无法保存";
self.imageIsInvalid = en ? @"Image is invalid" : @"图片无效";
self.downloadFailed = en ? @"Download failed" : @"加载图片失败";
self.getPhotoAlbumAuthorizationFailed = en ? @"Failed to get album authorization" : @"获取相册权限失败";
self.saveToPhotoAlbumSuccess = en ? @"Save successful" : @"已保存到系统相册";
self.saveToPhotoAlbumFailed = en ? @"Save failed" : @"保存失败";
self.saveToPhotoAlbum = en ? @"Save" : @"保存到相册";
self.cancel = en ? @"Cancel" : @"取消";
}
#pragma mark - public
- (void)setType:(YBIBCopywriterType)type {
_type = type;
[self initCopy];
}
@end

View File

@@ -0,0 +1,63 @@
//
// YBIBIconManager.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/8/29.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIImage (YBImageBrowser)
/**
获取图片便利构造方法
@param name 图片名字
@param bundle 资源对象
@return 图片实例
*/
+ (instancetype)ybib_imageNamed:(NSString *)name bundle:(NSBundle *)bundle;
@end
/// 获取图片闭包
typedef UIImage * _Nullable (^YBIBIconBlock)(void);
/**
图标管理类
*/
@interface YBIBIconManager : NSObject
/**
唯一有效单例
*/
+ (instancetype)sharedManager;
#pragma - 以下图片可更改
/// 基本-加载
@property (nonatomic, copy) YBIBIconBlock loadingImage;
/// 工具视图-保存
@property (nonatomic, copy) YBIBIconBlock toolSaveImage;
/// 工具视图-更多
@property (nonatomic, copy) YBIBIconBlock toolMoreImage;
/// 视频-播放
@property (nonatomic, copy) YBIBIconBlock videoPlayImage;
/// 视频-暂停
@property (nonatomic, copy) YBIBIconBlock videoPauseImage;
/// 视频-取消
@property (nonatomic, copy) YBIBIconBlock videoCancelImage;
/// 视频-播放大图
@property (nonatomic, copy) YBIBIconBlock videoBigPlayImage;
/// 视频-拖动圆点
@property (nonatomic, copy) YBIBIconBlock videoDragCircleImage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,138 @@
//
// YBIBIconManager.m
// YBImageBrowserDemo
//
// Created by on 2018/8/29.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBIconManager.h"
// The best order for path scale search.
static NSArray *_NSBundlePreferredScales() {
static NSArray *scales;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale <= 1) {
scales = @[@1,@2,@3];
} else if (screenScale <= 2) {
scales = @[@2,@3,@1];
} else {
scales = @[@3,@2,@1];
}
});
return scales;
}
// Add scale modifier to the file name (without path extension), from @"name" to @"name@2x".
static NSString *_NSStringByAppendingNameScale(NSString *string, CGFloat scale) {
if (!string) return nil;
if (fabs(scale - 1) <= __FLT_EPSILON__ || string.length == 0 || [string hasSuffix:@"/"]) return string.copy;
return [string stringByAppendingFormat:@"@%@x", @(scale)];
}
@implementation UIImage (YBImageBrowser)
+ (instancetype)ybib_imageNamed:(NSString *)name bundle:(NSBundle *)bundle {
if (name.length == 0) return nil;
if ([name hasSuffix:@"/"]) return nil;
NSString *res = name.stringByDeletingPathExtension;
NSString *ext = name.pathExtension;
NSString *path = nil;
CGFloat scale = 1;
// If no extension, guess by system supported (same as UIImage).
NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
NSArray *scales = _NSBundlePreferredScales();
for (int s = 0; s < scales.count; s++) {
scale = ((NSNumber *)scales[s]).floatValue;
NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
for (NSString *e in exts) {
path = [bundle pathForResource:scaledName ofType:e];
if (path) break;
}
if (path) break;
}
if (path.length == 0) {
// Assets.xcassets supported.
return [self imageNamed:name];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) return nil;
return [[self alloc] initWithData:data scale:scale];
}
@end
NSBundle *YBIBDefaultBundle(void) {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *_bundle = [NSBundle bundleForClass:NSClassFromString(@"YBImageBrowser")];
NSString *path = [_bundle pathForResource:@"YBImageBrowser" ofType:@"bundle"];
bundle = [NSBundle bundleWithPath:path];
});
return bundle;
}
NSBundle *YBIBVideoBundle(void) {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *_bundle = [NSBundle bundleForClass:NSClassFromString(@"YBImageBrowser")];
NSString *path = [_bundle pathForResource:@"YBImageBrowserVideo" ofType:@"bundle"];
bundle = [NSBundle bundleWithPath:path];
});
return bundle;
}
@implementation YBIBIconManager
+ (instancetype)sharedManager {
static YBIBIconManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [YBIBIconManager new];
});
return manager;
}
- (instancetype)init {
self = [super init];
if (self) {
_loadingImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_loading" bundle:YBIBDefaultBundle()];
};
_toolSaveImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_save" bundle:YBIBDefaultBundle()];
};
_toolMoreImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_more" bundle:YBIBDefaultBundle()];
};
_videoPlayImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_play" bundle:YBIBVideoBundle()];
};
_videoPauseImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_pause" bundle:YBIBVideoBundle()];
};
_videoCancelImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_cancel" bundle:YBIBVideoBundle()];
};
_videoBigPlayImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_bigPlay" bundle:YBIBVideoBundle()];
};
_videoDragCircleImage = ^UIImage * _Nullable{
return [UIImage ybib_imageNamed:@"ybib_circlePoint" bundle:YBIBVideoBundle()];
};
}
return self;
}
@end

View File

@@ -0,0 +1,32 @@
//
// YBIBPhotoAlbumManager.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/8/28.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <Photos/Photos.h>
NS_ASSUME_NONNULL_BEGIN
@interface YBIBPhotoAlbumManager : NSObject
/**
Get photo album authorization.
*/
+ (void)getPhotoAlbumAuthorizationSuccess:(void(^)(void))success failed:(void(^)(void))failed;
/**
Get 'AVAsset' by 'PHAsset' asynchronously, callback is in child thread.
*/
+ (void)getAVAssetWithPHAsset:(PHAsset *)phAsset completion:(void(^)(AVAsset * _Nullable asset))completion;
/**
Get 'ImageData' by 'PHAsset' synchronously, callback is in child thread.
*/
+ (void)getImageDataWithPHAsset:(PHAsset *)phAsset completion:(void(^)(NSData * _Nullable data))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,65 @@
//
// YBIBPhotoAlbumManager.m
// YBImageBrowserDemo
//
// Created by on 2018/8/28.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBPhotoAlbumManager.h"
#import "YBIBUtilities.h"
@implementation YBIBPhotoAlbumManager
+ (void)getAVAssetWithPHAsset:(PHAsset *)phAsset completion:(nonnull void (^)(AVAsset * _Nullable))completion {
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
options.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestAVAssetForVideo:phAsset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
completion(asset);
}];
}
+ (void)getImageDataWithPHAsset:(PHAsset *)phAsset completion:(nonnull void (^)(NSData * _Nullable))completion {
PHImageRequestOptions *options = [PHImageRequestOptions new];
options.networkAccessAllowed = YES;
options.resizeMode = PHImageRequestOptionsResizeModeNone;
// Only when this property is YES, the callback will in child thread.
options.synchronous = YES;
[[PHImageManager defaultManager] requestImageDataForAsset:phAsset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
BOOL complete = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (complete && imageData) {
completion(imageData);
} else {
completion(nil);
}
}];
}
+ (void)getPhotoAlbumAuthorizationSuccess:(void(^)(void))success failed:(void(^)(void))failed {
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
switch (status) {
case PHAuthorizationStatusDenied:
if (failed) failed();
break;
case PHAuthorizationStatusRestricted:
if (failed) failed();
break;
case PHAuthorizationStatusNotDetermined: {
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status){
YBIB_DISPATCH_ASYNC_MAIN(^{
if (status == PHAuthorizationStatusAuthorized) {
if (success) success();
} else {
if (failed) failed();
}
})
}];
}
break;
case PHAuthorizationStatusAuthorized:
if (success) success();
break;
}
}
@end

View File

@@ -0,0 +1,29 @@
//
// YBIBSentinel.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/18.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
Thread safe.
*/
@interface YBIBSentinel : NSObject
/// Returns the current value of the counter.
@property (readonly) int32_t value;
/**
Increase the value atomically.
@return The new value.
*/
- (int32_t)increase;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,24 @@
//
// YBIBSentinel.m
// YBImageBrowserDemo
//
// Created by on 2019/6/18.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBSentinel.h"
#import <libkern/OSAtomic.h>
@implementation YBIBSentinel {
int32_t _value;
}
- (int32_t)value {
return _value;
}
- (int32_t)increase {
return OSAtomicIncrement32(&_value);
}
@end

View File

@@ -0,0 +1,52 @@
//
// YBIBUtilities.h
// YBImageBrowserDemo
//
// Created by 杨少 on 2018/4/11.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#define YBIB_DISPATCH_ASYNC(queue, block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(queue)) == 0) {\
block();\
} else {\
dispatch_async(queue, block);\
}
#define YBIB_DISPATCH_ASYNC_MAIN(block) YBIB_DISPATCH_ASYNC(dispatch_get_main_queue(), block)
#define YBIB_CODE_EXEC_TIME(KEY, ...) \
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); \
__VA_ARGS__ \
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime); \
NSLog(@"%@ Time-Consuming: %fms", KEY, linkTime * 1000.0);
UIWindow * _Nonnull YBIBNormalWindow(void);
UIViewController * _Nullable YBIBTopController(void);
UIViewController * _Nullable YBIBTopControllerByWindow(UIWindow *);
BOOL YBIBLowMemory(void);
BOOL YBIBIsIphoneXSeries(void);
CGFloat YBIBStatusbarHeight(void);
CGFloat YBIBSafeAreaBottomHeight(void);
UIImage *YBIBSnapshotView(UIView *);
/// This is orientation of 'YBImageBrowser' not 'UIDevice'.
UIEdgeInsets YBIBPaddingByBrowserOrientation(UIDeviceOrientation);
@interface YBIBUtilities : NSObject
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,134 @@
//
// YBIBUtilities.m
// YBImageBrowserDemo
//
// Created by on 2018/4/11.
// Copyright © 2018 . All rights reserved.
//
#import "YBIBUtilities.h"
#import <sys/utsname.h>
UIWindow * _Nullable YBIBNormalWindow(void) {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
if (window.windowLevel != UIWindowLevelNormal) {
NSArray *windows = [[UIApplication sharedApplication] windows];
for(UIWindow *temp in windows) {
if (temp.windowLevel == UIWindowLevelNormal) {
return temp;
}
}
}
return window;
}
UIViewController * _Nullable YBIBTopController(void) {
return YBIBTopControllerByWindow(YBIBNormalWindow());
}
UIViewController * _Nullable YBIBTopControllerByWindow(UIWindow *window) {
if (!window) return nil;
UIViewController *top = nil;
id nextResponder;
if (window.subviews.count > 0) {
UIView *frontView = [window.subviews objectAtIndex:0];
nextResponder = frontView.nextResponder;
}
if (nextResponder && [nextResponder isKindOfClass:UIViewController.class]) {
top = nextResponder;
} else {
top = window.rootViewController;
}
while ([top isKindOfClass:UITabBarController.class] || [top isKindOfClass:UINavigationController.class] || top.presentedViewController) {
if ([top isKindOfClass:UITabBarController.class]) {
top = ((UITabBarController *)top).selectedViewController;
} else if ([top isKindOfClass:UINavigationController.class]) {
top = ((UINavigationController *)top).topViewController;
} else if (top.presentedViewController) {
top = top.presentedViewController;
}
}
return top;
}
BOOL YBIBLowMemory(void) {
static BOOL lowMemory = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
unsigned long long physicalMemory = [[NSProcessInfo processInfo] physicalMemory];
lowMemory = physicalMemory > 0 && physicalMemory < 1024 * 1024 * 1500;
});
return lowMemory;
}
BOOL YBIBIsIphoneXSeries(void) {
return YBIBStatusbarHeight() > 20;
}
CGFloat YBIBStatusbarHeight(void) {
CGFloat height = 0;
if (@available(iOS 11.0, *)) {
height = UIApplication.sharedApplication.delegate.window.safeAreaInsets.top;
}
if (height <= 0) {
height = UIApplication.sharedApplication.statusBarFrame.size.height;
}
if (height <= 0) {
height = 20;
}
return height;
}
CGFloat YBIBSafeAreaBottomHeight(void) {
CGFloat bottom = 0;
if (@available(iOS 11.0, *)) {
bottom = UIApplication.sharedApplication.delegate.window.safeAreaInsets.bottom;
}
return bottom;
}
UIImage *YBIBSnapshotView(UIView *view) {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, [UIScreen mainScreen].scale);
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
UIEdgeInsets YBIBPaddingByBrowserOrientation(UIDeviceOrientation orientation) {
UIEdgeInsets padding = UIEdgeInsetsZero;
if (!YBIBIsIphoneXSeries()) return padding;
UIDeviceOrientation barOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
if (UIDeviceOrientationIsLandscape(orientation)) {
BOOL same = orientation == barOrientation;
BOOL reverse = !same && UIDeviceOrientationIsLandscape(barOrientation);
if (same) {
padding.bottom = YBIBSafeAreaBottomHeight();
padding.top = 0;
} else if (reverse) {
padding.top = YBIBSafeAreaBottomHeight();
padding.bottom = 0;
}
padding.left = padding.right = MAX(YBIBSafeAreaBottomHeight(), YBIBStatusbarHeight());
} else {
if (orientation == UIDeviceOrientationPortrait) {
padding.top = YBIBStatusbarHeight();
padding.bottom = barOrientation == UIDeviceOrientationPortrait ? YBIBSafeAreaBottomHeight() : 0;
} else {
padding.bottom = YBIBStatusbarHeight();
padding.top = barOrientation == UIDeviceOrientationPortrait ? YBIBSafeAreaBottomHeight() : 0;
}
padding.left = padding.right = UIDeviceOrientationIsLandscape(barOrientation) ? YBIBSafeAreaBottomHeight() : 0 ;
}
return padding;
}
@implementation YBIBUtilities
@end

View File

@@ -0,0 +1,33 @@
//
// YBIBImageCache+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/13.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBImageCache.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, YBIBImageCacheType) {
YBIBImageCacheTypeOrigin,
YBIBImageCacheTypeCompressed
};
/**
Not thread safe.
*/
@interface YBIBImageCache ()
- (void)setImage:(UIImage *)image type:(YBIBImageCacheType)type forKey:(NSString *)key resident:(BOOL)resident;
- (nullable UIImage *)imageForKey:(NSString *)key type:(YBIBImageCacheType)type;
- (void)removeForKey:(NSString *)key;
- (void)removeResidentForKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,30 @@
//
// YBIBImageCache.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/13.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class YBIBImageCache;
@interface NSObject (YBIBImageCache)
/// 图片浏览器持有的图片缓存管理类
@property (nonatomic, strong, readonly) YBIBImageCache *ybib_imageCache;
@end
@interface YBIBImageCache : NSObject
/// 缓存数量限制(一个单位表示一个 YBIBImageData 产生的所有图片数据)
@property (nonatomic, assign) NSUInteger imageCacheCountLimit;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,112 @@
//
// YBIBImageCache.m
// YBImageBrowserDemo
//
// Created by on 2019/6/13.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBImageCache.h"
#import "YBIBImageCache+Internal.h"
#import "YBIBUtilities.h"
#import <objc/runtime.h>
@implementation NSObject (YBIBImageCache)
static void *YBIBImageCacheKey = &YBIBImageCacheKey;
- (void)setYbib_imageCache:(YBIBImageCache *)ybib_imageCache {
objc_setAssociatedObject(self, YBIBImageCacheKey, ybib_imageCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (YBIBImageCache *)ybib_imageCache {
YBIBImageCache *cache = objc_getAssociatedObject(self, YBIBImageCacheKey);
if (!cache) {
cache = [YBIBImageCache new];
self.ybib_imageCache = cache;
}
return cache;
}
@end
@interface YBIBImageCachePack : NSObject
@property (nonatomic, strong) UIImage *originImage;
@property (nonatomic, strong) UIImage *compressedImage;
@end
@implementation YBIBImageCachePack
@end
@implementation YBIBImageCache {
NSCache<NSString *, YBIBImageCachePack *> *_imageCache;
NSMutableDictionary<NSString *, YBIBImageCachePack *> *_residentCache;
}
#pragma mark - life cycle
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- (instancetype)init {
self = [super init];
if (self) {
_imageCache = [NSCache new];
_imageCache.countLimit = _imageCacheCountLimit = YBIBLowMemory() ? 6 : 12;
_residentCache = [NSMutableDictionary dictionary];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
#pragma mark - event
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
[_imageCache removeAllObjects];
[_residentCache removeAllObjects];
}
#pragma mark - public
- (void)setImage:(UIImage *)image type:(YBIBImageCacheType)type forKey:(NSString *)key resident:(BOOL)resident {
YBIBImageCachePack *pack = [_imageCache objectForKey:key];
if (!pack) {
pack = [YBIBImageCachePack new];
[_imageCache setObject:pack forKey:key];
}
switch (type) {
case YBIBImageCacheTypeOrigin:
pack.originImage = image;
break;
case YBIBImageCacheTypeCompressed:
pack.compressedImage = image;
break;
}
[_residentCache setObject:pack forKey:key];
}
- (UIImage *)imageForKey:(NSString *)key type:(YBIBImageCacheType)type {
YBIBImageCachePack *pack = [_imageCache objectForKey:key] ?: [_residentCache objectForKey:key];
switch (type) {
case YBIBImageCacheTypeOrigin: return pack.originImage;
case YBIBImageCacheTypeCompressed: return pack.compressedImage;
default: return nil;
}
}
- (void)removeForKey:(NSString *)key {
[_imageCache removeObjectForKey:key];
[_residentCache removeObjectForKey:key];
}
- (void)removeResidentForKey:(NSString *)key {
[_residentCache removeObjectForKey:key];
}
#pragma mark - setter
- (void)setImageCacheCountLimit:(NSUInteger)imageCacheCountLimit {
_imageCacheCountLimit = imageCacheCountLimit;
_imageCache.countLimit = imageCacheCountLimit;
}
@end

View File

@@ -0,0 +1,22 @@
//
// YBIBImageCell+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/12/23.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBImageCell.h"
#import "YBIBImageScrollView.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBImageCell ()
@property (nonatomic, strong) YBIBImageScrollView *imageScrollView;
@property (nonatomic, strong) UIImageView *tailoringImageView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,17 @@
//
// YBIBImageCell.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/5.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBCellProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBImageCell : UICollectionViewCell <YBIBCellProtocol>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,522 @@
//
// YBIBImageCell.m
// YBImageBrowserDemo
//
// Created by on 2019/6/5.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBImageCell.h"
#import "YBIBImageData.h"
#import "YBIBIconManager.h"
#import "YBIBImageCell+Internal.h"
#import "YBIBImageData+Internal.h"
#import "YBIBCopywriter.h"
#import "YBIBUtilities.h"
@interface YBIBImageCell () <YBIBImageDataDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
@end
@implementation YBIBImageCell {
CGPoint _interactStartPoint;
BOOL _interacting;
}
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initValue];
[self.contentView addSubview:self.imageScrollView];
[self addGesture];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.imageScrollView.frame = self.bounds;
}
- (void)initValue {
_interactStartPoint = CGPointZero;
_interacting = NO;
}
- (void)prepareForReuse {
((YBIBImageData *)self.yb_cellData).delegate = nil;
[self.imageScrollView reset];
[self hideTailoringImageView];
[self hideAuxiliaryView];
[super prepareForReuse];
}
#pragma mark - <YBIBCellProtocol>
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_backView = _yb_backView;
@synthesize yb_collectionView = _yb_collectionView;
@synthesize yb_isTransitioning = _yb_isTransitioning;
@synthesize yb_isRotating = _yb_isRotating;
@synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
@synthesize yb_hideStatusBar = _yb_hideStatusBar;
@synthesize yb_hideBrowser = _yb_hideBrowser;
@synthesize yb_hideToolViews = _yb_hideToolViews;
@synthesize yb_cellData = _yb_cellData;
@synthesize yb_cellIsInCenter = _yb_cellIsInCenter;
@synthesize yb_selfPage = _yb_selfPage;
@synthesize yb_currentPage = _yb_currentPage;
- (void)setYb_cellData:(id<YBIBDataProtocol>)yb_cellData {
_yb_cellData = yb_cellData;
((YBIBImageData *)yb_cellData).delegate = self;
}
- (UIView *)yb_foregroundView {
return self.imageScrollView.imageView;
}
- (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation {
[self hideTailoringImageView];
}
- (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation {
[self updateImageLayoutWithOrientation:orientation previousImageSize:self.imageScrollView.imageView.image.size];
}
#pragma mark - private
- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize imageViewFrame:(CGRect)imageViewFrame {
return CGSizeMake(MAX(containerSize.width, imageViewFrame.size.width), MAX(containerSize.height, imageViewFrame.size.height));
}
- (void)updateImageLayoutWithOrientation:(UIDeviceOrientation)orientation previousImageSize:(CGSize)previousImageSize {
if (_interacting) [self restoreInteractionWithDuration:0];
YBIBImageData *data = self.yb_cellData;
CGSize imageSize;
UIImage *image = self.imageScrollView.imageView.image;
YBIBScrollImageType imageType = self.imageScrollView.imageType;
if (imageType == YBIBScrollImageTypeCompressed) {
imageSize = data.originImage ? data.originImage.size : image.size;
} else {
imageSize = image.size;
}
CGSize containerSize = self.yb_containerSize(orientation);
CGRect imageViewFrame = [data.layout yb_imageViewFrameWithContainerSize:containerSize imageSize:imageSize orientation:orientation];
CGSize contentSize = [self contentSizeWithContainerSize:containerSize imageViewFrame:imageViewFrame];
CGFloat maxZoomScale = imageType == YBIBScrollImageTypeThumb ? 1 : [data.layout yb_maximumZoomScaleWithContainerSize:containerSize imageSize:imageSize orientation:orientation];
// 'zoomScale' must set before 'contentSize' and 'imageView.frame'.
self.imageScrollView.zoomScale = 1;
self.imageScrollView.contentSize = contentSize;
self.imageScrollView.minimumZoomScale = 1;
self.imageScrollView.maximumZoomScale = maxZoomScale;
CGFloat scale;
if (previousImageSize.width > 0 && previousImageSize.height > 0) {
scale = imageSize.width / imageSize.height - previousImageSize.width / previousImageSize.height;
} else {
scale = 0;
}
// '0.001' is admissible error.
if (ABS(scale) <= 0.001) {
self.imageScrollView.imageView.frame = imageViewFrame;
} else {
[UIView animateWithDuration:0.25 animations:^{
self.imageScrollView.imageView.frame = imageViewFrame;
}];
}
}
- (void)cuttingImage {
// This method has been delayed called, so 'browser' may be in transit now.
if (self.yb_isTransitioning()) return;
if (_interacting) return;
YBIBImageData *data = self.yb_cellData;
if (!data.originImage) return;
if (self.imageScrollView.zoomScale < data.cuttingZoomScale) return;
if ([data shouldCompress]) {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cuttingImage_) object:nil];
[self performSelector:@selector(cuttingImage_) withObject:nil afterDelay:0.15];
}
}
- (void)cuttingImage_ {
YBIBImageData *data = self.yb_cellData;
if (!data.originImage) return;
CGFloat scale = data.originImage.size.width / self.imageScrollView.contentSize.width;
CGFloat x = self.imageScrollView.contentOffset.x * scale,
y = self.imageScrollView.contentOffset.y * scale,
width = self.imageScrollView.bounds.size.width * scale,
height = self.imageScrollView.bounds.size.height * scale;
__weak typeof(self) wSelf = self;
[data cuttingImageToRect:CGRectMake(x, y, width, height) complete:^(UIImage *image) {
if (!image) return;
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
if (data == self.yb_cellData && !self.imageScrollView.isDragging && !self->_interacting && !self.yb_isTransitioning()) {
[self showTailoringImageView:image];
}
})
}];
}
- (void)showTailoringImageView:(UIImage *)image {
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
if (!self.tailoringImageView.superview) {
[self.contentView addSubview:self.tailoringImageView];
}
self.tailoringImageView.frame = CGRectMake(0, 0, containerSize.width, containerSize.height);
self.tailoringImageView.hidden = NO;
self.tailoringImageView.image = image;
}
- (void)hideTailoringImageView {
// Don't use 'getter' method, because it's according to the need to load.
if (_tailoringImageView) {
self.tailoringImageView.hidden = YES;
}
}
- (void)hideAuxiliaryView {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_hideToastWithContainer:self];
}
- (void)hideBrowser {
((YBIBImageData *)self.yb_cellData).delegate = nil;
[self hideTailoringImageView];
[self hideAuxiliaryView];
self.yb_hideBrowser();
_interacting = NO;
}
#pragma mark - <YBIBImageDataDelegate>
- (void)yb_imageData:(YBIBImageData *)data startLoadingWithStatus:(YBIBImageLoadingStatus)status {
switch (status) {
case YBIBImageLoadingStatusDecoding: {
if (!self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
break;
case YBIBImageLoadingStatusProcessing: {
if (!self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
break;
case YBIBImageLoadingStatusCompressing: {
if (!self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
break;
case YBIBImageLoadingStatusReadingPHAsset: {
if (!self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
break;
case YBIBImageLoadingStatusNone: {
[self hideAuxiliaryView];
}
break;
default:
break;
}
}
- (void)yb_imageData:(YBIBImageData *)data readyForImage:(__kindof UIImage *)image {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
if (self.imageScrollView.imageView.image == image) return;
CGSize size = self.imageScrollView.imageView.image.size;
[self.imageScrollView setImage:image type:YBIBScrollImageTypeOriginal];
[self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:size];
}
- (void)yb_imageData:(YBIBImageData *)data readyForCompressedImage:(__kindof UIImage *)image {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
if (self.imageScrollView.imageView.image == image) return;
CGSize size = self.imageScrollView.imageView.image.size;
[self.imageScrollView setImage:image type:YBIBScrollImageTypeCompressed];
[self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:size];
}
- (void)yb_imageData:(YBIBImageData *)data readyForThumbImage:(__kindof UIImage *)image {
if (self.imageScrollView.imageView.image) return;
[self.imageScrollView setImage:image type:YBIBScrollImageTypeThumb];
[self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:image.size];
}
- (void)yb_imageIsInvalidForData:(YBIBImageData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
NSString *imageIsInvalid = [YBIBCopywriter sharedCopywriter].imageIsInvalid;
if (self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:imageIsInvalid];
} else {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:imageIsInvalid];
}
}
- (void)yb_imageData:(YBIBImageData *)data downloadProgress:(CGFloat)progress {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self progress:progress];
}
- (void)yb_imageDownloadFailedForData:(YBIBImageData *)data {
if (self.imageScrollView.imageView.image) {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:[YBIBCopywriter sharedCopywriter].downloadFailed];
} else {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:[YBIBCopywriter sharedCopywriter].downloadFailed];
}
}
#pragma mark - gesture
- (void)addGesture {
UITapGestureRecognizer *tapSingle = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapSingle:)];
tapSingle.numberOfTapsRequired = 1;
UITapGestureRecognizer *tapDouble = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapDouble:)];
tapDouble.numberOfTapsRequired = 2;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToPan:)];
pan.maximumNumberOfTouches = 1;
pan.delegate = self;
[tapSingle requireGestureRecognizerToFail:tapDouble];
[tapSingle requireGestureRecognizerToFail:pan];
[tapDouble requireGestureRecognizerToFail:pan];
[self addGestureRecognizer:tapSingle];
[self addGestureRecognizer:tapDouble];
[self addGestureRecognizer:pan];
}
- (void)respondsToTapSingle:(UITapGestureRecognizer *)tap {
if (self.yb_isRotating()) return;
YBIBImageData *data = self.yb_cellData;
if (data.singleTouchBlock) {
data.singleTouchBlock(data);
} else {
[self hideTailoringImageView];
[self hideAuxiliaryView];
self.yb_hideBrowser();
}
}
- (void)respondsToTapDouble:(UITapGestureRecognizer *)tap {
if (self.yb_isRotating()) return;
[self hideTailoringImageView];
UIScrollView *scrollView = self.imageScrollView;
UIView *zoomView = [self viewForZoomingInScrollView:scrollView];
CGPoint point = [tap locationInView:zoomView];
if (!CGRectContainsPoint(zoomView.bounds, point)) return;
if (scrollView.zoomScale == scrollView.maximumZoomScale) {
[scrollView setZoomScale:1 animated:YES];
} else {
[scrollView zoomToRect:CGRectMake(point.x, point.y, 1, 1) animated:YES];
}
}
- (void)respondsToPan:(UIPanGestureRecognizer *)pan {
if (self.yb_isRotating()) return;
YBIBInteractionProfile *profile = ((YBIBImageData *)self.yb_cellData).interactionProfile;
if (profile.disable) return;
if ((CGRectIsEmpty(self.imageScrollView.imageView.frame) || !self.imageScrollView.imageView.image)) return;
CGPoint point = [pan locationInView:self];
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
if (pan.state == UIGestureRecognizerStateBegan) {
_interactStartPoint = point;
} else if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateRecognized || pan.state == UIGestureRecognizerStateFailed) {
// End.
if (_interacting) {
CGPoint velocity = [pan velocityInView:self.imageScrollView];
BOOL velocityArrive = ABS(velocity.y) > profile.dismissVelocityY;
BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > containerSize.height * profile.dismissScale;
BOOL shouldDismiss = distanceArrive || velocityArrive;
if (shouldDismiss) {
[self hideBrowser];
} else {
[self restoreInteractionWithDuration:profile.restoreDuration];
}
}
} else if (pan.state == UIGestureRecognizerStateChanged) {
if (_interacting) {
// Change.
self.imageScrollView.center = point;
CGFloat scale = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 1.2);
if (scale > 1) scale = 1;
if (scale < 0.35) scale = 0.35;
self.imageScrollView.transform = CGAffineTransformMakeScale(scale, scale);
CGFloat alpha = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 0.7);
if (alpha > 1) alpha = 1;
if (alpha < 0) alpha = 0;
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:alpha];
} else {
// Start.
if (CGPointEqualToPoint(_interactStartPoint, CGPointZero) || self.yb_currentPage() != self.yb_selfPage() || !self.yb_cellIsInCenter() || self.imageScrollView.isZooming) return;
CGPoint velocity = [pan velocityInView:self.imageScrollView];
CGFloat triggerDistance = profile.triggerDistance;
CGFloat offsetY = self.imageScrollView.contentOffset.y, height = self.imageScrollView.bounds.size.height;
BOOL distanceArrive = ABS(point.x - _interactStartPoint.x) < triggerDistance && ABS(velocity.x) < 500;
BOOL upArrive = point.y - _interactStartPoint.y > triggerDistance && offsetY <= 1;
BOOL downArrive = point.y - _interactStartPoint.y < -triggerDistance && offsetY + height >= MAX(self.imageScrollView.contentSize.height, height) - 1;
BOOL shouldStart = (upArrive || downArrive) && distanceArrive;
if (!shouldStart) return;
_interactStartPoint = point;
CGRect startFrame = self.imageScrollView.frame;
CGFloat anchorX = point.x / startFrame.size.width, anchorY = point.y / startFrame.size.height;
self.imageScrollView.layer.anchorPoint = CGPointMake(anchorX, anchorY);
self.imageScrollView.userInteractionEnabled = NO;
self.imageScrollView.scrollEnabled = NO;
self.imageScrollView.center = point;
self.yb_hideToolViews(YES);
self.yb_hideStatusBar(NO);
self.yb_collectionView().scrollEnabled = NO;
[self hideTailoringImageView];
_interacting = YES;
}
}
}
- (void)restoreInteractionWithDuration:(NSTimeInterval)duration {
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
void (^animations)(void) = ^{
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:1];
CGPoint anchorPoint = self.imageScrollView.layer.anchorPoint;
self.imageScrollView.center = CGPointMake(containerSize.width * anchorPoint.x, containerSize.height * anchorPoint.y);
self.imageScrollView.transform = CGAffineTransformIdentity;
};
void (^completion)(BOOL finished) = ^(BOOL finished){
self.imageScrollView.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.imageScrollView.center = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
self.imageScrollView.userInteractionEnabled = YES;
self.imageScrollView.scrollEnabled = YES;
self.yb_hideToolViews(NO);
self.yb_hideStatusBar(YES);
self.yb_collectionView().scrollEnabled = YES;
[self cuttingImage];
self->_interactStartPoint = CGPointZero;
self->_interacting = NO;
};
if (duration <= 0) {
animations();
completion(NO);
} else {
[UIView animateWithDuration:duration animations:animations completion:completion];
}
}
#pragma mark - <UIScrollViewDelegate>
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
YBIBImageData *data = self.yb_cellData;
if (data.imageDidZoomBlock) {
data.imageDidZoomBlock(data, scrollView);
}
CGRect imageViewFrame = self.imageScrollView.imageView.frame;
CGFloat width = imageViewFrame.size.width,
height = imageViewFrame.size.height,
sHeight = scrollView.bounds.size.height,
sWidth = scrollView.bounds.size.width;
if (height > sHeight) {
imageViewFrame.origin.y = 0;
} else {
imageViewFrame.origin.y = (sHeight - height) / 2.0;
}
if (width > sWidth) {
imageViewFrame.origin.x = 0;
} else {
imageViewFrame.origin.x = (sWidth - width) / 2.0;
}
self.imageScrollView.imageView.frame = imageViewFrame;
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageScrollView.imageView;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
YBIBImageData *data = self.yb_cellData;
if (data.imageDidScrollBlock) {
data.imageDidScrollBlock(data, scrollView);
}
[self cuttingImage];
}
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
[self hideTailoringImageView];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self hideTailoringImageView];
}
#pragma mark - <UIGestureRecognizerDelegate>
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
#pragma mark - getters
- (YBIBImageScrollView *)imageScrollView {
if (!_imageScrollView) {
_imageScrollView = [YBIBImageScrollView new];
_imageScrollView.delegate = self;
}
return _imageScrollView;
}
- (UIImageView *)tailoringImageView {
if (!_tailoringImageView) {
_tailoringImageView = [UIImageView new];
_tailoringImageView.contentMode = UIViewContentModeScaleAspectFit;
}
return _tailoringImageView;
}
@end

View File

@@ -0,0 +1,63 @@
//
// YBIBImageData+Internal.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/12.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBImageData.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, YBIBImageLoadingStatus) {
YBIBImageLoadingStatusNone,
YBIBImageLoadingStatusCompressing,
YBIBImageLoadingStatusDecoding,
YBIBImageLoadingStatusQuerying,
YBIBImageLoadingStatusProcessing,
YBIBImageLoadingStatusDownloading,
YBIBImageLoadingStatusReadingPHAsset,
};
@class YBIBImageData;
@protocol YBIBImageDataDelegate <NSObject>
@required
- (void)yb_imageData:(YBIBImageData *)data startLoadingWithStatus:(YBIBImageLoadingStatus)status;
- (void)yb_imageIsInvalidForData:(YBIBImageData *)data;
- (void)yb_imageData:(YBIBImageData *)data readyForImage:(__kindof UIImage *)image;
- (void)yb_imageData:(YBIBImageData *)data readyForThumbImage:(__kindof UIImage *)image;
- (void)yb_imageData:(YBIBImageData *)data readyForCompressedImage:(__kindof UIImage *)image;
- (void)yb_imageData:(YBIBImageData *)data downloadProgress:(CGFloat)progress;
- (void)yb_imageDownloadFailedForData:(YBIBImageData *)data;
@end
@interface YBIBImageData ()
@property (nonatomic, weak) id<YBIBImageDataDelegate> delegate;
/// The status of asynchronous tasks.
@property (nonatomic, assign) YBIBImageLoadingStatus loadingStatus;
/// The origin image.
@property (nonatomic, strong) UIImage *originImage;
/// The image compressed by 'originImage' if need.
@property (nonatomic, strong) UIImage *compressedImage;
- (BOOL)shouldCompress;
- (void)cuttingImageToRect:(CGRect)rect complete:(void(^)(UIImage * _Nullable image))complete;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,157 @@
//
// YBIBImageData.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/5.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <Photos/Photos.h>
#import "YBIBDataProtocol.h"
#import "YBIBImageLayout.h"
#import "YBIBImageCache.h"
#import "YBIBInteractionProfile.h"
NS_ASSUME_NONNULL_BEGIN
@class YBIBImageData;
/// 获取 NSData 的闭包
typedef NSData * _Nullable (^YBIBImageDataBlock)(void);
/// 获取 UIImage 的闭包
typedef UIImage * _Nullable (^YBIBImageBlock)(void);
/// 修改 NSURLRequest 并返回的闭包
typedef NSURLRequest * _Nullable (^YBIBRequestModifierBlock)(YBIBImageData *imageData, NSURLRequest *request);
/// 根据图片逻辑像素和 scale 判断是否需要预解码的闭包
typedef BOOL (^YBIBPreDecodeDecisionBlock)(YBIBImageData *imageData, CGSize imageSize, CGFloat scale);
/// 修改 UIImage 并返回的闭包
typedef void (^YBIBImageModifierBlock)(YBIBImageData *imageData, UIImage *image, void(^completion)(UIImage *processedImage));
/// 单击事件的处理闭包
typedef void (^YBIBImageSingleTouchBlock)(YBIBImageData *imageData);
/// 内部图片滚动视图状态回调闭包
typedef void (^YBIBImageScrollViewStatusBlock)(YBIBImageData *imageData, UIScrollView *scrollView);
/**
图片数据类,承担配置数据和处理数据的责任
*/
@interface YBIBImageData : NSObject <YBIBDataProtocol>
/// 本地图片名字
@property (nonatomic, copy, nullable) NSString *imageName;
/// 本地图片路径
@property (nonatomic, copy, nullable) NSString *imagePath;
/// 本地图片字节码,返回 NSData
@property (nonatomic, copy, nullable) YBIBImageDataBlock imageData;
/// 本地图片,返回 UIImage 及其衍生类 (若不是遵循'YYAnimatedImage'协议的类型,将失去对动图和拓展格式的支持)
@property (nonatomic, copy, nullable) YBIBImageBlock image;
/// 网络图片资源
@property (nonatomic, copy, nullable) NSURL *imageURL;
/// 修改 NSURLRequest 并返回
@property (nonatomic, copy, nullable) YBIBRequestModifierBlock requestModifier;
/// 相册图片资源
@property (nonatomic, strong, nullable) PHAsset *imagePHAsset;
/// 投影视图,当前数据模型对应外界业务的 UIView (通常为 UIImageView),做转场动效用
@property (nonatomic, weak, nullable) __kindof UIView *projectiveView;
/// 预览图/缩约图,注意若这个图片过大会导致内存压力(若 projectiveView 存在且是 UIImageView 类型将会自动获取缩约图)
@property (nonatomic, strong, nullable) UIImage *thumbImage;
/// 预览图/缩约图 URL缓存中未找到则忽略若 projectiveView 存在且是 UIImageView 类型将会自动获取缩约图)
@property (nonatomic, copy, nullable) NSURL *thumbURL;
/// 是否允许保存到相册
@property (nonatomic, assign) BOOL allowSaveToPhotoAlbum;
/// 根据图片信息判断是否需要预解码
@property (nonatomic, copy, nullable) YBIBPreDecodeDecisionBlock preDecodeDecision;
/// 是否异步预解码,默认为 YES
@property (nonatomic, assign) BOOL shouldPreDecodeAsync;
/// 压缩物理像素界限大小,当图片超过这个值将会被压缩显示,默认为 4096*4096
@property (nonatomic, assign) CGFloat compressingSize;
/// 触发裁剪的缩放比例,必须大于等于 1默认情况内部会动态计算 (仅当图片需要压缩显示时有效)
@property (nonatomic, assign) CGFloat cuttingZoomScale;
/**
判断图片是否需要压缩显示
*/
- (BOOL)shouldCompressWithImage:(UIImage *)image;
/**
修改原始图片并返回处理后的图片 (特别注意当 image 是大图的时候,避免 OOM)
Example:
[... setImageModifier:^(YBIBImageData *imageData, UIImage * _Nonnull image, void (^ _Nonnull completion)(UIImage * _Nonnull)) {
//step 1 : Add watermark, trademark, etc. (Sync or async).
... processing code ...
//step 2 : Return the processed UIImage.
completion(image);
}];
*/
@property (nonatomic, copy, nullable) YBIBImageModifierBlock originImageModifier;
/// 修改压缩图片并返回处理后的图片 (仅在当前图片需要被压缩时有效)
@property (nonatomic, copy, nullable) YBIBImageModifierBlock compressedImageModifier;
/// 修改裁剪图片并返回处理后的图片 (仅在当前图片需要被裁剪时有效)
@property (nonatomic, copy, nullable) YBIBImageModifierBlock cuttedImageModifier;
/// 预留属性可随意使用
@property (nonatomic, strong, nullable) id extraData;
/// 手势交互动效配置文件
@property (nonatomic, strong) YBIBInteractionProfile *interactionProfile;
/// 单击的处理,默认是退出图片浏览器
@property (nonatomic, copy, nullable) YBIBImageSingleTouchBlock singleTouchBlock;
/// 图片滚动的回调
@property (nonatomic, copy, nullable) YBIBImageScrollViewStatusBlock imageDidScrollBlock;
/// 图片缩放的回调
@property (nonatomic, copy, nullable) YBIBImageScrollViewStatusBlock imageDidZoomBlock;
/// 图片布局类 (赋值可自定义)
@property (nonatomic, strong) id<YBIBImageLayout> layout;
/// 默认图片布局类 (可配置其属性)
@property (nonatomic, weak, readonly) YBIBImageLayout *defaultLayout;
/**
终止处理数据程序
*/
- (void)stopLoading;
/**
清除缓存的数据
*/
- (void)clearCache;
/**
加载数据,一般不需要调用这个方法,当该数据对象做了数据更新时调用
*/
- (void)loadData;
/// 处理后的原始图片
@property (nonatomic, strong, readonly) UIImage *originImage;
/// 处理后的压缩图片
@property (nonatomic, strong, readonly) UIImage *compressedImage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,695 @@
//
// YBIBImageData.m
// YBImageBrowserDemo
//
// Created by on 2019/6/5.
// Copyright © 2019 . All rights reserved.
//
#import "YBImage.h"
#import "YBIBImageData.h"
#import "YBIBImageCell.h"
#import "YBIBPhotoAlbumManager.h"
#import "YBIBImageData+Internal.h"
#import "YBIBUtilities.h"
#import "YBIBImageCache+Internal.h"
#import "YBIBSentinel.h"
#import "YBIBCopywriter.h"
#import <AssetsLibrary/AssetsLibrary.h>
extern CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay);
static dispatch_queue_t YBIBImageProcessingQueue(void) {
static dispatch_queue_t queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("com.yangbo.imagebrowser.imageprocessing", DISPATCH_QUEUE_CONCURRENT);
});
return queue;
}
@implementation YBIBImageData {
__weak id _downloadToken;
YBIBSentinel *_cuttingSentinel;
/// Stop processing tasks when in freeze.
BOOL _freezing;
}
#pragma mark - life cycle
- (void)dealloc {
if (_downloadToken && [self.yb_webImageMediator() respondsToSelector:@selector(yb_cancelTaskWithDownloadToken:)]) {
[self.yb_webImageMediator() yb_cancelTaskWithDownloadToken:_downloadToken];
}
[self.imageCache removeForKey:self.cacheKey];
}
- (instancetype)init {
self = [super init];
if (self) {
[self initValue];
}
return self;
}
- (void)initValue {
_defaultLayout = _layout = [YBIBImageLayout new];
_loadingStatus = YBIBImageLoadingStatusNone;
_compressingSize = 4096 * 4096;
_shouldPreDecodeAsync = YES;
_freezing = NO;
_cuttingSentinel = [YBIBSentinel new];
_interactionProfile = [YBIBInteractionProfile new];
_allowSaveToPhotoAlbum = YES;
}
#pragma mark - load data
- (void)loadData {
_freezing = NO;
// Avoid handling asynchronous tasks repeatedly.
if (self.loadingStatus != YBIBImageLoadingStatusNone) {
[self loadThumbImage];
self.loadingStatus = self.loadingStatus;
return;
}
if (self.originImage) {
[self loadOriginImage];
} else if (self.imageName || self.imagePath || self.imageData) {
[self loadYBImage];
} else if (self.image) {
[self loadImageBlock];
} else if (self.imageURL) {
[self loadThumbImage];
[self loadURL];
} else if (self.imagePHAsset) {
[self loadThumbImage];
[self loadPHAsset];
} else {
[self.delegate yb_imageIsInvalidForData:self];
}
}
- (void)loadOriginImage {
if (_freezing) return;
if (!self.originImage) return;
if ([self shouldCompress]) {
if (self.compressedImage) {
[self.delegate yb_imageData:self readyForCompressedImage:self.compressedImage];
} else {
[self loadThumbImage];
[self loadOriginImage_compress];
}
} else {
[self.delegate yb_imageData:self readyForImage:self.originImage];
}
}
- (void)loadOriginImage_compress {
if (_freezing) return;
if (!self.originImage) return;
self.loadingStatus = YBIBImageLoadingStatusCompressing;
__weak typeof(self) wSelf = self;
CGSize size = [self bestSizeOfCompressing];
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), ^{
if (self->_freezing) {
self.loadingStatus = YBIBImageLoadingStatusNone;
return;
}
// Ensure the best display effect.
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
[self.originImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
if (self->_freezing) {
UIGraphicsEndImageContext();
self.loadingStatus = YBIBImageLoadingStatusNone;
return;
}
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
[self modifyImageWithModifier:self.compressedImageModifier image:resultImage completion:^(UIImage *processedImage) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.compressedImage = processedImage ?: self.originImage;
[self.delegate yb_imageData:self readyForCompressedImage:self.compressedImage];
}];
})
})
}
- (void)loadYBImage {
if (_freezing) return;
NSString *name = self.imageName.copy;
NSString *path = self.imagePath.copy;
NSData *data = self.imageData ? self.imageData().copy : nil;
if (name.length == 0 && path.length == 0 && data.length == 0) return;
YBImageDecodeDecision decision = [self defaultDecodeDecision];
__block YBImage *image;
__weak typeof(self) wSelf = self;
void(^dealBlock)(void) = ^{
if (name.length > 0) {
image = [YBImage imageNamed:name decodeDecision:decision];
} else if (path.length > 0) {
image = [YBImage imageWithContentsOfFile:path decodeDecision:decision];
} else if (data.length > 0) {
image = [YBImage imageWithData:data scale:UIScreen.mainScreen.scale decodeDecision:decision];
}
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
if (image) {
[self setOriginImageAndLoadWithImage:image];
} else {
[self.delegate yb_imageIsInvalidForData:self];
}
})
};
if (self.shouldPreDecodeAsync) {
[self loadThumbImage];
self.loadingStatus = YBIBImageLoadingStatusDecoding;
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), dealBlock)
} else {
self.loadingStatus = YBIBImageLoadingStatusDecoding;
dealBlock();
}
}
- (void)loadImageBlock {
if (_freezing) return;
__block UIImage *image = self.image ? self.image() : nil;
if (!image) return;
BOOL shouldPreDecode = self.preDecodeDecision ? self.preDecodeDecision(self, image.size, image.scale) : ![self shouldCompressWithImage:image];
__weak typeof(self) wSelf = self;
void(^dealBlock)(void) = ^{
// Do not need to decode If 'image' conformed 'YYAnimatedImage'. (Not entirely accurate.)
if (![image conformsToProtocol:@protocol(YYAnimatedImage)]) {
CGImageRef cgImage = YYCGImageCreateDecodedCopy(image.CGImage, shouldPreDecode);
image = [UIImage imageWithCGImage:cgImage scale:image.scale orientation:image.imageOrientation];
if (cgImage) CGImageRelease(cgImage);
}
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
[self setOriginImageAndLoadWithImage:image];
})
};
if (self.shouldPreDecodeAsync) {
[self loadThumbImage];
self.loadingStatus = YBIBImageLoadingStatusDecoding;
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), dealBlock)
} else {
self.loadingStatus = YBIBImageLoadingStatusDecoding;
dealBlock();
}
}
- (void)loadURL {
if (!self.imageURL || self.imageURL.absoluteString.length == 0) return;
[self loadURL_queryCache];
}
- (void)loadURL_queryCache {
if (_freezing) return;
if (!self.imageURL || self.imageURL.absoluteString.length == 0) return;
YBImageDecodeDecision decision = [self defaultDecodeDecision];
self.loadingStatus = YBIBImageLoadingStatusQuerying;
[self.yb_webImageMediator() yb_queryCacheOperationForKey:self.imageURL completed:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
if (!imageData || imageData.length == 0) {
YBIB_DISPATCH_ASYNC_MAIN(^{
self.loadingStatus = YBIBImageLoadingStatusNone;
[self loadURL_download];
})
return;
}
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), ^{
if (self->_freezing) {
self.loadingStatus = YBIBImageLoadingStatusNone;
return;
}
YBImage *image = [YBImage imageWithData:imageData scale:UIScreen.mainScreen.scale decodeDecision:decision];
__weak typeof(self) wSelf = self;
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
if (image) { // Maybe the image data is invalid.
[self setOriginImageAndLoadWithImage:image];
} else {
[self loadURL_download];
}
})
})
}];
}
- (void)loadURL_download {
if (_freezing) return;
if (!self.imageURL || self.imageURL.absoluteString.length == 0) return;
YBImageDecodeDecision decision = [self defaultDecodeDecision];
self.loadingStatus = YBIBImageLoadingStatusDownloading;
__weak typeof(self) wSelf = self;
_downloadToken = [self.yb_webImageMediator() yb_downloadImageWithURL:self.imageURL requestModifier:^NSURLRequest * _Nullable(NSURLRequest * _Nonnull request) {
return self.requestModifier ? self.requestModifier(self, request) : request;
} progress:^(NSInteger receivedSize, NSInteger expectedSize) {
CGFloat progress = receivedSize * 1.0 / expectedSize ?: 0;
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
[self.delegate yb_imageData:self downloadProgress:progress];
})
} success:^(NSData * _Nullable imageData, BOOL finished) {
if (!finished) return;
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), ^{
if (self->_freezing) {
self.loadingStatus = YBIBImageLoadingStatusNone;
return;
}
YBImage *image = [YBImage imageWithData:imageData scale:UIScreen.mainScreen.scale decodeDecision:decision];
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
[self.yb_webImageMediator() yb_storeToDiskWithImageData:imageData forKey:self.imageURL];
self.loadingStatus = YBIBImageLoadingStatusNone;
if (image) {
[self setOriginImageAndLoadWithImage:image];
} else {
[self.delegate yb_imageIsInvalidForData:self];
}
})
})
} failed:^(NSError * _Nullable error, BOOL finished) {
if (!finished) return;
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
[self.delegate yb_imageDownloadFailedForData:self];
}];
}
- (void)loadPHAsset {
if (_freezing) return;
if (!self.imagePHAsset) return;
YBImageDecodeDecision decision = [self defaultDecodeDecision];
self.loadingStatus = YBIBImageLoadingStatusReadingPHAsset;
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), ^{
[YBIBPhotoAlbumManager getImageDataWithPHAsset:self.imagePHAsset completion:^(NSData * _Nullable data) {
if (self->_freezing) {
self.loadingStatus = YBIBImageLoadingStatusNone;
return;
}
YBImage *image = [YBImage imageWithData:data scale:UIScreen.mainScreen.scale decodeDecision:decision];
__weak typeof(self) wSelf = self;
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
if (image) {
[self setOriginImageAndLoadWithImage:image];
} else {
[self.delegate yb_imageIsInvalidForData:self];
}
})
}];
})
}
- (void)loadThumbImage {
if (_freezing) return;
if (self.thumbImage) {
[self.delegate yb_imageData:self readyForThumbImage:self.thumbImage];
} else if (self.thumbURL) {
__weak typeof(self) wSelf = self;
[self.yb_webImageMediator() yb_queryCacheOperationForKey:self.thumbURL completed:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
UIImage *thumbImage;
if (image) {
thumbImage = image;
} else if (imageData) {
thumbImage = [UIImage imageWithData:imageData];
}
// If the target image is ready, ignore the thumb image.
BOOL shouldIgnore = [self shouldCompress] ? (self.compressedImage != nil) : (self.originImage != nil);
if (!shouldIgnore && thumbImage) {
[self.delegate yb_imageData:self readyForThumbImage:thumbImage];
}
}];
} else if (self.projectiveView && [self.projectiveView isKindOfClass:UIImageView.self] && ((UIImageView *)self.projectiveView).image) {
UIImage *thumbImage = ((UIImageView *)self.projectiveView).image;
[self.delegate yb_imageData:self readyForThumbImage:thumbImage];
}
}
#pragma mark - internal
- (void)cuttingImageToRect:(CGRect)rect complete:(void (^)(UIImage * _Nullable))complete {
if (_freezing) return;
if (!self.originImage) return;
int32_t value = [_cuttingSentinel increase];
BOOL (^isCancelled)(void) = ^BOOL(void) {
if (self->_freezing) return YES;
return value != self->_cuttingSentinel.value;
};
YBIB_DISPATCH_ASYNC(YBIBImageProcessingQueue(), ^{
if (isCancelled()) {
complete(nil);
return;
}
CGFloat scale = self.originImage.scale;
CGFloat width = self.originImage.size.width;
CGFloat height = self.originImage.size.height;
BOOL reverseWidthHeight = NO;
CGAffineTransform transform = CGAffineTransformIdentity;
switch (self.originImage.imageOrientation) {
case UIImageOrientationDown:
case UIImageOrientationDownMirrored:
CGAffineTransformTranslate(CGAffineTransformMakeRotation(-M_PI), -width, -height);
break;
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
transform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(M_PI_2), 0, -height);
reverseWidthHeight = YES;
break;
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(-M_PI_2), -width, 0);
reverseWidthHeight = YES;
break;
default: break;
}
transform = CGAffineTransformScale(transform, scale, scale);
CGRect correctRect = CGRectApplyAffineTransform(rect, transform);
CGImageRef cgImage = CGImageCreateWithImageInRect(self.originImage.CGImage, correctRect);
if (isCancelled()) {
complete(nil);
if (cgImage) CGImageRelease(cgImage);
return;
}
CGFloat cgWidth = reverseWidthHeight ? CGImageGetHeight(cgImage) : CGImageGetWidth(cgImage);
CGFloat cgHeight = reverseWidthHeight ? CGImageGetWidth(cgImage) : CGImageGetHeight(cgImage);
CGSize size = [self bestSizeOfCuttingWithOriginSize:CGSizeMake(cgWidth / scale, cgHeight / scale)];
UIImage *tmpImage = [UIImage imageWithCGImage:cgImage scale:self.originImage.scale orientation:self.originImage.imageOrientation];
if (isCancelled()) {
complete(nil);
if (cgImage) CGImageRelease(cgImage);
return;
}
// Ensure the best display effect.
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
[tmpImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
if (isCancelled()) {
complete(nil);
UIGraphicsEndImageContext();
if (cgImage) CGImageRelease(cgImage);
return;
}
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (cgImage) CGImageRelease(cgImage);
__weak typeof(self) wSelf = self;
YBIB_DISPATCH_ASYNC_MAIN(^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
[self modifyImageWithModifier:self.cuttedImageModifier image:resultImage completion:^(UIImage *image) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
complete(image);
}];
})
})
}
- (BOOL)shouldCompress {
return [self shouldCompressWithImage:self.originImage];
}
#pragma mark - public
- (BOOL)shouldCompressWithImage:(UIImage *)image {
if (!image) return NO;
return [self shouldCompressWithImageSize:image.size scale:image.scale];
}
- (void)stopLoading {
_freezing = YES;
self.loadingStatus = YBIBImageLoadingStatusNone;
}
- (void)clearCache {
[self.imageCache removeForKey:self.cacheKey];
}
#pragma mark - private
/// 'size': logic pixel.
- (BOOL)shouldCompressWithImageSize:(CGSize)size scale:(CGFloat)scale {
return size.width * scale * size.height * scale > self.compressingSize;
}
/// Logic pixel.
- (CGSize)bestSizeOfCompressing {
if (!self.originImage) return CGSizeZero;
UIDeviceOrientation orientation = self.yb_currentOrientation();
CGRect imageViewFrame = [self.layout yb_imageViewFrameWithContainerSize:self.yb_containerSize(orientation) imageSize:self.originImage.size orientation:orientation];
return imageViewFrame.size;
}
/// Logic pixel.
- (CGSize)bestSizeOfCuttingWithOriginSize:(CGSize)originSize {
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
CGFloat maxWidth = containerSize.width, maxHeight = containerSize.height;
CGFloat oWidth = originSize.width, oHeight = originSize.height;
if (maxWidth <= 0 || maxHeight <= 0 || oWidth <= 0 || oHeight <= 0) return CGSizeZero;
if (oWidth <= maxWidth && oHeight <= maxHeight) {
return originSize;
}
CGFloat rWidth = 0, rHeight = 0;
if (oWidth / maxWidth < oHeight / maxHeight) {
rHeight = maxHeight;
rWidth = oWidth / oHeight * rHeight;
} else {
rWidth = maxWidth;
rHeight = oHeight / oWidth * rWidth;
}
return CGSizeMake(rWidth, rHeight);
}
- (YBImageDecodeDecision)defaultDecodeDecision {
__weak typeof(self) wSelf = self;
return ^BOOL(CGSize imageSize, CGFloat scale) {
__strong typeof(wSelf) self = wSelf;
if (!self) return NO;
CGSize logicSize = CGSizeMake(imageSize.width / scale, imageSize.height / scale);
if (self.preDecodeDecision) return self.preDecodeDecision(self, logicSize, scale);
if ([self shouldCompressWithImageSize:logicSize scale:scale]) return NO;
return YES;
};
}
- (void)modifyImageWithModifier:(YBIBImageModifierBlock)modifier image:(UIImage *)image completion:(void(^)(UIImage *processedImage))completion {
if (modifier) {
self.loadingStatus = YBIBImageLoadingStatusProcessing;
__weak typeof(self) wSelf = self;
modifier(self, image, ^(UIImage *processedImage){
// This step is necessary, maybe 'self' is already 'dealloc' if processing code takes too much time.
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.loadingStatus = YBIBImageLoadingStatusNone;
completion(processedImage);
});
} else {
completion(image);
}
}
- (void)setOriginImageAndLoadWithImage:(UIImage *)image {
__weak typeof(self) wSelf = self;
[self modifyImageWithModifier:self.originImageModifier image:image completion:^(UIImage *processedImage) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
self.originImage = processedImage;
[self loadOriginImage];
}];
}
- (void)saveToPhotoAlbumCompleteWithError:(nullable NSError *)error {
if (error) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbumFailed];
} else {
[self.yb_auxiliaryViewHandler() yb_showCorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbumSuccess];
}
}
- (void)UIImageWriteToSavedPhotosAlbum_completedWithImage:(UIImage *)image error:(NSError *)error context:(void *)context {
[self saveToPhotoAlbumCompleteWithError:error];
}
- (YBIBImageCache *)imageCache {
return self.yb_backView.ybib_imageCache;
}
#pragma mark - <YBIBDataProtocol>
@synthesize yb_isHideTransitioning = _yb_isHideTransitioning;
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_containerView = _yb_containerView;
@synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
@synthesize yb_webImageMediator = _yb_webImageMediator;
@synthesize yb_backView = _yb_backView;
- (nonnull Class)yb_classOfCell {
return YBIBImageCell.self;
}
- (UIView *)yb_projectiveView {
return self.projectiveView;
}
- (CGRect)yb_imageViewFrameWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation {
return [self.layout yb_imageViewFrameWithContainerSize:containerSize imageSize:imageSize orientation:orientation];
}
- (void)yb_preload {
if (!self.delegate) {
[self loadData];
}
}
- (BOOL)yb_allowSaveToPhotoAlbum {
return self.allowSaveToPhotoAlbum;
}
- (void)yb_saveToPhotoAlbum {
void(^saveData)(NSData *) = ^(NSData * _Nonnull data){
[[ALAssetsLibrary new] writeImageDataToSavedPhotosAlbum:data metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {
[self saveToPhotoAlbumCompleteWithError:error];
}];
};
void(^saveImage)(UIImage *) = ^(UIImage * _Nonnull image){
UIImageWriteToSavedPhotosAlbum(image, self, @selector(UIImageWriteToSavedPhotosAlbum_completedWithImage:error:context:), NULL);
};
void(^unableToSave)(void) = ^(){
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].unableToSave];
};
[YBIBPhotoAlbumManager getPhotoAlbumAuthorizationSuccess:^{
if ([self.originImage conformsToProtocol:@protocol(YYAnimatedImage)] && [self.originImage respondsToSelector:@selector(animatedImageData)] && [self.originImage performSelector:@selector(animatedImageData)]) {
NSData *data = [self.originImage performSelector:@selector(animatedImageData)];
data ? saveData(data) : unableToSave();
} else if (self.originImage) {
saveImage(self.originImage);
} else if (self.imageURL) {
[self.yb_webImageMediator() yb_queryCacheOperationForKey:self.imageURL completed:^(UIImage * _Nullable image, NSData * _Nullable data) {
if (data) {
saveData(data);
} else if (image) {
saveImage(image);
} else {
unableToSave();
}
}];
} else {
unableToSave();
}
} failed:^{
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self.yb_containerView text:[YBIBCopywriter sharedCopywriter].getPhotoAlbumAuthorizationFailed];
}];
}
#pragma mark - getters & setters
@synthesize delegate = _delegate;
- (void)setDelegate:(id<YBIBImageDataDelegate>)delegate {
_delegate = delegate;
if (delegate) {
[self loadData];
} else {
_freezing = YES;
// Remove the resident cache if '_delegate' is nil.
[self.imageCache removeResidentForKey:self.cacheKey];
}
}
- (id<YBIBImageDataDelegate>)delegate {
// Stop sending data to the '_delegate' if it is transiting.
return self.yb_isHideTransitioning() ? nil : _delegate;
}
- (void)setImageURL:(NSURL *)imageURL {
_imageURL = [imageURL isKindOfClass:NSString.class] ? [NSURL URLWithString:(NSString *)imageURL] : imageURL;
}
- (NSString *)cacheKey {
return [NSString stringWithFormat:@"%p", self];
}
- (void)setOriginImage:(__kindof UIImage *)originImage {
// 'image' should be resident if '_delegate' exists.
[self.imageCache setImage:originImage type:YBIBImageCacheTypeOrigin forKey:self.cacheKey resident:self->_delegate != nil];
}
- (UIImage *)originImage {
return [self.imageCache imageForKey:self.cacheKey type:YBIBImageCacheTypeOrigin];
}
- (void)setCompressedImage:(UIImage *)compressedImage {
// 'image' should be resident if '_delegate' exists.
[self.imageCache setImage:compressedImage type:YBIBImageCacheTypeCompressed forKey:self.cacheKey resident:_delegate != nil];
}
- (UIImage *)compressedImage {
return [self.imageCache imageForKey:self.cacheKey type:YBIBImageCacheTypeCompressed];
}
- (void)setLoadingStatus:(YBIBImageLoadingStatus)loadingStatus {
// Ensure thread safety.
YBIB_DISPATCH_ASYNC_MAIN(^{
self->_loadingStatus = loadingStatus;
[self.delegate yb_imageData:self startLoadingWithStatus:loadingStatus];
});
}
- (CGFloat)cuttingZoomScale {
if (_cuttingZoomScale >= 1) return _cuttingZoomScale;
_cuttingZoomScale = 1.1;
if (!self.originImage) return _cuttingZoomScale;
CGFloat imagePixel = self.originImage.size.width * self.originImage.size.height * self.originImage.scale * self.originImage.scale;
// The larger the image size, the larger the '_cuttingZoomScale', in order to reduce the burden of CPU and memory.
CGFloat scale = YBIBLowMemory() ? 0.28 : 0.19;
_cuttingZoomScale += (imagePixel / self.compressingSize * scale);
return _cuttingZoomScale;
}
@end

View File

@@ -0,0 +1,61 @@
//
// YBIBImageLayout.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/12.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBImageLayout <NSObject>
@required
/**
计算图片展示的位置
@param containerSize 容器大小
@param imageSize 图片大小 (逻辑像素)
@param orientation 图片浏览器的方向
@return 图片展示的位置 (frame)
*/
- (CGRect)yb_imageViewFrameWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation;
/**
计算最大缩放比例
@param containerSize 容器大小
@param imageSize 图片大小 (逻辑像素)
@param orientation 图片浏览器的方向
@return 最大缩放比例
*/
- (CGFloat)yb_maximumZoomScaleWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation;
@end
typedef NS_ENUM(NSUInteger, YBIBImageFillType) {
/// 宽度优先填充满
YBIBImageFillTypeFullWidth,
/// 完整显示
YBIBImageFillTypeCompletely
};
@interface YBIBImageLayout : NSObject <YBIBImageLayout>
/// 纵向的填充方式,默认 YBIBImageFillTypeCompletely
@property (nonatomic, assign) YBIBImageFillType verticalFillType;
/// 横向的填充方式,默认 YBIBImageFillTypeFullWidth
@property (nonatomic, assign) YBIBImageFillType horizontalFillType;
/// 最大缩放比例 (必须大于 1 才有效,若不指定内部会自动计算)
@property (nonatomic, assign) CGFloat maxZoomScale;
/// 自动计算严格缩放比例后,再乘以这个值作为最终缩放比例,默认 1.5
@property (nonatomic, assign) CGFloat zoomScaleSurplus;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,90 @@
//
// YBIBImageLayout.m
// YBImageBrowserDemo
//
// Created by on 2019/6/12.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBImageLayout.h"
@implementation YBIBImageLayout
#pragma mark - life cycle
- (instancetype)init {
self = [super init];
if (self) {
_verticalFillType = YBIBImageFillTypeCompletely;
_horizontalFillType = YBIBImageFillTypeFullWidth;
_zoomScaleSurplus = 1.5;
}
return self;
}
#pragma mark - private
- (YBIBImageFillType)fillTypeByOrientation:(UIDeviceOrientation)orientation {
return UIDeviceOrientationIsLandscape(orientation) ? self.horizontalFillType : self.verticalFillType;
}
#pragma mark - <YBIBImageLayout>
- (CGRect)yb_imageViewFrameWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation {
if (containerSize.width <= 0 || containerSize.height <= 0 || imageSize.width <= 0 || imageSize.height <= 0) return CGRectZero;
CGFloat x = 0, y = 0, width = 0, height = 0;
switch ([self fillTypeByOrientation:orientation]) {
case YBIBImageFillTypeFullWidth: {
x = 0;
width = containerSize.width;
height = containerSize.width * (imageSize.height / imageSize.width);
if (imageSize.width / imageSize.height >= containerSize.width / containerSize.height)
y = (containerSize.height - height) / 2.0;
else
y = 0;
}
break;
case YBIBImageFillTypeCompletely: {
if (imageSize.width / imageSize.height >= containerSize.width / containerSize.height) {
width = containerSize.width;
height = containerSize.width * (imageSize.height / imageSize.width);
x = 0;
y = (containerSize.height - height) / 2.0;
} else {
height = containerSize.height;
width = containerSize.height * (imageSize.width / imageSize.height);
x = (containerSize.width - width) / 2.0;
y = 0;
}
}
break;
default: return CGRectZero;
}
return CGRectMake(x, y, width, height);
}
- (CGFloat)yb_maximumZoomScaleWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation {
if (self.maxZoomScale >= 1) return self.maxZoomScale;
if (containerSize.width <= 0 || containerSize.height <= 0) return 0;
CGFloat scale = [UIScreen mainScreen].scale;
if (scale <= 0) return 0;
CGFloat widthScale = imageSize.width / scale / containerSize.width,
heightScale = imageSize.height / scale / containerSize.height,
maxScale = 1;
switch ([self fillTypeByOrientation:orientation]) {
case YBIBImageFillTypeFullWidth:
maxScale = widthScale;
break;
case YBIBImageFillTypeCompletely:
maxScale = MAX(widthScale, heightScale);
break;
default: return 0;
}
return MAX(maxScale, 1) * self.zoomScaleSurplus;
}
@end

View File

@@ -0,0 +1,32 @@
//
// YBIBImageScrollView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/10.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBImage.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, YBIBScrollImageType) {
YBIBScrollImageTypeNone,
YBIBScrollImageTypeOriginal,
YBIBScrollImageTypeCompressed,
YBIBScrollImageTypeThumb
};
@interface YBIBImageScrollView : UIScrollView
- (void)setImage:(__kindof UIImage *)image type:(YBIBScrollImageType)type;
@property (nonatomic, strong, readonly) YYAnimatedImageView *imageView;
@property (nonatomic, assign) YBIBScrollImageType imageType;
- (void)reset;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,64 @@
//
// YBIBImageScrollView.m
// YBImageBrowserDemo
//
// Created by on 2019/6/10.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBImageScrollView.h"
@interface YBIBImageScrollView ()
@property (nonatomic, strong) YYAnimatedImageView *imageView;
@end
@implementation YBIBImageScrollView
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.decelerationRate = UIScrollViewDecelerationRateFast;
self.maximumZoomScale = 1;
self.minimumZoomScale = 1;
self.alwaysBounceHorizontal = NO;
self.alwaysBounceVertical = NO;
self.layer.masksToBounds = NO;
if (@available(iOS 11.0, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
[self addSubview:self.imageView];
}
return self;
}
#pragma mark - public
- (void)setImage:(__kindof UIImage *)image type:(YBIBScrollImageType)type {
self.imageView.image = image;
self.imageType = type;
}
- (void)reset {
self.zoomScale = 1;
self.imageView.image = nil;
self.imageView.frame = CGRectZero;
self.imageType = YBIBScrollImageTypeNone;
}
#pragma mark - getters
- (YYAnimatedImageView *)imageView {
if (!_imageView) {
_imageView = [YYAnimatedImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFill;
_imageView.layer.masksToBounds = YES;
}
return _imageView;
}
@end

View File

@@ -0,0 +1,32 @@
//
// YBIBInteractionProfile.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/30.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface YBIBInteractionProfile : NSObject
/// 是否取消手势交互动效
@property (nonatomic, assign) BOOL disable;
/// 拖动的距离与最大高度的比例,达到这个比例就会出场
@property (nonatomic, assign) CGFloat dismissScale;
/// 拖动的速度,达到这个值就会出场
@property (nonatomic, assign) CGFloat dismissVelocityY;
/// 拖动动效复位时的时长
@property (nonatomic, assign) CGFloat restoreDuration;
/// 拖动触发手势交互动效的起始距离
@property (nonatomic, assign) CGFloat triggerDistance;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
//
// YBIBInteractionProfile.m
// YBImageBrowserDemo
//
// Created by on 2019/6/30.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBInteractionProfile.h"
@implementation YBIBInteractionProfile
- (instancetype)init {
self = [super init];
if (self) {
_disable = NO;
_dismissScale = 0.22;
_dismissVelocityY = 800;
_restoreDuration = 0.15;
_triggerDistance = 3;
}
return self;
}
@end

View File

@@ -0,0 +1,89 @@
//
// YBImage.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/8/31.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#if __has_include(<YYImage/YYImage.h>)
#import <YYImage/YYFrameImage.h>
#import <YYImage/YYSpriteSheetImage.h>
#import <YYImage/YYImageCoder.h>
#import <YYImage/YYAnimatedImageView.h>
#elif __has_include(<YYWebImage/YYImage.h>)
#import <YYWebImage/YYFrameImage.h>
#import <YYWebImage/YYSpriteSheetImage.h>
#import <YYWebImage/YYImageCoder.h>
#import <YYWebImage/YYAnimatedImageView.h>
#else
#import "YYFrameImage.h"
#import "YYSpriteSheetImage.h"
#import "YYImageCoder.h"
#import "YYAnimatedImageView.h"
#endif
NS_ASSUME_NONNULL_BEGIN
/// 🙄波儿菜Decide whether should to decode by image size. ('imageSize': physical pixel)
typedef BOOL(^YBImageDecodeDecision)(CGSize imageSize, CGFloat scale);
/**
It is a fully compatible `UIImage` subclass. It extends the UIImage
to support animated WebP, APNG and GIF format image data decoding. It also
support NSCoding protocol to archive and unarchive multi-frame image data.
If the image is created from multi-frame image data, and you want to play the
animation, try replace UIImageView with `YYAnimatedImageView`.
🙄波儿菜Copied from 'YYImage' and made some extensions.
*/
@interface YBImage : UIImage <YYAnimatedImage>
+ (nullable __kindof UIImage *)imageNamed:(NSString *)name; // no cache!
+ (nullable YBImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YBImage *)imageWithData:(NSData *)data;
+ (nullable YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;
/// 🙄波儿菜Expand methodes.
/// Start ->
+ (nullable __kindof UIImage *)imageNamed:(NSString *)name decodeDecision:(nullable YBImageDecodeDecision)decodeDecision;
+ (nullable YBImage *)imageWithContentsOfFile:(NSString *)path decodeDecision:(nullable YBImageDecodeDecision)decodeDecision;
+ (nullable YBImage *)imageWithData:(NSData *)data decodeDecision:(nullable YBImageDecodeDecision)decodeDecision;
+ (nullable YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale decodeDecision:(nullable YBImageDecodeDecision)decodeDecision;
/// <- End
/**
If the image is created from data or file, then the value indicates the data type.
*/
@property (nonatomic, readonly) YYImageType animatedImageType;
/**
If the image is created from animated image data (multi-frame GIF/APNG/WebP),
this property stores the original image data.
*/
@property (nullable, nonatomic, readonly) NSData *animatedImageData;
/**
The total memory usage (in bytes) if all frame images was loaded into memory.
The value is 0 if the image is not created from a multi-frame image data.
*/
@property (nonatomic, readonly) NSUInteger animatedImageMemorySize;
/**
Preload all frame image to memory.
@discussion Set this property to `YES` will block the calling thread to decode
all animation frame image to memory, set to `NO` will release the preloaded frames.
If the image is shared by lots of image views (such as emoticon), preload all
frames will reduce the CPU cost.
See `animatedImageMemorySize` for memory cost.
*/
@property (nonatomic) BOOL preloadAllAnimatedImageFrames;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,284 @@
//
// YBImage.m
// YBImageBrowserDemo
//
// Created by on 2018/8/31.
// Copyright © 2018 . All rights reserved.
//
#import "YBImage.h"
#import "YBIBImageData.h"
#import "YBIBUtilities.h"
/**
An array of NSNumber objects, shows the best order for path scale search.
e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1] iPhone6 Plus:@[@3,@2,@1]
*/
static NSArray *_NSBundlePreferredScales() {
static NSArray *scales;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale <= 1) {
scales = @[@1,@2,@3];
} else if (screenScale <= 2) {
scales = @[@2,@3,@1];
} else {
scales = @[@3,@2,@1];
}
});
return scales;
}
/**
Add scale modifier to the file name (without path extension),
From @"name" to @"name@2x".
e.g.
<table>
<tr><th>Before </th><th>After(scale:2)</th></tr>
<tr><td>"icon" </td><td>"icon@2x" </td></tr>
<tr><td>"icon " </td><td>"icon @2x" </td></tr>
<tr><td>"icon.top" </td><td>"icon.top@2x" </td></tr>
<tr><td>"/p/name" </td><td>"/p/name@2x" </td></tr>
<tr><td>"/path/" </td><td>"/path/" </td></tr>
</table>
@param scale Resource scale.
@return String by add scale modifier, or just return if it's not end with file name.
*/
static NSString *_NSStringByAppendingNameScale(NSString *string, CGFloat scale) {
if (!string) return nil;
if (fabs(scale - 1) <= __FLT_EPSILON__ || string.length == 0 || [string hasSuffix:@"/"]) return string.copy;
return [string stringByAppendingFormat:@"@%@x", @(scale)];
}
/**
Return the path scale.
e.g.
<table>
<tr><th>Path </th><th>Scale </th></tr>
<tr><td>"icon.png" </td><td>1 </td></tr>
<tr><td>"icon@2x.png" </td><td>2 </td></tr>
<tr><td>"icon@2.5x.png" </td><td>2.5 </td></tr>
<tr><td>"icon@2x" </td><td>1 </td></tr>
<tr><td>"icon@2x..png" </td><td>1 </td></tr>
<tr><td>"icon@2x.png/" </td><td>1 </td></tr>
</table>
*/
static CGFloat _NSStringPathScale(NSString *string) {
if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
NSString *name = string.stringByDeletingPathExtension;
__block CGFloat scale = 1;
NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil];
[pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
if (result.range.location >= 3) {
scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue;
}
}];
return scale;
}
@implementation YBImage {
YYImageDecoder *_decoder;
NSArray *_preloadedFrames;
dispatch_semaphore_t _preloadedLock;
NSUInteger _bytesPerFrame;
}
+ (__kindof UIImage *)imageNamed:(NSString *)name {
return [self imageNamed:name decodeDecision:nil];
}
+ (__kindof UIImage *)imageNamed:(NSString *)name decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
if (name.length == 0) return nil;
if ([name hasSuffix:@"/"]) return nil;
NSString *res = name.stringByDeletingPathExtension;
NSString *ext = name.pathExtension;
NSString *path = nil;
CGFloat scale = 1;
// If no extension, guess by system supported (same as UIImage).
NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
NSArray *scales = _NSBundlePreferredScales();
for (int s = 0; s < scales.count; s++) {
scale = ((NSNumber *)scales[s]).floatValue;
NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
for (NSString *e in exts) {
path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
if (path) break;
}
if (path) break;
}
if (path.length == 0) {
// 🙄Assets.xcassets supported.
return [super imageNamed:name];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) return nil;
return [[self alloc] initWithData:data scale:scale decodeDecision:decodeDecision];
}
+ (YBImage *)imageWithContentsOfFile:(NSString *)path {
return [self imageWithContentsOfFile:path decodeDecision:nil];
}
+ (YBImage *)imageWithContentsOfFile:(NSString *)path decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
return [[self alloc] initWithContentsOfFile:path decodeDecision:decodeDecision];
}
+ (YBImage *)imageWithData:(NSData *)data {
return [self imageWithData:data decodeDecision:nil];
}
+ (YBImage *)imageWithData:(NSData *)data decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
return [[self alloc] initWithData:data decodeDecision:decodeDecision];
}
+ (YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale {
return [self imageWithData:data scale:scale decodeDecision:nil];
}
+ (YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
return [[self alloc] initWithData:data scale:scale decodeDecision:decodeDecision];
}
- (instancetype)initWithContentsOfFile:(NSString *)path {
return [self initWithContentsOfFile:path decodeDecision:nil];
}
- (instancetype)initWithContentsOfFile:(NSString *)path decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
NSData *data = [NSData dataWithContentsOfFile:path];
return [self initWithData:data scale:_NSStringPathScale(path) decodeDecision:decodeDecision];
}
- (instancetype)initWithData:(NSData *)data {
return [self initWithData:data decodeDecision:nil];
}
- (instancetype)initWithData:(NSData *)data decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
return [self initWithData:data scale:1 decodeDecision:decodeDecision];
}
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
return [self initWithData:data scale:scale decodeDecision:nil];
}
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
if (data.length == 0) return nil;
if (scale <= 0) scale = [UIScreen mainScreen].scale;
_preloadedLock = dispatch_semaphore_create(1);
@autoreleasepool {
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// 🙄Determine whether should to decode.
BOOL decodeForDisplay = YES;
if (decodeDecision) {
decodeForDisplay = decodeDecision(CGSizeMake(decoder.width, decoder.height), decoder.scale ?: 1);
}
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:decodeForDisplay];
UIImage *image = frame.image;
if (!image) return nil;
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
if (!self) return nil;
_animatedImageType = decoder.type;
if (decoder.frameCount > 1) {
_decoder = decoder;
_bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
_animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
self.yy_isDecodedForDisplay = YES;
}
return self;
}
- (NSData *)animatedImageData {
return _decoder.data;
}
- (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames {
if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) {
if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) {
NSMutableArray *frames = [NSMutableArray new];
for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) {
UIImage *img = [self animatedImageFrameAtIndex:i];
if (img) {
[frames addObject:img];
} else {
[frames addObject:[NSNull null]];
}
}
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
_preloadedFrames = frames;
dispatch_semaphore_signal(_preloadedLock);
} else {
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
_preloadedFrames = nil;
dispatch_semaphore_signal(_preloadedLock);
}
}
}
#pragma mark - protocol NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSNumber *scale = [aDecoder decodeObjectForKey:@"YYImageScale"];
NSData *data = [aDecoder decodeObjectForKey:@"YYImageData"];
if (data.length) {
self = [self initWithData:data scale:scale.doubleValue];
} else {
self = [super initWithCoder:aDecoder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
if (_decoder.data.length) {
[aCoder encodeObject:@(self.scale) forKey:@"YYImageScale"];
[aCoder encodeObject:_decoder.data forKey:@"YYImageData"];
} else {
[super encodeWithCoder:aCoder]; // Apple use UIImagePNGRepresentation() to encode UIImage.
}
}
#pragma mark - protocol YYAnimatedImage
- (NSUInteger)animatedImageFrameCount {
return _decoder.frameCount;
}
- (NSUInteger)animatedImageLoopCount {
return _decoder.loopCount;
}
- (NSUInteger)animatedImageBytesPerFrame {
return _bytesPerFrame;
}
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= _decoder.frameCount) return nil;
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
UIImage *image = _preloadedFrames[index];
dispatch_semaphore_signal(_preloadedLock);
if (image) return image == (id)[NSNull null] ? nil : image;
return [_decoder frameAtIndex:index decodeForDisplay:YES].image;
}
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
NSTimeInterval duration = [_decoder frameDurationAtIndex:index];
/*
http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
Many annoying ads specify a 0 duration to make an image flash as quickly as
possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
for any frames that specify a duration of <= 10 ms.
See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
*/
if (duration < 0.011f) return 0.100f;
return duration;
}
@end

View File

@@ -0,0 +1,43 @@
//
// YBIBCellProtocol.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/5.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBGetBaseInfoProtocol.h"
#import "YBIBOrientationReceiveProtocol.h"
#import "YBIBOperateBrowserProtocol.h"
@protocol YBIBDataProtocol;
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBCellProtocol <YBIBGetBaseInfoProtocol, YBIBOperateBrowserProtocol, YBIBOrientationReceiveProtocol>
@required
/// Cell 对应的 Data
@property (nonatomic, strong) id<YBIBDataProtocol> yb_cellData;
@optional
/**
获取前景视图,出入场时需要用这个返回值做动效
@return 前景视图
*/
- (__kindof UIView *)yb_foregroundView;
/**
页码变化了
*/
- (void)yb_pageChanged;
/// 当前 Cell 的页码
@property (nonatomic, copy) NSInteger(^yb_selfPage)(void);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,64 @@
//
// YBIBDataProtocol.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/5.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBGetBaseInfoProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBDataProtocol <YBIBGetBaseInfoProtocol>
@required
/**
当前 Data 对应 Cell 的类类型
@return Class 类型
*/
- (Class)yb_classOfCell;
@optional
/**
获取投影视图,当前数据模型对应外界业务的 UIView (通常为 UIImageView),做转场动效用
这个方法会在做出入场动效时调用,若未实现时将无法进行平滑的入场
@return 投影视图
*/
- (__kindof UIView *)yb_projectiveView;
/**
通过一系列数据,计算并返回图片视图在容器中的 frame
这个方法会在做入场动效时调用,若未实现时将无法进行平滑的入场
@param containerSize 容器大小
@param imageSize 图片大小 (逻辑像素)
@param orientation 图片浏览器的方向
@return 计算好的 frame
*/
- (CGRect)yb_imageViewFrameWithContainerSize:(CGSize)containerSize imageSize:(CGSize)imageSize orientation:(UIDeviceOrientation)orientation;
/**
预加载数据,有效的预加载能提高性能,请注意管理内存
*/
- (void)yb_preload;
/**
保存到相册
*/
- (void)yb_saveToPhotoAlbum;
/**
是否允许保存到相册
*/
- (BOOL)yb_allowSaveToPhotoAlbum;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,61 @@
//
// YBIBGetBaseInfoProtocol.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/23.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "YBIBAuxiliaryViewHandler.h"
#import "YBIBWebImageMediator.h"
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBGetBaseInfoProtocol <NSObject>
@optional
/// 当前的方向
@property (nonatomic, copy) UIDeviceOrientation(^yb_currentOrientation)(void);
/// 根据方向获取容器大小
@property (nonatomic, copy) CGSize(^yb_containerSize)(UIDeviceOrientation orientation);
/// 容器视图 (可在上面添加子视图)
@property (nonatomic, weak) UIView *yb_containerView;
/// 辅助视图处理器
@property (nonatomic, copy) id<YBIBAuxiliaryViewHandler>(^yb_auxiliaryViewHandler)(void);
/// 图片下载缓存相关中介者
@property (nonatomic, copy) id<YBIBWebImageMediator>(^yb_webImageMediator)(void);
/// 当前页码
@property (nonatomic, copy) NSInteger(^yb_currentPage)(void);
/// 总页码数量
@property (nonatomic, copy) NSInteger(^yb_totalPage)(void);
/// 判断当前展示的 cell 是否恰好在屏幕中间
@property (nonatomic, copy) BOOL(^yb_cellIsInCenter)(void);
/// 是否正在转场
@property (nonatomic, copy) BOOL(^yb_isTransitioning)(void);
/// 是否正在展示过程转场
@property (nonatomic, copy) BOOL(^yb_isShowTransitioning)(void);
/// 是否正在隐藏过程转场
@property (nonatomic, copy) BOOL(^yb_isHideTransitioning)(void);
/// 是否正在旋转
@property (nonatomic, copy) BOOL(^yb_isRotating)(void);
/// 背景视图 (也就是 YBImageBrower 对象,不可在上面添加子视图。作用:一是可以更改透明度和颜色,入场和出场动效有用;二是可以用来比较内存指针,在做不同实例差异化功能时可能有用,虽然不提倡这么做)
@property (nonatomic, weak) __kindof UIView *yb_backView;
/// 集合视图
@property (nonatomic, copy) __kindof UICollectionView *(^yb_collectionView)(void);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,28 @@
//
// YBIBOperateBrowserProtocol.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/8.
// Copyright © 2019 杨波. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBOperateBrowserProtocol <NSObject>
@optional
/// 隐藏图片浏览器
@property (nonatomic, copy) void(^yb_hideBrowser)(void);
/// 是否隐藏状态栏
@property (nonatomic, copy) void(^yb_hideStatusBar)(BOOL);
/// 是否隐藏工具视图
@property (nonatomic, copy) void(^yb_hideToolViews)(BOOL);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
//
// YBIBOrientationReceiveProtocol.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/8.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBOrientationReceiveProtocol <NSObject>
@optional
/**
图片浏览器的方向将要变化
@param orientation 期望的方向
*/
- (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation;
/**
图片浏览器的方向变化动效调用,实现的变化会自动转换为动画
@param orientation 期望的方向
*/
- (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation;
/**
图片浏览器的方向已经变化
@param orientation 当前的方向
*/
- (void)yb_orientationDidChangedWithOrientation:(UIDeviceOrientation)orientation;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,38 @@
//
// YBImageBrowserDataSource.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2018/8/25.
// Copyright © 2018年 波儿菜. All rights reserved.
//
#import "YBIBDataProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class YBImageBrowser;
@protocol YBImageBrowserDataSource <NSObject>
@required
/**
返回数据源数量
@param imageBrowser 图片浏览器
@return 数量
*/
- (NSInteger)yb_numberOfCellsInImageBrowser:(YBImageBrowser *)imageBrowser;
/**
返回当前下标对应的数据
@param imageBrowser 图片浏览器
@param index 当前下标
@return 数据
*/
- (id<YBIBDataProtocol>)yb_imageBrowser:(YBImageBrowser *)imageBrowser dataForCellAtIndex:(NSInteger)index;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,54 @@
//
// YBImageBrowserDelegate.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/9.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBDataProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class YBImageBrowser;
@protocol YBImageBrowserDelegate <NSObject>
@optional
/**
页码变化
@param imageBrowser 图片浏览器
@param page 当前页码
@param data 数据
*/
- (void)yb_imageBrowser:(YBImageBrowser *)imageBrowser pageChanged:(NSInteger)page data:(id<YBIBDataProtocol>)data;
/**
响应长按手势(若实现该方法将阻止其它地方捕获到长按事件)
@param imageBrowser 图片浏览器
@param data 数据
*/
- (void)yb_imageBrowser:(YBImageBrowser *)imageBrowser respondsToLongPressWithData:(id<YBIBDataProtocol>)data;
/**
开始转场
@param imageBrowser 图片浏览器
@param isShow YES 表示入场NO 表示出场
*/
- (void)yb_imageBrowser:(YBImageBrowser *)imageBrowser beginTransitioningWithIsShow:(BOOL)isShow;
/**
结束转场
@param imageBrowser 图片浏览器
@param isShow YES 表示入场NO 表示出场
*/
- (void)yb_imageBrowser:(YBImageBrowser *)imageBrowser endTransitioningWithIsShow:(BOOL)isShow;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,73 @@
//
// YBIBSheetView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/6.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBDataProtocol;
typedef void(^YBIBSheetActionBlock)(id<YBIBDataProtocol> data);
@interface YBIBSheetAction : NSObject
/// 显示的名字
@property (nonatomic, copy) NSString *name;
/// 点击回调闭包
@property (nonatomic, copy, nullable) YBIBSheetActionBlock action;
+ (instancetype)actionWithName:(NSString *)name action:(_Nullable YBIBSheetActionBlock)action;
@end
@interface YBIBSheetView : UIView
/// 数据源 (可自定义添加)
@property (nonatomic, strong) NSMutableArray<YBIBSheetAction *> *actions;
/// 列表 Cell 的高度
@property (nonatomic, assign) CGFloat cellHeight;
/// 列表最大高度与容器高度的比例
@property (nonatomic, assign) CGFloat maxHeightScale;
/// 取消的文本
@property (nonatomic, copy) NSString *cancelText;
/// 显示动画持续时间
@property (nonatomic, assign) NSTimeInterval showDuration;
/// 隐藏动画持续时间
@property (nonatomic, assign) NSTimeInterval hideDuration;
/// 背景透明度
@property (nonatomic, assign) CGFloat backAlpha;
/**
展示
@param view 指定父视图
@param orientation 当前方向
*/
- (void)showToView:(UIView *)view orientation:(UIDeviceOrientation)orientation;
/**
隐藏
@param animation 是否带动画
*/
- (void)hideWithAnimation:(BOOL)animation;
/// 获取当前数据的闭包
@property (nonatomic, copy) id<YBIBDataProtocol>(^currentdata)(void);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,209 @@
//
// YBIBSheetView.m
// YBImageBrowserDemo
//
// Created by on 2019/7/6.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBSheetView.h"
#import "YBIBUtilities.h"
#import "YBIBCopywriter.h"
@implementation YBIBSheetAction
+ (instancetype)actionWithName:(NSString *)name action:(YBIBSheetActionBlock)action {
YBIBSheetAction *sheetAction = [YBIBSheetAction new];
sheetAction.name = name;
sheetAction.action = action;
return sheetAction;
}
@end
@interface YBIBSheetCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) CALayer *line;
@end
@implementation YBIBSheetCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
_titleLabel = [UILabel new];
_titleLabel.textColor = UIColor.darkTextColor;
_titleLabel.font = [UIFont fontWithName:@"Avenir-Medium" size:16];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_line = [CALayer new];
_line.backgroundColor = UIColor.groupTableViewBackgroundColor.CGColor;
[self.contentView addSubview:_titleLabel];
[self.contentView.layer addSublayer:_line];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.bounds.size.width, height = self.contentView.bounds.size.height;
CGFloat lineHeight = 0.5;
_line.frame = CGRectMake(0, height - lineHeight, width, lineHeight);
CGFloat offset = 15;
_titleLabel.frame = CGRectMake(offset, 0, width - offset * 2, height);
}
@end
static CGFloat kOffsetSpace = 5;
@interface YBIBSheetView () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation YBIBSheetView {
CGRect _tableShowFrame;
CGRect _tableHideFrame;
}
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_cancelText = [YBIBCopywriter sharedCopywriter].cancel;
_maxHeightScale = 0.7;
_showDuration = 0.2;
_hideDuration = 0.1;
_cellHeight = 50;
_backAlpha = 0.3;
_actions = [NSMutableArray array];
[self addSubview:self.tableView];
}
return self;
}
#pragma mark - public
- (void)showToView:(UIView *)view orientation:(UIDeviceOrientation)orientation {
if (self.actions.count == 0) return;
[view addSubview:self];
self.frame = view.bounds;
UIEdgeInsets padding = YBIBPaddingByBrowserOrientation(orientation);
CGFloat footerHeight = padding.bottom;
CGFloat tableHeight = self.cellHeight * (self.actions.count + 1) + kOffsetSpace + footerHeight;
_tableShowFrame = self.frame;
_tableShowFrame.size.height = MIN(self.maxHeightScale * self.bounds.size.height, tableHeight);
_tableShowFrame.origin.y = self.bounds.size.height - _tableShowFrame.size.height;
_tableHideFrame = _tableShowFrame;
_tableHideFrame.origin.y = self.bounds.size.height;
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0];
self.tableView.frame = _tableHideFrame;
self.tableView.tableFooterView.bounds = CGRectMake(0, 0, self.tableView.frame.size.width, footerHeight);
[UIView animateWithDuration:self.showDuration animations:^{
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:self->_backAlpha];
self.tableView.frame = self->_tableShowFrame;
}];
}
- (void)hideWithAnimation:(BOOL)animation {
if (!self.superview) return;
void(^animationsBlock)(void) = ^{
self.tableView.frame = self->_tableHideFrame;
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0];
};
void(^completionBlock)(BOOL n) = ^(BOOL n){
[self removeFromSuperview];
};
if (animation) {
[UIView animateWithDuration:self.hideDuration animations:animationsBlock completion:completionBlock];
} else {
animationsBlock();
completionBlock(NO);
}
}
#pragma mark - touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint point = [touches.anyObject locationInView:self];
if (!CGRectContainsPoint(self.tableView.frame, point)) {
[self hideWithAnimation:YES];
}
}
#pragma mark - <UITableViewDataSource>
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return section == 0 ? self.actions.count : 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.cellHeight;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return section == 0 ? CGFLOAT_MIN : kOffsetSpace;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return CGFLOAT_MIN;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YBIBSheetCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(YBIBSheetCell.self)];
if (indexPath.section == 0) {
cell.line.hidden = NO;
YBIBSheetAction *action = self.actions[indexPath.row];
cell.titleLabel.text = action.name;
} else {
cell.line.hidden = YES;
cell.titleLabel.text = self.cancelText;
}
return cell;
}
#pragma mark - <UITableViewDelegate>
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
YBIBSheetAction *action = self.actions[indexPath.row];
if (action.action) action.action(self.currentdata());
} else {
[self hideWithAnimation:YES];
}
}
#pragma mark - getters
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.estimatedRowHeight = 44;
_tableView.estimatedSectionFooterHeight = 0;
_tableView.estimatedSectionHeaderHeight = 0;
_tableView.backgroundColor = [UIColor clearColor];
_tableView.alwaysBounceVertical = NO;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
UIView *footer = [UIView new];
footer.backgroundColor = UIColor.whiteColor;
_tableView.tableFooterView = footer;
[_tableView registerClass:YBIBSheetCell.self forCellReuseIdentifier:NSStringFromClass(YBIBSheetCell.self)];
}
return _tableView;
}
@end

View File

@@ -0,0 +1,67 @@
//
// YBIBToolViewHandler.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/7.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBSheetView.h"
#import "YBIBTopView.h"
#import "YBIBDataProtocol.h"
#import "YBIBOrientationReceiveProtocol.h"
#import "YBIBOperateBrowserProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@protocol YBIBToolViewHandler <YBIBGetBaseInfoProtocol, YBIBOperateBrowserProtocol, YBIBOrientationReceiveProtocol>
@required
/**
容器视图准备好了,可进行子视图的添加和布局
*/
- (void)yb_containerViewIsReadied;
/**
隐藏视图
@param hide 是否隐藏
*/
- (void)yb_hide:(BOOL)hide;
@optional
/// 当前数据
@property (nonatomic, copy) id<YBIBDataProtocol>(^yb_currentData)(void);
/**
页码变化了
*/
- (void)yb_pageChanged;
/**
偏移量变化了
@param offsetX 当前偏移量
*/
- (void)yb_offsetXChanged:(CGFloat)offsetX;
/**
响应长按手势
*/
- (void)yb_respondsToLongPress;
@end
@interface YBIBToolViewHandler : NSObject <YBIBToolViewHandler>
/// 弹出表单视图
@property (nonatomic, strong, readonly) YBIBSheetView *sheetView;
/// 顶部显示页码视图
@property (nonatomic, strong, readonly) YBIBTopView *topView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,144 @@
//
// YBIBToolViewHandler.m
// YBImageBrowserDemo
//
// Created by on 2019/7/7.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBToolViewHandler.h"
#import "YBIBCopywriter.h"
#import "YBIBUtilities.h"
@interface YBIBToolViewHandler ()
@property (nonatomic, strong) YBIBSheetView *sheetView;
@property (nonatomic, strong) YBIBSheetAction *saveAction;
@property (nonatomic, strong) YBIBTopView *topView;
@end
@implementation YBIBToolViewHandler
#pragma mark - <YBIBToolViewHandler>
@synthesize yb_containerView = _yb_containerView;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_currentPage = _yb_currentPage;
@synthesize yb_totalPage = _yb_totalPage;
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_currentData = _yb_currentData;
- (void)yb_containerViewIsReadied {
[self.yb_containerView addSubview:self.topView];
[self layoutWithExpectOrientation:self.yb_currentOrientation()];
}
- (void)yb_pageChanged {
if (self.topView.operationType == YBIBTopViewOperationTypeSave) {
self.topView.operationButton.hidden = [self currentDataShouldHideSaveButton];
}
[self.topView setPage:self.yb_currentPage() totalPage:self.yb_totalPage()];
}
- (void)yb_respondsToLongPress {
[self showSheetView];
}
- (void)yb_hide:(BOOL)hide {
self.topView.hidden = hide;
[self.sheetView hideWithAnimation:NO];
}
- (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation {
[self.sheetView hideWithAnimation:NO];
}
- (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation {
[self layoutWithExpectOrientation:orientation];
}
#pragma mark - private
- (BOOL)currentDataShouldHideSaveButton {
id<YBIBDataProtocol> data = self.yb_currentData();
BOOL allow = [data respondsToSelector:@selector(yb_allowSaveToPhotoAlbum)] && [data yb_allowSaveToPhotoAlbum];
BOOL can = [data respondsToSelector:@selector(yb_saveToPhotoAlbum)];
return !(allow && can);
}
- (void)layoutWithExpectOrientation:(UIDeviceOrientation)orientation {
CGSize containerSize = self.yb_containerSize(orientation);
UIEdgeInsets padding = YBIBPaddingByBrowserOrientation(orientation);
self.topView.frame = CGRectMake(padding.left, padding.top, containerSize.width - padding.left - padding.right, [YBIBTopView defaultHeight]);
}
- (void)showSheetView {
if ([self currentDataShouldHideSaveButton]) {
[self.sheetView.actions removeObject:self.saveAction];
} else {
if (![self.sheetView.actions containsObject:self.saveAction]) {
[self.sheetView.actions addObject:self.saveAction];
}
}
[self.sheetView showToView:self.yb_containerView orientation:self.yb_currentOrientation()];
}
#pragma mark - getters
- (YBIBSheetView *)sheetView {
if (!_sheetView) {
_sheetView = [YBIBSheetView new];
__weak typeof(self) wSelf = self;
[_sheetView setCurrentdata:^id<YBIBDataProtocol>{
__strong typeof(wSelf) self = wSelf;
if (!self) return nil;
return self.yb_currentData();
}];
}
return _sheetView;
}
- (YBIBSheetAction *)saveAction {
if (!_saveAction) {
__weak typeof(self) wSelf = self;
_saveAction = [YBIBSheetAction actionWithName:[YBIBCopywriter sharedCopywriter].saveToPhotoAlbum action:^(id<YBIBDataProtocol> data) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
if ([data respondsToSelector:@selector(yb_saveToPhotoAlbum)]) {
[data yb_saveToPhotoAlbum];
}
[self.sheetView hideWithAnimation:YES];
}];
}
return _saveAction;
}
- (YBIBTopView *)topView {
if (!_topView) {
_topView = [YBIBTopView new];
_topView.operationType = YBIBTopViewOperationTypeMore;
__weak typeof(self) wSelf = self;
[_topView setClickOperation:^(YBIBTopViewOperationType type) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
switch (type) {
case YBIBTopViewOperationTypeSave: {
id<YBIBDataProtocol> data = self.yb_currentData();
if ([data respondsToSelector:@selector(yb_saveToPhotoAlbum)]) {
[data yb_saveToPhotoAlbum];
}
}
break;
case YBIBTopViewOperationTypeMore: {
[self showSheetView];
}
break;
default:
break;
}
}];
}
return _topView;
}
@end

View File

@@ -0,0 +1,44 @@
//
// YBIBTopView.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/6.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, YBIBTopViewOperationType) {
YBIBTopViewOperationTypeSave, //保存
YBIBTopViewOperationTypeMore //更多
};
@interface YBIBTopView : UIView
/// 页码标签
@property (nonatomic, strong, readonly) UILabel *pageLabel;
/// 操作按钮(自定义:直接修改图片或文字,然后添加点击事件)
@property (nonatomic, strong, readonly) UIButton *operationButton;
/// 按钮类型
@property (nonatomic, assign) YBIBTopViewOperationType operationType;
/**
设置页码
@param page 当前页码
@param totalPage 总页码数
*/
- (void)setPage:(NSInteger)page totalPage:(NSInteger)totalPage;
/// 点击操作按钮的回调
@property (nonatomic, copy) void(^clickOperation)(YBIBTopViewOperationType type);
+ (CGFloat)defaultHeight;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,113 @@
//
// YBIBTopView.m
// YBImageBrowserDemo
//
// Created by on 2019/7/6.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBTopView.h"
#import "YBIBIconManager.h"
#import "YBIBUtilities.h"
@interface YBIBTopView ()
@property (nonatomic, strong) UILabel *pageLabel;
@property (nonatomic, strong) UIButton *operationButton;
@end
@implementation YBIBTopView
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.pageLabel];
[self addSubview:self.operationButton];
[self setOperationType:YBIBTopViewOperationTypeMore];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat height = self.bounds.size.height, width = self.bounds.size.width;
self.pageLabel.frame = CGRectMake(16, 0, width / 3, height);
CGFloat buttonWidth = 54;
self.operationButton.frame = CGRectMake(width - buttonWidth, 0, buttonWidth, height);
}
#pragma mark - public
+ (CGFloat)defaultHeight {
return 50;
}
- (void)setPage:(NSInteger)page totalPage:(NSInteger)totalPage {
if (totalPage <= 1) {
self.pageLabel.hidden = YES;
} else {
self.pageLabel.hidden = NO;
NSString *text = [NSString stringWithFormat:@"%ld/%ld", page + (NSInteger)1, totalPage];
NSShadow *shadow = [NSShadow new];
shadow.shadowBlurRadius = 4;
shadow.shadowOffset = CGSizeMake(0, 1);
shadow.shadowColor = UIColor.darkGrayColor;
NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:text attributes:@{NSShadowAttributeName:shadow}];
self.pageLabel.attributedText = attr;
}
}
#pragma mark - event
- (void)clickOperationButton:(UIButton *)button {
if (self.clickOperation) self.clickOperation(self.operationType);
}
#pragma mark - getters & setters
- (void)setOperationType:(YBIBTopViewOperationType)operationType {
_operationType = operationType;
UIImage *image = nil;
switch (operationType) {
case YBIBTopViewOperationTypeSave:
image = [YBIBIconManager sharedManager].toolSaveImage();
break;
case YBIBTopViewOperationTypeMore:
image = [YBIBIconManager sharedManager].toolMoreImage();
break;
}
[self.operationButton setImage:image forState:UIControlStateNormal];
}
- (UILabel *)pageLabel {
if (!_pageLabel) {
_pageLabel = [UILabel new];
_pageLabel.textColor = [UIColor whiteColor];
_pageLabel.font = [UIFont boldSystemFontOfSize:16];
_pageLabel.textAlignment = NSTextAlignmentLeft;
_pageLabel.adjustsFontSizeToFitWidth = YES;
}
return _pageLabel;
}
- (UIButton *)operationButton {
if (!_operationButton) {
_operationButton = [UIButton buttonWithType:UIButtonTypeCustom];
_operationButton.titleLabel.font = [UIFont boldSystemFontOfSize:16];
_operationButton.titleLabel.adjustsFontSizeToFitWidth = YES;
[_operationButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_operationButton addTarget:self action:@selector(clickOperationButton:) forControlEvents:UIControlEventTouchUpInside];
_operationButton.layer.shadowColor = UIColor.darkGrayColor.CGColor;
_operationButton.layer.shadowOffset = CGSizeMake(0, 1);
_operationButton.layer.shadowOpacity = 1;
_operationButton.layer.shadowRadius = 4;
}
return _operationButton;
}
@end

View File

@@ -0,0 +1,17 @@
//
// YBIBDefaultWebImageMediator.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/8/27.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBWebImageMediator.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBIBDefaultWebImageMediator : NSObject <YBIBWebImageMediator>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,74 @@
//
// YBIBDefaultWebImageMediator.m
// YBImageBrowserDemo
//
// Created by on 2019/8/27.
// Copyright © 2019 . All rights reserved.
//
#import "YBIBDefaultWebImageMediator.h"
#import "YBIBUtilities.h"
#if __has_include(<SDWebImage/SDWebImage.h>)
#import <SDWebImage/SDWebImage.h>
#else
#import "SDWebImage.h"
#endif
@implementation YBIBDefaultWebImageMediator
#pragma mark - <YBIBWebImageMediator>
- (id)yb_downloadImageWithURL:(NSURL *)URL requestModifier:(nullable YBIBWebImageRequestModifierBlock)requestModifier progress:(nonnull YBIBWebImageProgressBlock)progress success:(nonnull YBIBWebImageSuccessBlock)success failed:(nonnull YBIBWebImageFailedBlock)failed {
if (!URL) return nil;
SDWebImageContext *context = nil;
if (requestModifier) {
SDWebImageDownloaderRequestModifier *modifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock:requestModifier];
context = @{SDWebImageContextDownloadRequestModifier:modifier};
}
SDWebImageDownloaderOptions options = SDWebImageDownloaderLowPriority | SDWebImageDownloaderAvoidDecodeImage;
SDWebImageDownloadToken *token = [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:URL options:options context:context progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (progress) progress(receivedSize, expectedSize);
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (error) {
if (failed) failed(error, finished);
} else {
if (success) success(data, finished);
}
}];
return token;
}
- (void)yb_cancelTaskWithDownloadToken:(id)token {
if (token && [token isKindOfClass:SDWebImageDownloadToken.class]) {
[((SDWebImageDownloadToken *)token) cancel];
}
}
- (void)yb_storeToDiskWithImageData:(NSData *)data forKey:(NSURL *)key {
if (!key) return;
NSString *cacheKey = [SDWebImageManager.sharedManager cacheKeyForURL:key];
if (!cacheKey) return;
YBIB_DISPATCH_ASYNC(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[[SDImageCache sharedImageCache] storeImageDataToDisk:data forKey:cacheKey];
})
}
- (void)yb_queryCacheOperationForKey:(NSURL *)key completed:(YBIBWebImageCacheQueryCompletedBlock)completed {
#define QUERY_CACHE_FAILED if (completed) {completed(nil, nil); return;}
if (!key) QUERY_CACHE_FAILED
NSString *cacheKey = [SDWebImageManager.sharedManager cacheKeyForURL:key];
if (!cacheKey) QUERY_CACHE_FAILED
#undef QUERY_CACHE_FAILED
// 'NSData' of image must be read to ensure decoding correctly.
SDImageCacheOptions options = SDImageCacheQueryMemoryData | SDImageCacheAvoidDecodeImage;
[[SDImageCache sharedImageCache] queryCacheOperationForKey:cacheKey options:options done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
if (completed) completed(image, data);
}];
}
@end

View File

@@ -0,0 +1,62 @@
//
// YBIBWebImageMediator.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/8/27.
// Copyright © 2019 杨波. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NSURLRequest * _Nullable (^YBIBWebImageRequestModifierBlock)(NSURLRequest *request);
typedef void(^YBIBWebImageProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
typedef void(^YBIBWebImageSuccessBlock)(NSData * _Nullable imageData, BOOL finished);
typedef void(^YBIBWebImageFailedBlock)(NSError * _Nullable error, BOOL finished);
typedef void(^YBIBWebImageCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);
@protocol YBIBWebImageMediator <NSObject>
@required
/**
下载图片
@param URL 图片地址
@param requestModifier 修改默认 NSURLRequest 的闭包
@param progress 进度回调
@param success 成功回调
@param failed 失败回调
@return 下载 token (可为空)
*/
- (id)yb_downloadImageWithURL:(NSURL *)URL requestModifier:(nullable YBIBWebImageRequestModifierBlock)requestModifier progress:(YBIBWebImageProgressBlock)progress success:(YBIBWebImageSuccessBlock)success failed:(YBIBWebImageFailedBlock)failed;
/**
缓存图片数据到磁盘
@param data 图片数据
@param key 缓存标识
*/
- (void)yb_storeToDiskWithImageData:(nullable NSData *)data forKey:(NSURL *)key;
/**
读取图片数据
@param key 缓存标识
@param completed 读取回调
*/
- (void)yb_queryCacheOperationForKey:(NSURL *)key completed:(YBIBWebImageCacheQueryCompletedBlock)completed;
@optional
/**
取消下载
@param token 下载 token
*/
- (void)yb_cancelTaskWithDownloadToken:(id)token;
@end
NS_ASSUME_NONNULL_END

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

View File

@@ -0,0 +1,111 @@
//
// YBImageBrowser.h
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/6/5.
// Copyright © 2019 波儿菜. All rights reserved.
//
#import "YBIBCollectionView.h"
#import "YBImageBrowserDataSource.h"
#import "YBImageBrowserDelegate.h"
#import "YBIBDataProtocol.h"
#import "YBIBCellProtocol.h"
#import "YBIBAnimatedTransition.h"
#import "YBIBAuxiliaryViewHandler.h"
#import "YBIBToolViewHandler.h"
#import "YBIBWebImageMediator.h"
#import "YBIBImageData.h"
NS_ASSUME_NONNULL_BEGIN
@interface YBImageBrowser : UIView
/// 数据源数组
@property (nonatomic, copy) NSArray<id<YBIBDataProtocol>> *dataSourceArray;
/// 数据源代理
@property (nonatomic, weak) id<YBImageBrowserDataSource> dataSource;
/// 状态回调代理
@property (nonatomic, weak) id<YBImageBrowserDelegate> delegate;
/**
展示图片浏览器
@param view 指定父视图view 的大小不能为 CGSizeZero但允许变化
@param containerSize 容器大小(当 view 的大小允许变化时,必须指定确切的 containerSize
*/
- (void)showToView:(UIView *)view containerSize:(CGSize)containerSize;
- (void)showToView:(UIView *)view;
- (void)show;
/**
隐藏图片浏览器(不建议外部持有图片浏览器重复使用)
*/
- (void)hide;
/// 当前页码
@property (nonatomic, assign) NSInteger currentPage;
/// 分页间距
@property (nonatomic, assign) CGFloat distanceBetweenPages;
/// 当前图片浏览器的方向
@property (nonatomic, assign, readonly) UIDeviceOrientation currentOrientation;
/// 图片浏览器支持的方向 (仅当前控制器不支持旋转时有效,否则将跟随控制器旋转)
@property (nonatomic, assign) UIInterfaceOrientationMask supportedOrientations;
/// 是否自动隐藏 id<YBIBImageData> 设置的映射视图,默认为 YES
@property (nonatomic, assign) BOOL autoHideProjectiveView;
/// 是否正在转场
@property (nonatomic, assign, readonly, getter=isTransitioning) BOOL transitioning;
/// 是否正在进行展示过程转场
@property (nonatomic, assign, readonly, getter=isShowTransitioning) BOOL showTransitioning;
/// 是否正在进行隐藏过程转场
@property (nonatomic, assign, readonly, getter=isHideTransitioning) BOOL hideTransitioning;
/// 预加载数量 (默认为 2低内存设备默认为 0)
@property (nonatomic, assign) NSUInteger preloadCount;
/**
重载数据,请保证数据源被正确修改
*/
- (void)reloadData;
/**
获取当前展示的数据对象
@return 数据对象
*/
- (id<YBIBDataProtocol>)currentData;
/// 是否隐藏状态栏,默认为 YES该值为 YES 时需要在 info.plist 中添加 View controller-based status bar appearance : NO 才能生效)
@property (nonatomic, assign) BOOL shouldHideStatusBar;
/// 工具视图处理器
/// 赋值可自定义,实现者可以直接用 UIView或者创建一个中介者管理一系列的 UIView。
/// 内部消息是按照数组下标顺序调度的,所以如果有多个处理器注意添加 UIView 的视图层级。
@property (nonatomic, copy) NSArray<id<YBIBToolViewHandler>> *toolViewHandlers;
/// 默认工具视图处理器
@property (nonatomic, weak, readonly) YBIBToolViewHandler *defaultToolViewHandler;
/// Toast/Loading 处理器 (赋值可自定义)
@property (nonatomic, strong) id<YBIBAuxiliaryViewHandler> auxiliaryViewHandler;
/// 转场实现类 (赋值可自定义)
@property (nonatomic, strong) id<YBIBAnimatedTransition> animatedTransition;
/// 默认转场实现类 (可配置其属性)
@property (nonatomic, weak, readonly) YBIBAnimatedTransition *defaultAnimatedTransition;
/// 图片下载缓存相关的中介者(赋值可自定义)
@property (nonatomic, strong) id<YBIBWebImageMediator> webImageMediator;
/// 核心集合视图
@property (nonatomic, strong, readonly) YBIBCollectionView *collectionView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,538 @@
//
// YBImageBrowser.m
// YBImageBrowserDemo
//
// Created by on 2019/6/5.
// Copyright © 2019 . All rights reserved.
//
#import "YBImageBrowser.h"
#import "YBIBUtilities.h"
#import "YBIBCellProtocol.h"
#import "YBIBDataMediator.h"
#import "YBIBScreenRotationHandler.h"
#import "NSObject+YBImageBrowser.h"
#import "YBImageBrowser+Internal.h"
#if __has_include("YBIBDefaultWebImageMediator.h")
#import "YBIBDefaultWebImageMediator.h"
#endif
@interface YBImageBrowser () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) YBIBCollectionView *collectionView;
@property (nonatomic, strong) YBIBDataMediator *dataMediator;
@property (nonatomic, strong) YBIBScreenRotationHandler *rotationHandler;
@end
@implementation YBImageBrowser {
BOOL _originStatusBarHidden;
}
#pragma mark - life cycle
- (void)dealloc {
self.hiddenProjectiveView = nil;
[self showStatusBar];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.blackColor;
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToLongPress:)];
[self addGestureRecognizer:longPress];
[self initValue];
}
return self;
}
- (void)initValue {
_transitioning = _showTransitioning = _hideTransitioning = NO;
_defaultAnimatedTransition = _animatedTransition = [YBIBAnimatedTransition new];
_toolViewHandlers = @[[YBIBToolViewHandler new]];
_defaultToolViewHandler = _toolViewHandlers[0];
_auxiliaryViewHandler = [YBIBAuxiliaryViewHandler new];
_shouldHideStatusBar = YES;
_autoHideProjectiveView = YES;
#if __has_include("YBIBDefaultWebImageMediator.h")
_webImageMediator = [YBIBDefaultWebImageMediator new];
#endif
}
#pragma mark - private
- (void)build {
[self addSubview:self.collectionView];
self.collectionView.frame = self.bounds;
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:self.containerView];
self.containerView.frame = self.bounds;
self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self buildToolView];
[self layoutIfNeeded];
[self collectionViewScrollToPage:self.currentPage];
[self.rotationHandler startObserveDeviceOrientation];
}
- (void)buildToolView {
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
[self implementGetBaseInfoProtocol:handler];
[self implementOperateBrowserProtocol:handler];
__weak typeof(self) wSelf = self;
if ([handler respondsToSelector:@selector(setYb_currentData:)]) {
[handler setYb_currentData:^id<YBIBDataProtocol>{
__strong typeof(wSelf) self = wSelf;
if (!self) return nil;
return self.currentData;
}];
}
[handler yb_containerViewIsReadied];
[handler yb_hide:NO];
}
}
- (void)rebuild {
self.hiddenProjectiveView = nil;
[self showStatusBar];
[self.containerView removeFromSuperview];
_containerView = nil;
[self.collectionView removeFromSuperview];
_collectionView = nil;
[self.dataMediator clear];
[self.rotationHandler clear];
}
- (void)collectionViewScrollToPage:(NSInteger)page {
[self.collectionView scrollToPage:page];
[self pageNumberChanged];
}
- (void)pageNumberChanged {
id<YBIBDataProtocol> data = self.currentData;
UIView *projectiveView = nil;
if ([data respondsToSelector:@selector(yb_projectiveView)]) {
projectiveView = [data yb_projectiveView];
}
self.hiddenProjectiveView = projectiveView;
if (self.delegate && [self.delegate respondsToSelector:@selector(yb_imageBrowser:pageChanged:data:)]) {
[self.delegate yb_imageBrowser:self pageChanged:self.currentPage data:data];
}
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_pageChanged)]) {
[handler yb_pageChanged];
}
}
NSArray *visibleCells = self.collectionView.visibleCells;
for (UICollectionViewCell<YBIBCellProtocol> *cell in visibleCells) {
if ([cell respondsToSelector:@selector(yb_pageChanged)]) {
[cell yb_pageChanged];
}
}
}
- (void)showStatusBar {
if (self.shouldHideStatusBar) {
[UIApplication sharedApplication].statusBarHidden = _originStatusBarHidden;
}
}
- (void)hideStatusBar {
if (self.shouldHideStatusBar) {
[UIApplication sharedApplication].statusBarHidden = YES;
}
}
#pragma mark - public
- (void)show {
[self showToView:[UIApplication sharedApplication].keyWindow];
}
- (void)showToView:(UIView *)view {
[self showToView:view containerSize:view.bounds.size];
}
- (void)showToView:(UIView *)view containerSize:(CGSize)containerSize {
[self.rotationHandler startObserveStatusBarOrientation];
[view addSubview:self];
self.frame = view.bounds;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_originStatusBarHidden = [UIApplication sharedApplication].isStatusBarHidden;
[self.rotationHandler configContainerSize:containerSize];
[self.dataMediator preloadWithPage:self.currentPage];
__kindof UIView *startView;
UIImage *startImage;
CGRect endFrame = CGRectZero;
id<YBIBDataProtocol> data = [self.dataMediator dataForCellAtIndex:self.currentPage];
if ([data respondsToSelector:@selector(yb_projectiveView)]) {
startView = data.yb_projectiveView;
self.hiddenProjectiveView = startView;
if ([startView isKindOfClass:UIImageView.class]) {
startImage = ((UIImageView *)startView).image;
} else {
startImage = YBIBSnapshotView(startView);
}
}
if ([data respondsToSelector:@selector(yb_imageViewFrameWithContainerSize:imageSize:orientation:)]) {
endFrame = [data yb_imageViewFrameWithContainerSize:self.bounds.size imageSize:startImage.size orientation:self.rotationHandler.currentOrientation];
}
[self setTransitioning:YES isShow:YES];
[self.animatedTransition yb_showTransitioningWithContainer:self startView:startView startImage:startImage endFrame:endFrame orientation:self.rotationHandler.currentOrientation completion:^{
[self hideStatusBar];
[self build];
[self setTransitioning:NO isShow:YES];
}];
}
- (void)hide {
__kindof UIView *startView;
__kindof UIView *endView;
UICollectionViewCell<YBIBCellProtocol> *cell = (UICollectionViewCell<YBIBCellProtocol> *)self.collectionView.centerCell;
if ([cell respondsToSelector:@selector(yb_foregroundView)]) {
startView = cell.yb_foregroundView;
}
if ([cell.yb_cellData respondsToSelector:@selector(yb_projectiveView)]) {
endView = cell.yb_cellData.yb_projectiveView;
}
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
[handler yb_hide:YES];
}
[self showStatusBar];
[self setTransitioning:YES isShow:NO];
[self.animatedTransition yb_hideTransitioningWithContainer:self startView:startView endView:endView orientation:self.rotationHandler.currentOrientation completion:^{
[self rebuild];
[self removeFromSuperview];
[self setTransitioning:NO isShow:NO];
}];
}
- (void)reloadData {
[self.dataMediator clear];
NSInteger page = self.currentPage;
[self.collectionView reloadData];
self.currentPage = page;
}
- (id<YBIBDataProtocol>)currentData {
return [self.dataMediator dataForCellAtIndex:self.currentPage];
}
#pragma mark - internal
- (void)setHiddenProjectiveView:(NSObject *)hiddenProjectiveView {
if (_hiddenProjectiveView && [_hiddenProjectiveView respondsToSelector:@selector(setAlpha:)]) {
CGFloat originAlpha = _hiddenProjectiveView.ybib_originAlpha;
if (originAlpha != 1) {
[_hiddenProjectiveView setValue:@(1) forKey:@"alpha"];
[UIView animateWithDuration:0.2 animations:^{
[self->_hiddenProjectiveView setValue:@(originAlpha) forKey:@"alpha"];
}];
} else {
[_hiddenProjectiveView setValue:@(originAlpha) forKey:@"alpha"];
}
}
_hiddenProjectiveView = hiddenProjectiveView;
if (!self.autoHideProjectiveView) return;
if (hiddenProjectiveView && [hiddenProjectiveView respondsToSelector:@selector(setAlpha:)]) {
hiddenProjectiveView.ybib_originAlpha = ((NSNumber *)[hiddenProjectiveView valueForKey:@"alpha"]).floatValue;
[hiddenProjectiveView setValue:@(0) forKey:@"alpha"];
}
}
- (void)implementOperateBrowserProtocol:(id<YBIBOperateBrowserProtocol>)obj {
__weak typeof(self) wSelf = self;
if ([obj respondsToSelector:@selector(setYb_hideBrowser:)]) {
[obj setYb_hideBrowser:^{
__strong typeof(wSelf) self = wSelf;
if (!self) return;
[self hide];
}];
}
if ([obj respondsToSelector:@selector(setYb_hideStatusBar:)]) {
[obj setYb_hideStatusBar:^(BOOL hide) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
hide ? [self hideStatusBar] : [self showStatusBar];
}];
}
if ([obj respondsToSelector:@selector(setYb_hideToolViews:)]) {
[obj setYb_hideToolViews:^(BOOL hide) {
__strong typeof(wSelf) self = wSelf;
if (!self) return;
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
[handler yb_hide:hide];
}
}];
}
}
- (void)implementGetBaseInfoProtocol:(id<YBIBGetBaseInfoProtocol>)obj {
__weak typeof(self) wSelf = self;
if ([obj respondsToSelector:@selector(setYb_currentOrientation:)]) {
[obj setYb_currentOrientation:^UIDeviceOrientation{
__strong typeof(wSelf) self = wSelf;
if (!self) return UIDeviceOrientationPortrait;
return self.rotationHandler.currentOrientation;
}];
}
if ([obj respondsToSelector:@selector(setYb_containerSize:)]) {
[obj setYb_containerSize:^CGSize(UIDeviceOrientation orientation) {
__strong typeof(wSelf) self = wSelf;
if (!self) return CGSizeZero;
return [self.rotationHandler containerSizeWithOrientation:orientation];
}];
}
if ([obj respondsToSelector:@selector(setYb_auxiliaryViewHandler:)]) {
[obj setYb_auxiliaryViewHandler:^id<YBIBAuxiliaryViewHandler>{
__strong typeof(wSelf) self = wSelf;
if (!self) return nil;
return self.auxiliaryViewHandler;
}];
}
if ([obj respondsToSelector:@selector(setYb_webImageMediator:)]) {
[obj setYb_webImageMediator:^id<YBIBWebImageMediator> {
__strong typeof(wSelf) self = wSelf;
if (!self) return nil;
NSAssert(self.webImageMediator, @"'webImageMediator' should not be nil.");
return self.webImageMediator;
}];
}
if ([obj respondsToSelector:@selector(setYb_currentPage:)]) {
[obj setYb_currentPage:^NSInteger{
__strong typeof(wSelf) self = wSelf;
if (!self) return 0;
return self.currentPage;
}];
}
if ([obj respondsToSelector:@selector(setYb_totalPage:)]) {
[obj setYb_totalPage:^NSInteger{
__strong typeof(wSelf) self = wSelf;
if (!self) return 0;
return [self.dataMediator numberOfCells];
}];
}
if ([obj respondsToSelector:@selector(setYb_backView:)]) {
obj.yb_backView = self;
}
if ([obj respondsToSelector:@selector(setYb_containerView:)]) {
obj.yb_containerView = self.containerView;
}
if ([obj respondsToSelector:@selector(setYb_collectionView:)]) {
[obj setYb_collectionView:^__kindof UICollectionView *{
__strong typeof(wSelf) self = wSelf;
if (!self) return nil;
return self.collectionView;
}];
}
if ([obj respondsToSelector:@selector(setYb_cellIsInCenter:)]) {
[obj setYb_cellIsInCenter:^BOOL{
__strong typeof(wSelf) self = wSelf;
CGFloat pageF = self.collectionView.contentOffset.x / self.collectionView.bounds.size.width;
// '0.001' is admissible error.
return ABS(pageF - (NSInteger)pageF) <= 0.001;
}];
}
if ([obj respondsToSelector:@selector(setYb_isTransitioning:)]) {
[obj setYb_isTransitioning:^BOOL{
__strong typeof(wSelf) self = wSelf;
if (!self) return NO;
return self.isTransitioning;
}];
}
if ([obj respondsToSelector:@selector(setYb_isShowTransitioning:)]) {
[obj setYb_isShowTransitioning:^BOOL{
__strong typeof(wSelf) self = wSelf;
if (!self) return NO;
return self.isShowTransitioning;
}];
}
if ([obj respondsToSelector:@selector(setYb_isHideTransitioning:)]) {
[obj setYb_isHideTransitioning:^BOOL{
__strong typeof(wSelf) self = wSelf;
if (!self) return NO;
return self.isHideTransitioning;
}];
}
if ([obj respondsToSelector:@selector(setYb_isRotating:)]) {
[obj setYb_isRotating:^BOOL{
__strong typeof(wSelf) self = wSelf;
if (!self) return NO;
return self.rotationHandler.isRotating;
}];
}
}
#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.dataMediator numberOfCells];
}
- (UICollectionViewCell *)collectionView:(YBIBCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
id<YBIBDataProtocol> data = [self.dataMediator dataForCellAtIndex:indexPath.row];
UICollectionViewCell<YBIBCellProtocol> *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[collectionView reuseIdentifierForCellClass:data.yb_classOfCell] forIndexPath:indexPath];
[self implementGetBaseInfoProtocol:cell];
[self implementOperateBrowserProtocol:cell];
if ([cell respondsToSelector:@selector(setYb_selfPage:)]) {
[cell setYb_selfPage:^NSInteger{
return indexPath.row;
}];
}
cell.yb_cellData = data;
if ([cell respondsToSelector:@selector(yb_pageChanged)]) {
[cell yb_pageChanged];
}
[self.dataMediator preloadWithPage:indexPath.row];
return cell;
}
#pragma mark - <UICollectionViewDelegate>
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageF = scrollView.contentOffset.x / scrollView.bounds.size.width;
NSInteger page = (NSInteger)(pageF + 0.5);
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_offsetXChanged:)]) {
[handler yb_offsetXChanged:pageF];
}
}
if (!scrollView.isDecelerating && !scrollView.isDragging) {
// Return if not scrolled by finger.
return;
}
if (page < 0 || page > [self.dataMediator numberOfCells] - 1) return;
if (self.rotationHandler.isRotating) return;
if (page != _currentPage) {
_currentPage = page;
[self pageNumberChanged];
}
}
#pragma mark - event
- (void)respondsToLongPress:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
if ([self.delegate respondsToSelector:@selector(yb_imageBrowser:respondsToLongPressWithData:)]) {
[self.delegate yb_imageBrowser:self respondsToLongPressWithData:[self currentData]];
} else {
for (id<YBIBToolViewHandler> handler in self.toolViewHandlers) {
if ([handler respondsToSelector:@selector(yb_respondsToLongPress)]) {
[handler yb_respondsToLongPress];
}
}
}
}
}
#pragma mark - getters & setters
- (YBIBContainerView *)containerView {
if (!_containerView) {
_containerView = [YBIBContainerView new];
_containerView.backgroundColor = UIColor.clearColor;
_containerView.layer.masksToBounds = YES;
}
return _containerView;
}
- (YBIBCollectionView *)collectionView {
if (!_collectionView) {
_collectionView = [YBIBCollectionView new];
_collectionView.delegate = self;
_collectionView.dataSource = self;
}
return _collectionView;
}
- (void)setCurrentPage:(NSInteger)currentPage {
NSInteger maxPage = self.dataMediator.numberOfCells - 1;
if (currentPage > maxPage) {
currentPage = maxPage;
}
_currentPage = currentPage;
if (self.collectionView.superview) {
[self collectionViewScrollToPage:currentPage];
}
}
- (void)setDistanceBetweenPages:(CGFloat)distanceBetweenPages {
self.collectionView.layout.distanceBetweenPages = distanceBetweenPages;
}
- (CGFloat)distanceBetweenPages {
return self.collectionView.layout.distanceBetweenPages;
}
- (void)setTransitioning:(BOOL)transitioning isShow:(BOOL)isShow {
_transitioning = transitioning;
_showTransitioning = transitioning && isShow;
_hideTransitioning = transitioning && !isShow;
// Make 'self.userInteractionEnabled' always 'YES' to block external interaction.
self.containerView.userInteractionEnabled = !transitioning;
self.collectionView.userInteractionEnabled = !transitioning;
if (transitioning) {
if ([self.delegate respondsToSelector:@selector(yb_imageBrowser:beginTransitioningWithIsShow:)]) {
[self.delegate yb_imageBrowser:self beginTransitioningWithIsShow:isShow];
}
} else {
if ([self.delegate respondsToSelector:@selector(yb_imageBrowser:endTransitioningWithIsShow:)]) {
[self.delegate yb_imageBrowser:self endTransitioningWithIsShow:isShow];
}
}
}
- (YBIBDataMediator *)dataMediator {
if (!_dataMediator) {
_dataMediator = [[YBIBDataMediator alloc] initWithBrowser:self];
_dataMediator.dataCacheCountLimit = YBIBLowMemory() ? 9 : 27;
_dataMediator.preloadCount = YBIBLowMemory() ? 0 : 2;
}
return _dataMediator;
}
- (void)setPreloadCount:(NSUInteger)preloadCount {
self.dataMediator.preloadCount = preloadCount;
}
- (NSUInteger)preloadCount {
return self.dataMediator.preloadCount;
}
- (YBIBScreenRotationHandler *)rotationHandler {
if (!_rotationHandler) {
_rotationHandler = [[YBIBScreenRotationHandler alloc] initWithBrowser:self];
}
return _rotationHandler;
}
- (void)setSupportedOrientations:(UIInterfaceOrientationMask)supportedOrientations {
self.rotationHandler.supportedOrientations = supportedOrientations;
}
- (UIInterfaceOrientationMask)supportedOrientations {
return self.rotationHandler.supportedOrientations;
}
- (UIDeviceOrientation)currentOrientation {
return self.rotationHandler.currentOrientation;
}
@end