创建项目提交全部文件
|
@ -0,0 +1,28 @@
|
|||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
|
||||
#
|
||||
#Pods/
|
||||
|
||||
.DS_Store
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Zhen Tan
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TZImagePickerController",
|
||||
platforms: [.iOS(.v8)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "TZImagePickerController",
|
||||
targets: ["TZImagePickerController"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "TZImagePickerController",
|
||||
path: "TZImagePickerController/TZImagePickerController",
|
||||
resources: [.process("TZImagePickerController.bundle")],
|
||||
publicHeadersPath: "."
|
||||
)
|
||||
]
|
||||
)
|
176
README.md
|
@ -0,0 +1,176 @@
|
|||
# TZImagePickerController
|
||||
[](https://github.com/banchichen/TZImagePickerController)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
|
||||
|
||||
A clone of UIImagePickerController, support picking multiple photos、original photo、video, also allow preview photo and video, support iOS10+.
|
||||
一个支持多选、选原图和视频的图片选择器,同时有预览功能,支持iOS10+。
|
||||
|
||||
## 重要提示1:提issue前,请先对照Demo、常见问题自查!Demo正常说明你可以升级下新版试试。
|
||||
|
||||
## 重要提示2:3.8.8版本修复了iOS18下无照片和openURL失效的问题
|
||||
关于iOS14模拟器的问题
|
||||
PHAuthorizationStatusLimited授权模式下,iOS14模拟器有bug,未授权照片无法显示,真机正常,暂可忽略:https://github.com/banchichen/TZImagePickerController/issues/1347
|
||||
|
||||
关于升级iOS10和Xcdoe8的提示:
|
||||
在Xcode8环境下将项目运行在iOS10的设备/模拟器中,访问相册和相机需要额外配置info.plist文件。分别是Privacy - Photo Library Usage Description和Privacy - Camera Usage Description字段,详见Demo中info.plist中的设置。
|
||||
|
||||
项目截图 1.Demo首页 2.照片列表页 3.照片预览页 4.视频预览页
|
||||
<img src="https://github.com/banchichen/TZImagePickerController/blob/master/TZImagePickerController/ScreenShots/DemoPage.png" width="40%" height="40%"><img src="https://github.com/banchichen/TZImagePickerController/blob/master/TZImagePickerController/ScreenShots/photoPickerVc.PNG" width="40%" height="40%">
|
||||
<img src="https://github.com/banchichen/TZImagePickerController/blob/master/TZImagePickerController/ScreenShots/photoPreviewVc.PNG" width="40%" height="40%"><img src="https://github.com/banchichen/TZImagePickerController/blob/master/TZImagePickerController/ScreenShots/videoPlayerVc.PNG" width="40%" height="40%">
|
||||
|
||||
## 一. Installation 安装
|
||||
|
||||
#### CocoaPods
|
||||
> pod 'TZImagePickerController' # Full version with all features
|
||||
> pod 'TZImagePickerController/Basic' # No location code
|
||||
|
||||
#### Carthage
|
||||
> github "banchichen/TZImagePickerController"
|
||||
|
||||
#### 手动安装
|
||||
> 将TZImagePickerController文件夹拽入项目中,导入头文件:#import "TZImagePickerController.h"
|
||||
|
||||
## 二. Example 例子
|
||||
|
||||
TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:9 delegate:self];
|
||||
|
||||
// You can get the photos by block, the same as by delegate.
|
||||
// 你可以通过block或者代理,来得到用户选择的照片.
|
||||
[imagePickerVc setDidFinishPickingPhotosHandle:^(NSArray<UIImage *> *photos, NSArray *assets, BOOL isSelectOriginalPhoto) {
|
||||
|
||||
}];
|
||||
[self presentViewController:imagePickerVc animated:YES completion:nil];
|
||||
|
||||
## 三. Requirements 要求
|
||||
iOS 10 or later.
|
||||
支持iOS10及以上系统。
|
||||
|
||||
TZImagePickerController uses Camera、Location、Microphone、Photo Library,you need add these properties to info.plist like Demo:
|
||||
TZImagePickerController使用了相机、定位、麦克风、相册,请参考Demo添加下列属性到info.plist文件:
|
||||
`Privacy - Camera Usage Description`
|
||||
`Privacy - Location Usage Description`
|
||||
`Privacy - Location When In Use Usage Description`
|
||||
`Privacy - Microphone Usage Description`
|
||||
`Privacy - Photo Library Usage Description`
|
||||
`Prevent limited photos access alert`
|
||||
|
||||
## 四. More 更多
|
||||
|
||||
If you find a bug, please create a issue.
|
||||
More information please view code.
|
||||
如果你发现了bug,请提一个issue。
|
||||
更多信息详见代码,也可查看我的博客: [我的博客](http://www.jianshu.com/p/1975411a31bb "半尺尘 - 简书")
|
||||
|
||||
关于issue:
|
||||
请尽可能详细地描述**系统版本**、**手机型号**、**库的版本**、**崩溃日志**和**复现步骤**,**请先更新到最新版再测试一下**,如果新版还存在再提~如果已有开启的类似issue,请直接在该issue下评论说出你的问题
|
||||
|
||||
## 五. FAQ 常见问题
|
||||
|
||||
**Q:pod search TZImagePickerController 搜索出来的不是最新版本**
|
||||
A:需要在终端执行cd转换文件路径命令退回到Desktop,然后执行pod setup命令更新本地spec缓存(可能需要几分钟),然后再搜索就可以了
|
||||
|
||||
**Q:拍照后照片保存失败**
|
||||
A:请参考issue481:https://github.com/banchichen/TZImagePickerController/issues/481 的信息排查,若还有问题请直接在issue内评论
|
||||
|
||||
**Q:photos数组图片不是原图,如何获取原图?**
|
||||
A:请参考issue457的解释:https://github.com/banchichen/TZImagePickerController/issues/457
|
||||
|
||||
**Q:系统语言是中文/英文,界面上却有部分相册名字、返回按钮显示成了英文/中文?**
|
||||
A:请参考 https://github.com/banchichen/TZImagePickerController/issues/443 和 https://github.com/banchichen/TZImagePickerController/issues/929
|
||||
|
||||
**Q:预览界面能否支持传入NSURL、UIImage对象?**
|
||||
A:3.0.1版本已支持,需新接一个库:[TZImagePreviewController](https://github.com/banchichen/TZImagePreviewController),请参考里面的Demo使用。
|
||||
|
||||
**Q:设置可选视频的最大/最小时长?照片的最小/最大尺寸?不符合要求的不显示**
|
||||
A:可以的,参照Demo的isAssetCanBeDisplayed方法实现。我会返回asset出来,显示与否你来决定,注意这个是一个同步方法,对于需要根据asset去异步获取的信息如视频的大小、视频是否存在iCloud里来过滤的,无法做到。如果真要这样做,相册打开速度会变慢,你需要改我源码。
|
||||
如果需要显示,选择时才提醒用户不可选,则实现isAssetCanBeSelected,用户选择时会调用它
|
||||
|
||||
**Q:预览页面出现了导航栏?**
|
||||
A:https://github.com/banchichen/TZImagePickerController/issues/652
|
||||
|
||||
**Q:可否增加微信编辑图片的功能?**
|
||||
A:考虑下,优先级低
|
||||
|
||||
**Q:是否有QQ/微信群/钉钉群?**
|
||||
A:有「钉钉群:33192786」和「QQ群:859033147」,推荐加钉钉群,答疑响应更快
|
||||
|
||||
**Q:想提交一个Pull Request?**
|
||||
A:请先加钉钉群(33192786)说下方案,和我确认下,避免同时改动同一处内容。**一个PR请只修复1个问题,变动内容越少越好**。
|
||||
|
||||
**Q:demo在真机上跑不起来?**
|
||||
A:1、team选你自己的;2、bundleId也改成你自己的或改成一个不会和别人重复的。可参考[简书的这篇博客](https://www.jianshu.com/p/cbe59138fca6)
|
||||
|
||||
**Q:3.6.4以上版本设置导航栏颜色无效?**
|
||||
A:参考Demo里的代码,加上imagePickerVc.navigationBar.standardAppearance的相关设置
|
||||
|
||||
**Q:设置导航栏颜色无效?导航栏颜色总是白色?**
|
||||
A:是否有集成WRNavigationBar?如有,参考其readme调一下它的wr_setBlackList,把TZImagePickerController相关的控制器放到黑名单里,使得不受WRNavigationBar的影响。如果没有集成,可在issues列表里搜一下看看类似的issue参考下,如实在没头绪,可加群提供个能复现该问题的demo,0~2天给你解决。最近发现WRNavigationBar的黑名单会有不生效的情况,临时解决方案大家可参考:[https://github.com/wangrui460/WRNavigationBar/issues/145](https://github.com/wangrui460/WRNavigationBar/issues/145)
|
||||
|
||||
**Q:导航栏没了?**
|
||||
A:是否有集成GKNavigationBarViewController?需要升级到2.0.4及以上版本,详见issue:[https://github.com/QuintGao/GKNavigationBarViewController/issues/7](https://github.com/QuintGao/GKNavigationBarViewController/issues/7)。
|
||||
|
||||
**Q:有的视频导出失败?**
|
||||
A:升级到2.2.6及以上版本试试,发现是修正视频转向导致的,2.2.6开始默认不再主动修正。如需打开,可设置needFixComposition为YES,但有几率导致安卓拍的视频导出失败。此外也可参考这个issue:https://github.com/banchichen/TZImagePickerController/issues/1073
|
||||
|
||||
**Q:视频导出慢?**
|
||||
A:视频导出分两步,第一步是通过PHAsset获取AVURLAsset,如是iCloud视频则涉及到网络请求,耗时容易不可控,第二步是通过AVURLAsset把视频保存到沙盒,耗时不算多。但第一步耗时不可控,你可以拷贝我源码出来拿到第一步的进度给用户一个进度提示...
|
||||
|
||||
**Q:有的图片info里没有PHImageFileURLKey?**
|
||||
A:不要去拿PHImageFileURLKey,没用的,只有通过Photos框架才能访问相册照片,光拿一个路径没用。
|
||||
如果需要通过路径上传照片,请先把UIImage保存到沙盒,**用沙盒路径**。
|
||||
如果你上传照片需要一个名字参数,请参考Demo**直接用照片名字**。
|
||||
|
||||
## 六. Release Notes 最近更新
|
||||
|
||||
**3.8.8 支持iOS18,修复openURL的失效问题** [#1686](https://github.com/banchichen/TZImagePickerController/issues/1686)
|
||||
**3.8.5 新增隐私清单文件** [#1675](https://github.com/banchichen/TZImagePickerController/pull/1675)
|
||||
**3.8.4 支持使用不带定位代码的版本** [#1606](https://github.com/banchichen/TZImagePickerController/pull/1606)
|
||||
3.8.1 iOS14下可添加访问更多照片,详见PR内的评论 [#1526](https://github.com/banchichen/TZImagePickerController/pull/1526)
|
||||
3.7.6 修复iOS15.2下初次授权相册权限时的长时间卡顿&白屏问题 [#1547](https://github.com/banchichen/TZImagePickerController/issues/1547)
|
||||
**3.6.7 修复Xcode13&iOS15下导航栏颜色异常问题**
|
||||
3.6.2 新增allowEditVideo,单选视频时支持裁剪
|
||||
3.6.0 修复iOS14下iCloud视频导出失败问题
|
||||
**3.5.2 适配iPhone12系列设备**
|
||||
3.4.4 支持Dark Mode
|
||||
3.4.2 适配iOS14,若干问题修复
|
||||
3.3.2 适配iOS13,若干问题修复
|
||||
3.2.1 新增裁剪用scaleAspectFillCrop属性,设置为YES后,照片尺寸小于裁剪框时会自动放大撑满
|
||||
3.2.0 加入用NSOperationQueue控制获取原图并发数降低内存的示例
|
||||
3.1.8 批量获取图片时加入队列控制,尝试优化大批量选择图片时CPU和内存占用过高的问题(仍然危险,maxImagesCount谨慎设置过大...)
|
||||
3.1.5 相册内无照片时给出提示,修复快速滑动时内存一直增加的问题
|
||||
3.1.3 适配阿拉伯等语言下从右往左布局的特性
|
||||
3.0.8 新增gifImagePlayBlock允许使用FLAnimatedImage等替换内部的GIF播放方案
|
||||
3.0.7 适配iPhoneXR、XS、XS Max
|
||||
3.0.6 优化保存照片、视频的方法
|
||||
3.0.1 新增对[TZImagePreviewController](https://github.com/banchichen/TZImagePreviewController)库的支持,允许预览UIImage、NSURL、PHAsset对象
|
||||
**3.0.0 去除iOS6和7的适配代码,更轻量,最低支持iOS8**
|
||||
2.2.6 新增needFixComposition属性,默认为NO,不再主动修正视频转向,防止部分安卓拍的视频导出失败(**最后一个支持iOS6和7的版本**)
|
||||
2.1.5 修复开启showSelectedIndex后照片列表页iCloud图片进度条紊乱的bug
|
||||
2.1.4 新增多个页面和组件的样式自定义block,允许自定义绝大多数UI样式
|
||||
2.1.2 新增showPhotoCannotSelectLayer属性,当已选照片张数达到最大可选张数时,可像微信一样让其它照片显示一个提示不可选的浮层
|
||||
2.1.1 新增是否显示图片选中序号的属性,优化一些细节
|
||||
2.1.0.3 新增拍摄视频功能,优化一些细节
|
||||
2.0.0.6 优化自定义languageBundle的支持,加入使用示例
|
||||
2.0.0.5 优化性能,提高选择器打开速度,新增越南语支持
|
||||
2.0.0.2 新增繁体语言,可设置首选语言,国际化支持更强大;优化一些细节
|
||||
1.9.8 支持Carthage,优化一些细节
|
||||
1.9.6 优化视频预览和gif预览页toolbar在iPhoneX上的样式
|
||||
...
|
||||
1.8.4 加入横竖屏适配;支持视频/gif多选;支持视频和照片一起选
|
||||
1.8.1 新增2个代理方法,支持由上层来决定相册/照片的显示与否
|
||||
...
|
||||
1.7.7 支持GIF图片的播放和选择
|
||||
1.7.6 支持对共享相册和同步相册的显示
|
||||
1.7.5 允许不进入预览页面直接选择照片
|
||||
1.7.4 支持单选模式下裁剪照片,支持任意矩形和圆形裁剪框
|
||||
1.7.3 优化iCloud照片的显示与选择
|
||||
...
|
||||
1.5.0 可把拍照按钮放在外面;可自定义照片排序方式;Demo页的UI大改版,新增若干开关;
|
||||
...
|
||||
1.4.5 性能大幅提升(性能测试截图请去博客查看);可在照片列表页拍照;Demo大幅优化;
|
||||
...
|
||||
|
||||
## 七. Common links 常用链接
|
||||
1. Json diff online: https://www.jsondiffonline.com/
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "TZImagePickerController"
|
||||
s.version = "3.8.8"
|
||||
s.summary = "A clone of UIImagePickerController, support picking multiple photos、original photo and video"
|
||||
s.homepage = "https://github.com/banchichen/TZImagePickerController"
|
||||
s.license = "MIT"
|
||||
s.author = { "banchichen" => "tanzhenios@foxmail.com" }
|
||||
s.platform = :ios
|
||||
s.ios.deployment_target = "10.0"
|
||||
s.source = { :git => "https://github.com/banchichen/TZImagePickerController.git", :tag => "3.8.8" }
|
||||
s.requires_arc = true
|
||||
|
||||
s.subspec 'Basic' do |b|
|
||||
b.resources = "TZImagePickerController/TZImagePickerController/*.{png,bundle}"
|
||||
b.source_files = "TZImagePickerController/TZImagePickerController/*.{h,m}"
|
||||
end
|
||||
|
||||
s.subspec 'Location' do |l|
|
||||
l.source_files = 'TZImagePickerController/Location/*.{h,m}'
|
||||
end
|
||||
|
||||
s.frameworks = "Photos", "PhotosUI"
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:TZImagePickerController.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9F763A411FA071CF00D9E526"
|
||||
BuildableName = "TZImagePickerController.framework"
|
||||
BlueprintName = "TZImagePickerControllerFramework"
|
||||
ReferencedContainer = "container:TZImagePickerController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9F763A411FA071CF00D9E526"
|
||||
BuildableName = "TZImagePickerController.framework"
|
||||
BlueprintName = "TZImagePickerControllerFramework"
|
||||
ReferencedContainer = "container:TZImagePickerController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9F763A411FA071CF00D9E526"
|
||||
BuildableName = "TZImagePickerController.framework"
|
||||
BlueprintName = "TZImagePickerControllerFramework"
|
||||
ReferencedContainer = "container:TZImagePickerController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// AppDelegate.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// AppDelegate.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
// 打开下面这句代码,使用导航栏控制器作为rootViewController
|
||||
// [self useNavControllerAsRoot];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)useNavControllerAsRoot {
|
||||
UIViewController *vc = [[UIViewController alloc] init];
|
||||
vc.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(20, 200, 300, 44)];
|
||||
[btn setTitle:@"pushTZImagePickerController" forState:UIControlStateNormal];
|
||||
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[btn addTarget:self action:@selector(pushTZImagePickerController) forControlEvents:UIControlEventTouchUpInside];
|
||||
[vc.view addSubview:btn];
|
||||
|
||||
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
|
||||
nav.navigationBar.barStyle = UIBarStyleBlack;
|
||||
nav.navigationBar.translucent = YES;
|
||||
nav.navigationBar.barTintColor = [UIColor colorWithRed:(34/255.0) green:(34/255.0) blue:(34/255.0) alpha:1.0];
|
||||
nav.navigationBar.tintColor = [UIColor blackColor];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UINavigationBarAppearance *barAppearance = [[UINavigationBarAppearance alloc] init];
|
||||
if (nav.navigationBar.isTranslucent) {
|
||||
UIColor *barTintColor = nav.navigationBar.barTintColor;
|
||||
barAppearance.backgroundColor = [barTintColor colorWithAlphaComponent:0.85];
|
||||
} else {
|
||||
barAppearance.backgroundColor = nav.navigationBar.barTintColor;
|
||||
}
|
||||
barAppearance.titleTextAttributes = nav.navigationBar.titleTextAttributes;
|
||||
nav.navigationBar.standardAppearance = barAppearance;
|
||||
nav.navigationBar.scrollEdgeAppearance = barAppearance;
|
||||
}
|
||||
[nav setNavigationBarHidden:YES];
|
||||
|
||||
self.window.rootViewController = nav;
|
||||
[self.window makeKeyAndVisible];
|
||||
}
|
||||
|
||||
- (void)pushTZImagePickerController {
|
||||
TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:9 columnNumber:4 delegate:nil pushPhotoPickerVc:YES];
|
||||
imagePickerVc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
UINavigationController *nav = (UINavigationController *)[UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
[nav.topViewController presentViewController:imagePickerVc animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
|
||||
}
|
||||
|
||||
@end
|
After Width: | Height: | Size: 136 B |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AlbumAddBtn@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "back@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "hx_videoedit_left@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 122 B |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "photo_delete@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 347 B |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "hx_videoedit_right@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 122 B |
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,393 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="AMD-xf-cz2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="640"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="obE-hs-XLa" userLabel="View1">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jpa-B9-8n6">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="showTakePhotoBtnSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="t9n-F2-EH7"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许拍照" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R92-Zn-NWV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="35" id="RuS-Zw-ecY"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wWK-ob-W4A" userLabel="View2">
|
||||
<rect key="frame" x="0.0" y="55" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Rbt-bh-r0x">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="照片按修改时间升序排列" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UL3-n3-Hmt">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kJv-DR-NlY" userLabel="View3">
|
||||
<rect key="frame" x="0.0" y="90" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="a58-aK-ZB0">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowPickingVideoSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="kMA-4q-CZa"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许选择视频" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xCu-D3-IZg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z20-6k-hhA" userLabel="View4">
|
||||
<rect key="frame" x="0.0" y="125" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iRQ-aE-1KG">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowPickingImageSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="MHy-72-YhW"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许选择照片" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vNN-OI-9z1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jKt-h1-BC7" userLabel="View5">
|
||||
<rect key="frame" x="0.0" y="160" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4L3-Da-pcd">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowPickingOriginPhotoSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="mdw-vb-h0y"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许选择照片原图" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7mi-Lf-2N5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gde-hr-2Sy" userLabel="View6">
|
||||
<rect key="frame" x="0.0" y="195" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="zLj-vc-Xwz">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowPickingGifSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="O9W-7a-uc8"/>
|
||||
<action selector="allowPickingOriginPhotoSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="Y1n-2W-q5O"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许选择Gif图片" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ly2-uJ-7DN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qrr-87-KzM" userLabel="View7">
|
||||
<rect key="frame" x="0.0" y="230" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="uHt-bT-lBZ">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="showSheetSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="kmV-vK-SDz"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="把拍照/拍视频按钮放在外面" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jop-5R-ioj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="PA2-zk-4Ey" userLabel="View8">
|
||||
<rect key="frame" x="0.0" y="265" width="375" height="35"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="照片最大可选张数" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rK3-1U-kcW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="9" borderStyle="roundedRect" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="37K-cO-hgf">
|
||||
<rect key="frame" x="220" y="2" width="80" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" keyboardType="numberPad"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="VUP-wm-Ah2" userLabel="View9">
|
||||
<rect key="frame" x="0.0" y="300" width="375" height="35"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="每行展示照片张数" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fbH-6q-oeE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="4" borderStyle="roundedRect" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="YR1-nJ-q0v">
|
||||
<rect key="frame" x="220" y="2" width="80" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" keyboardType="numberPad"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="92q-n9-Zwf" userLabel="View10">
|
||||
<rect key="frame" x="0.0" y="335" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="0ju-HJ-A06">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowCropSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="tw0-q2-Fa7"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="单选模式下允许裁剪" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ibb-kN-MWC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uex-bO-uvg" userLabel="View11">
|
||||
<rect key="frame" x="0.0" y="370" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="hsH-wt-l1R">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="needCircleCropSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="2yt-Uf-LNh"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="使用圆形裁剪框" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TiG-df-jDs">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kBC-oD-k6h" userLabel="View12">
|
||||
<rect key="frame" x="0.0" y="405" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="edM-ah-5KZ">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="allowPickingMultipleVideoSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="auh-9d-aAW"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许多选视频/GIF/图片" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7q9-aI-kbz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v2g-6C-m1J" userLabel="View13">
|
||||
<rect key="frame" x="0.0" y="440" width="375" height="35"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mMy-bi-cIJ">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="showTakeVideoBtnSwitchClick:" destination="BYZ-38-t0r" eventType="valueChanged" id="YVa-sT-R3M"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="允许拍视频" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8H2-cq-aUq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fWM-jc-1VU" userLabel="View14">
|
||||
<rect key="frame" x="0.0" y="475" width="375" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XsY-NI-Txd">
|
||||
<rect key="frame" x="225" y="2" width="51" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="右上角显示图片选中序号" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nm2-mT-siy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="VUP-wm-Ah2" firstAttribute="leading" secondItem="PA2-zk-4Ey" secondAttribute="leading" id="1iv-97-UAf"/>
|
||||
<constraint firstItem="v2g-6C-m1J" firstAttribute="leading" secondItem="kBC-oD-k6h" secondAttribute="leading" id="1xv-oX-xWZ"/>
|
||||
<constraint firstItem="92q-n9-Zwf" firstAttribute="trailing" secondItem="VUP-wm-Ah2" secondAttribute="trailing" id="2dI-dy-w3s"/>
|
||||
<constraint firstItem="PA2-zk-4Ey" firstAttribute="trailing" secondItem="Qrr-87-KzM" secondAttribute="trailing" id="3up-87-K1l"/>
|
||||
<constraint firstItem="VUP-wm-Ah2" firstAttribute="top" secondItem="PA2-zk-4Ey" secondAttribute="bottom" id="3wW-Pg-fzS"/>
|
||||
<constraint firstItem="Z20-6k-hhA" firstAttribute="leading" secondItem="kJv-DR-NlY" secondAttribute="leading" id="9l0-Jl-AUA"/>
|
||||
<constraint firstItem="Qrr-87-KzM" firstAttribute="height" secondItem="gde-hr-2Sy" secondAttribute="height" id="ClL-Ck-Dcn"/>
|
||||
<constraint firstItem="v2g-6C-m1J" firstAttribute="top" secondItem="kBC-oD-k6h" secondAttribute="bottom" id="ErC-BJ-ezP"/>
|
||||
<constraint firstItem="92q-n9-Zwf" firstAttribute="leading" secondItem="VUP-wm-Ah2" secondAttribute="leading" id="FGD-bj-L3J"/>
|
||||
<constraint firstItem="wWK-ob-W4A" firstAttribute="height" secondItem="obE-hs-XLa" secondAttribute="height" id="HFb-Z1-0pm"/>
|
||||
<constraint firstItem="VUP-wm-Ah2" firstAttribute="trailing" secondItem="PA2-zk-4Ey" secondAttribute="trailing" id="JFl-1c-hQh"/>
|
||||
<constraint firstItem="gde-hr-2Sy" firstAttribute="height" secondItem="jKt-h1-BC7" secondAttribute="height" id="KrA-dv-SzU"/>
|
||||
<constraint firstItem="PA2-zk-4Ey" firstAttribute="leading" secondItem="Qrr-87-KzM" secondAttribute="leading" id="NLP-pQ-Qgy"/>
|
||||
<constraint firstItem="92q-n9-Zwf" firstAttribute="top" secondItem="VUP-wm-Ah2" secondAttribute="bottom" id="NoR-J2-mVI"/>
|
||||
<constraint firstItem="wWK-ob-W4A" firstAttribute="top" secondItem="obE-hs-XLa" secondAttribute="bottom" id="PZa-2b-GlH"/>
|
||||
<constraint firstItem="PA2-zk-4Ey" firstAttribute="top" secondItem="Qrr-87-KzM" secondAttribute="bottom" id="QoI-na-E3C"/>
|
||||
<constraint firstItem="wWK-ob-W4A" firstAttribute="leading" secondItem="obE-hs-XLa" secondAttribute="leading" id="R0F-K3-Hve"/>
|
||||
<constraint firstItem="Z20-6k-hhA" firstAttribute="top" secondItem="kJv-DR-NlY" secondAttribute="bottom" id="T4D-dN-Lgh"/>
|
||||
<constraint firstItem="PA2-zk-4Ey" firstAttribute="height" secondItem="Qrr-87-KzM" secondAttribute="height" id="TES-KZ-ed7"/>
|
||||
<constraint firstItem="jKt-h1-BC7" firstAttribute="leading" secondItem="Z20-6k-hhA" secondAttribute="leading" id="VGe-vf-MfL"/>
|
||||
<constraint firstItem="uex-bO-uvg" firstAttribute="top" secondItem="92q-n9-Zwf" secondAttribute="bottom" id="Vae-Me-t4r"/>
|
||||
<constraint firstItem="kJv-DR-NlY" firstAttribute="leading" secondItem="wWK-ob-W4A" secondAttribute="leading" id="Wmr-AU-Xuo"/>
|
||||
<constraint firstItem="jKt-h1-BC7" firstAttribute="trailing" secondItem="Z20-6k-hhA" secondAttribute="trailing" id="WwT-Gk-NVH"/>
|
||||
<constraint firstItem="gde-hr-2Sy" firstAttribute="trailing" secondItem="jKt-h1-BC7" secondAttribute="trailing" id="Xih-PA-4s3"/>
|
||||
<constraint firstItem="Qrr-87-KzM" firstAttribute="leading" secondItem="gde-hr-2Sy" secondAttribute="leading" id="Zb4-47-UJy"/>
|
||||
<constraint firstItem="kBC-oD-k6h" firstAttribute="top" secondItem="uex-bO-uvg" secondAttribute="bottom" id="b6r-9M-h1g"/>
|
||||
<constraint firstItem="Z20-6k-hhA" firstAttribute="height" secondItem="kJv-DR-NlY" secondAttribute="height" id="cSa-cz-LpU"/>
|
||||
<constraint firstItem="Qrr-87-KzM" firstAttribute="top" secondItem="jKt-h1-BC7" secondAttribute="bottom" constant="35" id="eMB-pt-F7l"/>
|
||||
<constraint firstItem="gde-hr-2Sy" firstAttribute="top" secondItem="jKt-h1-BC7" secondAttribute="bottom" id="eqU-F1-2IQ"/>
|
||||
<constraint firstItem="v2g-6C-m1J" firstAttribute="trailing" secondItem="kBC-oD-k6h" secondAttribute="trailing" id="ez1-O3-KTh"/>
|
||||
<constraint firstItem="jKt-h1-BC7" firstAttribute="top" secondItem="Z20-6k-hhA" secondAttribute="bottom" id="gEr-Zu-lDh"/>
|
||||
<constraint firstItem="VUP-wm-Ah2" firstAttribute="height" secondItem="PA2-zk-4Ey" secondAttribute="height" id="hDo-rF-XEe"/>
|
||||
<constraint firstItem="obE-hs-XLa" firstAttribute="leading" secondItem="AMD-xf-cz2" secondAttribute="leading" id="hDy-MM-w5g"/>
|
||||
<constraint firstItem="uex-bO-uvg" firstAttribute="height" secondItem="92q-n9-Zwf" secondAttribute="height" id="hMH-pK-i1M"/>
|
||||
<constraint firstItem="kJv-DR-NlY" firstAttribute="top" secondItem="wWK-ob-W4A" secondAttribute="bottom" id="i27-SM-wgc"/>
|
||||
<constraint firstItem="kBC-oD-k6h" firstAttribute="height" secondItem="uex-bO-uvg" secondAttribute="height" id="iWw-4K-Ssi"/>
|
||||
<constraint firstItem="gde-hr-2Sy" firstAttribute="leading" secondItem="jKt-h1-BC7" secondAttribute="leading" id="ikB-6o-XrM"/>
|
||||
<constraint firstItem="kBC-oD-k6h" firstAttribute="trailing" secondItem="uex-bO-uvg" secondAttribute="trailing" id="jDo-vK-PhT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="obE-hs-XLa" secondAttribute="trailing" id="kZv-6I-6fD"/>
|
||||
<constraint firstItem="kJv-DR-NlY" firstAttribute="height" secondItem="wWK-ob-W4A" secondAttribute="height" id="kvY-3q-eeM"/>
|
||||
<constraint firstItem="wWK-ob-W4A" firstAttribute="trailing" secondItem="obE-hs-XLa" secondAttribute="trailing" id="mfr-E7-8po"/>
|
||||
<constraint firstAttribute="height" constant="440" id="miA-vX-6Hr"/>
|
||||
<constraint firstItem="92q-n9-Zwf" firstAttribute="height" secondItem="VUP-wm-Ah2" secondAttribute="height" id="oBc-gA-ADs"/>
|
||||
<constraint firstItem="uex-bO-uvg" firstAttribute="trailing" secondItem="92q-n9-Zwf" secondAttribute="trailing" id="r2c-JO-oNO"/>
|
||||
<constraint firstItem="kBC-oD-k6h" firstAttribute="leading" secondItem="uex-bO-uvg" secondAttribute="leading" id="r9g-v8-5U7"/>
|
||||
<constraint firstItem="obE-hs-XLa" firstAttribute="top" secondItem="AMD-xf-cz2" secondAttribute="top" constant="20" id="s2U-ND-yc4"/>
|
||||
<constraint firstItem="jKt-h1-BC7" firstAttribute="height" secondItem="Z20-6k-hhA" secondAttribute="height" id="sWH-6u-m28"/>
|
||||
<constraint firstItem="kJv-DR-NlY" firstAttribute="trailing" secondItem="wWK-ob-W4A" secondAttribute="trailing" id="t41-Zi-afs"/>
|
||||
<constraint firstItem="Qrr-87-KzM" firstAttribute="trailing" secondItem="gde-hr-2Sy" secondAttribute="trailing" id="vZF-Ly-XEP"/>
|
||||
<constraint firstItem="Z20-6k-hhA" firstAttribute="trailing" secondItem="kJv-DR-NlY" secondAttribute="trailing" id="w7j-cD-oSq"/>
|
||||
<constraint firstItem="v2g-6C-m1J" firstAttribute="height" secondItem="kBC-oD-k6h" secondAttribute="height" id="xp8-5X-dXy"/>
|
||||
<constraint firstItem="uex-bO-uvg" firstAttribute="leading" secondItem="92q-n9-Zwf" secondAttribute="leading" id="yDZ-7l-FMk"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="AMD-xf-cz2" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" id="4Ve-nD-CyY"/>
|
||||
<constraint firstItem="obE-hs-XLa" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" id="BAt-Qs-VNJ"/>
|
||||
<constraint firstItem="AMD-xf-cz2" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="O7i-qy-sG8"/>
|
||||
<constraint firstItem="AMD-xf-cz2" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" id="Oqp-06-c4N"/>
|
||||
<constraint firstAttribute="trailing" secondItem="AMD-xf-cz2" secondAttribute="trailing" id="eJF-uE-CjV"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="allowCropSwitch" destination="0ju-HJ-A06" id="PHl-Yf-uf2"/>
|
||||
<outlet property="allowPickingGifSwitch" destination="zLj-vc-Xwz" id="1UG-vY-alU"/>
|
||||
<outlet property="allowPickingImageSwitch" destination="iRQ-aE-1KG" id="V7a-u6-cLv"/>
|
||||
<outlet property="allowPickingMuitlpleVideoSwitch" destination="edM-ah-5KZ" id="VUN-EN-OaU"/>
|
||||
<outlet property="allowPickingOriginalPhotoSwitch" destination="4L3-Da-pcd" id="KFm-Zl-dpD"/>
|
||||
<outlet property="allowPickingVideoSwitch" destination="a58-aK-ZB0" id="9Ti-hW-QRj"/>
|
||||
<outlet property="columnNumberTF" destination="YR1-nJ-q0v" id="TE2-iH-EWE"/>
|
||||
<outlet property="maxCountTF" destination="37K-cO-hgf" id="776-gE-tea"/>
|
||||
<outlet property="needCircleCropSwitch" destination="hsH-wt-l1R" id="SVP-EE-wgf"/>
|
||||
<outlet property="scrollView" destination="AMD-xf-cz2" id="0Bb-Te-K4h"/>
|
||||
<outlet property="showSelectedIndexSwitch" destination="XsY-NI-Txd" id="ou9-YO-ebP"/>
|
||||
<outlet property="showSheetSwitch" destination="uHt-bT-lBZ" id="NxK-wM-ivE"/>
|
||||
<outlet property="showTakePhotoBtnSwitch" destination="jpa-B9-8n6" id="lh8-2O-IWS"/>
|
||||
<outlet property="showTakeVideoBtnSwitch" destination="mMy-bi-cIJ" id="jER-Fp-aOr"/>
|
||||
<outlet property="sortAscendingSwitch" destination="Rbt-bh-r0x" id="WEA-Vq-uSg"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="367.19999999999999" y="317.99100449775113"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// FLAnimatedImage.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Allow user classes conveniently just importing one header.
|
||||
#import "FLAnimatedImageView.h"
|
||||
|
||||
|
||||
#ifndef NS_DESIGNATED_INITIALIZER
|
||||
#if __has_attribute(objc_designated_initializer)
|
||||
#define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer))
|
||||
#else
|
||||
#define NS_DESIGNATED_INITIALIZER
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
//
|
||||
// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`.
|
||||
// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion.
|
||||
// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back.
|
||||
// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization.
|
||||
//
|
||||
@interface FLAnimatedImage : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0`
|
||||
@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger loopCount; // 0 means repeating the animation indefinitely
|
||||
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount]
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default)
|
||||
|
||||
// Intended to be called from main thread synchronously; will return immediately.
|
||||
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
|
||||
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
|
||||
|
||||
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
|
||||
+ (CGSize)sizeForImage:(id)image;
|
||||
|
||||
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data;
|
||||
// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLLogLevel) {
|
||||
FLLogLevelNone = 0,
|
||||
FLLogLevelError,
|
||||
FLLogLevelWarn,
|
||||
FLLogLevelInfo,
|
||||
FLLogLevelDebug,
|
||||
FLLogLevelVerbose
|
||||
};
|
||||
|
||||
@interface FLAnimatedImage (Logging)
|
||||
|
||||
+ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel;
|
||||
+ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level;
|
||||
|
||||
@end
|
||||
|
||||
#define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)]
|
||||
|
||||
@interface FLWeakProxy : NSProxy
|
||||
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject;
|
||||
|
||||
@end
|
|
@ -0,0 +1,816 @@
|
|||
//
|
||||
// FLAnimatedImage.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
|
||||
// From vm_param.h, define for iOS 8.0 or higher to build on device.
|
||||
#ifndef BYTE_SIZE
|
||||
#define BYTE_SIZE 8 // byte size in bits
|
||||
#endif
|
||||
|
||||
#define MEGABYTE (1024 * 1024)
|
||||
|
||||
// This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
|
||||
const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02;
|
||||
|
||||
// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB).
|
||||
// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category.
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) {
|
||||
FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU)
|
||||
FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile)
|
||||
FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance)
|
||||
FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no.
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) {
|
||||
FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit
|
||||
FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand.
|
||||
FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback.
|
||||
FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc.
|
||||
};
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache;
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index;
|
||||
- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImage ()
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes
|
||||
@property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance.
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default)
|
||||
@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index
|
||||
@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes
|
||||
@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background
|
||||
@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes
|
||||
@property (nonatomic, assign) NSUInteger memoryWarningCount;
|
||||
@property (nonatomic, strong, readonly) dispatch_queue_t serialQueue;
|
||||
@property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource;
|
||||
|
||||
// The weak proxy is used to break retain cycles with delayed actions from memory warnings.
|
||||
// We are lying about the actual type here to gain static type checking and eliminate casts.
|
||||
// The actual type of the object is `FLWeakProxy`.
|
||||
@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying.
|
||||
static NSHashTable *allAnimatedImagesWeak;
|
||||
|
||||
@implementation FLAnimatedImage
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
// This is the definite value the frame cache needs to size itself to.
|
||||
- (NSUInteger)frameCacheSizeCurrent
|
||||
{
|
||||
NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal;
|
||||
|
||||
// If set, respect the caps.
|
||||
if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax);
|
||||
}
|
||||
|
||||
if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal);
|
||||
}
|
||||
|
||||
return frameCacheSizeCurrent;
|
||||
}
|
||||
|
||||
|
||||
- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax
|
||||
{
|
||||
if (_frameCacheSizeMax != frameCacheSizeMax) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMax = frameCacheSizeMax;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal
|
||||
{
|
||||
if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMaxInternal = frameCacheSizeMaxInternal;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self == [FLAnimatedImage class]) {
|
||||
// UIKit memory warning notification handler shared by all of the instances
|
||||
allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
// UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak
|
||||
NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread");
|
||||
// Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
NSArray *images = nil;
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
images = [[allAnimatedImagesWeak allObjects] copy];
|
||||
}
|
||||
// Now issue notifications to all of the images while holding a strong reference to them
|
||||
[images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
FLAnimatedImage *animatedImage = [self initWithAnimatedGIFData:nil];
|
||||
if (!animatedImage) {
|
||||
FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`.");
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data
|
||||
{
|
||||
return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled
|
||||
{
|
||||
// Early return if no data supplied!
|
||||
BOOL hasData = ([data length] > 0);
|
||||
if (!hasData) {
|
||||
FLLog(FLLogLevelError, @"No animated GIF data supplied.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
|
||||
|
||||
// Keep a strong reference to `data` and expose it read-only publicly.
|
||||
// However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
|
||||
_data = data;
|
||||
_predrawingEnabled = isPredrawingEnabled;
|
||||
|
||||
// Initialize internal data structures
|
||||
_cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
|
||||
_cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
_requestedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
|
||||
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
|
||||
(__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
|
||||
// Early return on failure!
|
||||
if (!_imageSource) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Early return if not GIF!
|
||||
CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
|
||||
BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
|
||||
if (!isGIFData) {
|
||||
FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Get `LoopCount`
|
||||
// Note: 0 means repeating the animation indefinitely.
|
||||
// Image properties example:
|
||||
// {
|
||||
// FileSize = 314446;
|
||||
// "{GIF}" = {
|
||||
// HasGlobalColorMap = 1;
|
||||
// LoopCount = 0;
|
||||
// };
|
||||
// }
|
||||
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
|
||||
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
|
||||
|
||||
// Iterate through frame images
|
||||
size_t imageCount = CGImageSourceGetCount(_imageSource);
|
||||
NSUInteger skippedFrameCount = 0;
|
||||
NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
|
||||
for (size_t i = 0; i < imageCount; i++) {
|
||||
@autoreleasepool {
|
||||
CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
|
||||
if (frameImageRef) {
|
||||
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
|
||||
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
|
||||
if (frameImage) {
|
||||
// Set poster image
|
||||
if (!self.posterImage) {
|
||||
_posterImage = frameImage;
|
||||
// Set its size to proxy our size.
|
||||
_size = _posterImage.size;
|
||||
// Remember index of poster image so we never purge it; also add it to the cache.
|
||||
_posterImageFrameIndex = i;
|
||||
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
|
||||
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
// Get `DelayTime`
|
||||
// Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
|
||||
// Frame properties example:
|
||||
// {
|
||||
// ColorModel = RGB;
|
||||
// Depth = 8;
|
||||
// PixelHeight = 960;
|
||||
// PixelWidth = 640;
|
||||
// "{GIF}" = {
|
||||
// DelayTime = "0.4";
|
||||
// UnclampedDelayTime = "0.4";
|
||||
// };
|
||||
// }
|
||||
|
||||
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
|
||||
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
|
||||
|
||||
// Try to use the unclamped delay time; fall back to the normal delay time.
|
||||
NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
|
||||
if (!delayTime) {
|
||||
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
|
||||
}
|
||||
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
|
||||
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
|
||||
if (!delayTime) {
|
||||
if (i == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
} else {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
|
||||
delayTime = delayTimesForIndexesMutable[@(i - 1)];
|
||||
}
|
||||
}
|
||||
// Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
|
||||
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
|
||||
if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
|
||||
FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
}
|
||||
delayTimesForIndexesMutable[@(i)] = delayTime;
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
|
||||
}
|
||||
CFRelease(frameImageRef);
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, self->_imageSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
_delayTimesForIndexes = [delayTimesForIndexesMutable copy];
|
||||
_frameCount = imageCount;
|
||||
|
||||
if (self.frameCount == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties);
|
||||
return nil;
|
||||
} else if (self.frameCount == 1) {
|
||||
// Warn when we only have a single frame but return a valid GIF.
|
||||
FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
|
||||
} else {
|
||||
// We have multiple frames, rock on!
|
||||
}
|
||||
|
||||
// If no value is provided, select a default based on the GIF.
|
||||
if (optimalFrameCacheSize == 0) {
|
||||
// Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
|
||||
// It's only dependent on the image size & number of frames and never changes.
|
||||
CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE;
|
||||
if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
|
||||
_frameCacheSizeOptimal = self.frameCount;
|
||||
} else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
|
||||
// This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
|
||||
} else {
|
||||
// The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
}
|
||||
} else {
|
||||
// Use the provided value.
|
||||
_frameCacheSizeOptimal = optimalFrameCacheSize;
|
||||
}
|
||||
// In any case, cap the optimal cache size at the frame count.
|
||||
_frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
|
||||
|
||||
// Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
|
||||
_allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
|
||||
|
||||
// See the property declarations for descriptions.
|
||||
_weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
|
||||
|
||||
// Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
[allAnimatedImagesWeak addObject:self];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data
|
||||
{
|
||||
FLAnimatedImage *animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data];
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_weakProxy) {
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy];
|
||||
}
|
||||
|
||||
if (_imageSource) {
|
||||
CFRelease(_imageSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
// See header for more details.
|
||||
// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
|
||||
{
|
||||
// Early return if the requested index is beyond bounds.
|
||||
// Note: We're comparing an index with a count and need to bail on greater than or equal to.
|
||||
if (index >= self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Remember requested frame index, this influences what we should cache next.
|
||||
self.requestedFrameIndex = index;
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
|
||||
[self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
|
||||
}
|
||||
#endif
|
||||
|
||||
// Quick check to avoid doing any work if we already have all possible frames cached, a common case.
|
||||
if ([self.cachedFrameIndexes count] < self.frameCount) {
|
||||
// If we have frames that should be cached but aren't and aren't requested yet, request them.
|
||||
// Exclude existing cached frames, frames already requested, and specially cached poster image.
|
||||
NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
|
||||
NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
|
||||
|
||||
// Asynchronously add frames to our cache.
|
||||
if ([frameIndexesToAddToCache count] > 0) {
|
||||
[self addFrameIndexesToCache:frameIndexesToAddToCache];
|
||||
}
|
||||
}
|
||||
|
||||
// Get the specified image.
|
||||
UIImage *image = self.cachedFramesForIndexes[@(index)];
|
||||
|
||||
// Purge if needed based on the current playhead position.
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
|
||||
- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache
|
||||
{
|
||||
// Order matters. First, iterate over the indexes starting from the requested frame index.
|
||||
// Then, if there are any indexes before the requested frame index, do those.
|
||||
NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex);
|
||||
NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex);
|
||||
if (firstRange.length + secondRange.length != self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range.");
|
||||
}
|
||||
|
||||
// Add to the requested list before we actually kick them off, so they don't get into the queue twice.
|
||||
[self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache];
|
||||
|
||||
// Lazily create dedicated isolation queue.
|
||||
if (!self.serialQueue) {
|
||||
_serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
// Start streaming requested frames in the background into the cache.
|
||||
// Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away.
|
||||
FLAnimatedImage * __weak weakSelf = self;
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
// Produce and cache next needed frame.
|
||||
void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
#if defined(DEBUG) && DEBUG
|
||||
CFTimeInterval predrawBeginTime = CACurrentMediaTime();
|
||||
#endif
|
||||
UIImage *image = [weakSelf imageAtIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime;
|
||||
CFTimeInterval slowdownDuration = 0.0;
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) {
|
||||
CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self];
|
||||
slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration;
|
||||
[NSThread sleepForTimeInterval:slowdownDuration];
|
||||
}
|
||||
FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
|
||||
#endif
|
||||
// The results get returned one by one as soon as they're ready (and not in batch).
|
||||
// The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy.
|
||||
if (image && weakSelf) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
weakSelf.cachedFramesForIndexes[@(i)] = image;
|
||||
[weakSelf.cachedFrameIndexes addIndex:i];
|
||||
[weakSelf.requestedFrameIndexes removeIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
[weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes];
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock];
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+ (CGSize)sizeForImage:(id)image
|
||||
{
|
||||
CGSize imageSize = CGSizeZero;
|
||||
|
||||
// Early return for nil
|
||||
if (!image) {
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
if ([image isKindOfClass:[UIImage class]]) {
|
||||
UIImage *uiImage = (UIImage *)image;
|
||||
imageSize = uiImage.size;
|
||||
} else if ([image isKindOfClass:[FLAnimatedImage class]]) {
|
||||
FLAnimatedImage *animatedImage = (FLAnimatedImage *)image;
|
||||
imageSize = animatedImage.size;
|
||||
} else {
|
||||
// Bear trap to capture bad images; we have seen crashers cropping up on iOS 7.
|
||||
FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image);
|
||||
}
|
||||
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Frame Loading
|
||||
|
||||
- (UIImage *)imageAtIndex:(NSUInteger)index
|
||||
{
|
||||
// It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time.
|
||||
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
|
||||
|
||||
// Early return for nil
|
||||
if (!imageRef) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef];
|
||||
CFRelease(imageRef);
|
||||
|
||||
// Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread.
|
||||
if (self.isPredrawingEnabled) {
|
||||
image = [[self class] predrawnImageFromImage:image];
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Frame Caching
|
||||
|
||||
- (NSMutableIndexSet *)frameIndexesToCache
|
||||
{
|
||||
NSMutableIndexSet *indexesToCache = nil;
|
||||
// Quick check to avoid building the index set if the number of frames to cache equals the total frame count.
|
||||
if (self.frameCacheSizeCurrent == self.frameCount) {
|
||||
indexesToCache = [self.allFramesIndexSet mutableCopy];
|
||||
} else {
|
||||
indexesToCache = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end.
|
||||
// The second, if needed, the remaining number of frames beginning at index zero.
|
||||
NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex);
|
||||
NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength);
|
||||
[indexesToCache addIndexesInRange:firstRange];
|
||||
NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength;
|
||||
if (secondLength > 0) {
|
||||
NSRange secondRange = NSMakeRange(0, secondLength);
|
||||
[indexesToCache addIndexesInRange:secondRange];
|
||||
}
|
||||
// Double check our math, before we add the poster image index which may increase it by one.
|
||||
if ([indexesToCache count] != self.frameCacheSizeCurrent) {
|
||||
FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size.");
|
||||
}
|
||||
|
||||
[indexesToCache addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
return indexesToCache;
|
||||
}
|
||||
|
||||
|
||||
- (void)purgeFrameCacheIfNeeded
|
||||
{
|
||||
// Purge frames that are currently cached but don't need to be.
|
||||
// But not if we're still under the number of frames to cache.
|
||||
// This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below.
|
||||
if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) {
|
||||
NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy];
|
||||
[indexesToPurge removeIndexes:[self frameIndexesToCache]];
|
||||
[indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
[self.cachedFrameIndexes removeIndex:i];
|
||||
[self.cachedFramesForIndexes removeObjectForKey:@(i)];
|
||||
// Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access.
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes];
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue];
|
||||
FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
|
||||
|
||||
// Schedule resetting the frame cache size max completely after a while.
|
||||
const NSTimeInterval kResetDelay = 3.0;
|
||||
[self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay];
|
||||
}
|
||||
|
||||
|
||||
- (void)resetFrameCacheSizeMaxInternal
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit;
|
||||
FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark System Memory Warnings Notification Handler
|
||||
|
||||
- (void)didReceiveMemoryWarning:(NSNotification *)notification
|
||||
{
|
||||
self.memoryWarningCount++;
|
||||
|
||||
// If we were about to grow larger, but got rapped on our knuckles by the system again, cancel.
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)];
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil];
|
||||
|
||||
// Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand.
|
||||
FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
|
||||
// Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage.
|
||||
//
|
||||
// [mem]^ (2) (5) (6) 1) Loading frames for the first time
|
||||
// (*)| , , , 2) Mem warning #1; purge cache
|
||||
// | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs
|
||||
// | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs
|
||||
// |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache
|
||||
// |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache
|
||||
// +----------------------> 7) After 3 mem warnings, stay at minimum cache size
|
||||
// [t]
|
||||
// *) The mem high water mark before we get warned might change for every cycle.
|
||||
//
|
||||
const NSUInteger kGrowAttemptsMax = 2;
|
||||
const NSTimeInterval kGrowDelay = 2.0;
|
||||
if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) {
|
||||
[self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay];
|
||||
}
|
||||
|
||||
// Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Image Decoding
|
||||
|
||||
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
|
||||
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
|
||||
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
|
||||
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
|
||||
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
|
||||
{
|
||||
// Always use a device RGB color space for simplicity and predictability what will be going on.
|
||||
CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
|
||||
// Early return on failure!
|
||||
if (!colorSpaceDeviceRGBRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
|
||||
// kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
|
||||
// (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
|
||||
size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A
|
||||
|
||||
// "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
|
||||
void *data = NULL;
|
||||
size_t width = imageToPredraw.size.width;
|
||||
size_t height = imageToPredraw.size.height;
|
||||
size_t bitsPerComponent = CHAR_BIT;
|
||||
|
||||
size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
|
||||
size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
|
||||
size_t bytesPerRow = (bytesPerPixel * width);
|
||||
|
||||
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
|
||||
|
||||
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
|
||||
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
|
||||
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
|
||||
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
|
||||
alphaInfo = kCGImageAlphaNoneSkipFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaFirst) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaLast) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedLast;
|
||||
}
|
||||
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
|
||||
bitmapInfo |= alphaInfo;
|
||||
|
||||
// Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
|
||||
// Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
|
||||
CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpaceDeviceRGBRef);
|
||||
// Early return on failure!
|
||||
if (!bitmapContextRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Draw image in bitmap context and create image by preserving receiver's properties.
|
||||
CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
|
||||
CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
|
||||
UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation];
|
||||
CGImageRelease(predrawnImageRef);
|
||||
CGContextRelease(bitmapContextRef);
|
||||
|
||||
// Early return on failure!
|
||||
if (!predrawnImage) {
|
||||
FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
return predrawnImage;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Description
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)];
|
||||
description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount];
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
@implementation FLAnimatedImage (Logging)
|
||||
|
||||
static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil;
|
||||
static FLLogLevel _logLevel;
|
||||
|
||||
+ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel
|
||||
{
|
||||
_logBlock = logBlock;
|
||||
_logLevel = logLevel;
|
||||
}
|
||||
|
||||
+ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level
|
||||
{
|
||||
if (level <= _logLevel && _logBlock && stringBlock) {
|
||||
_logBlock(stringBlock(), level);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLWeakProxy
|
||||
|
||||
@interface FLWeakProxy ()
|
||||
|
||||
@property (nonatomic, weak) id target;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLWeakProxy
|
||||
|
||||
#pragma mark Life Cycle
|
||||
|
||||
// This is the designated creation method of an `FLWeakProxy` and
|
||||
// as a subclass of `NSProxy` it doesn't respond to or need `-init`.
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject
|
||||
{
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy alloc];
|
||||
weakProxy.target = targetObject;
|
||||
return weakProxy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Forwarding Messages
|
||||
|
||||
- (id)forwardingTargetForSelector:(SEL)selector
|
||||
{
|
||||
// Keep it lightweight: access the ivar directly
|
||||
return _target;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSWeakProxy Method Overrides
|
||||
#pragma mark Handling Unimplemented Methods
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
// Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
|
||||
// The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We can't really handle struct return types here because we don't know the length.
|
||||
void *nullPointer = NULL;
|
||||
[invocation setReturnValue:&nullPointer];
|
||||
}
|
||||
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
|
||||
{
|
||||
// We only get here if `forwardingTargetForSelector:` returns nil.
|
||||
// In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`.
|
||||
// Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well.
|
||||
// See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
|
||||
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLAnimatedImage;
|
||||
@protocol FLAnimatedImageViewDebugDelegate;
|
||||
|
||||
|
||||
//
|
||||
// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
|
||||
// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
|
||||
// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
|
||||
// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
|
||||
//
|
||||
@interface FLAnimatedImageView : UIImageView
|
||||
|
||||
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
|
||||
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
|
||||
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
|
||||
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
|
||||
|
||||
// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
|
||||
// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
|
||||
@property (nonatomic, copy) NSString *runLoopMode;
|
||||
|
||||
@end
|
|
@ -0,0 +1,442 @@
|
|||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImageView.h"
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageViewDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImageView ()
|
||||
|
||||
// Override of public `readonly` properties as private `readwrite`
|
||||
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
|
||||
|
||||
@property (nonatomic, assign) NSUInteger loopCountdown;
|
||||
@property (nonatomic, assign) NSTimeInterval accumulator;
|
||||
@property (nonatomic, strong) CADisplayLink *displayLink;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
|
||||
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLAnimatedImageView
|
||||
@synthesize runLoopMode = _runLoopMode;
|
||||
|
||||
#pragma mark - Initializers
|
||||
|
||||
// -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
|
||||
// Using -initWithImage: doesn't call any of the other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image
|
||||
{
|
||||
self = [super initWithImage:image];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
|
||||
{
|
||||
self = [super initWithImage:image highlightedImage:highlightedImage];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
self.runLoopMode = [[self class] defaultRunLoopMode];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.accessibilityIgnoresInvertColors = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
|
||||
{
|
||||
if (![_animatedImage isEqual:animatedImage]) {
|
||||
if (animatedImage) {
|
||||
// Clear out the image.
|
||||
super.image = nil;
|
||||
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
|
||||
super.highlighted = NO;
|
||||
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
|
||||
[self invalidateIntrinsicContentSize];
|
||||
} else {
|
||||
// Stop animating before the animated image gets cleared out.
|
||||
[self stopAnimating];
|
||||
}
|
||||
|
||||
_animatedImage = animatedImage;
|
||||
|
||||
self.currentFrame = animatedImage.posterImage;
|
||||
self.currentFrameIndex = 0;
|
||||
if (animatedImage.loopCount > 0) {
|
||||
self.loopCountdown = animatedImage.loopCount;
|
||||
} else {
|
||||
self.loopCountdown = NSUIntegerMax;
|
||||
}
|
||||
self.accumulator = 0.0;
|
||||
|
||||
// Start animating after the new animated image has been set.
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
}
|
||||
|
||||
[self.layer setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Removes the display link from all run loop modes.
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIView Method Overrides
|
||||
#pragma mark Observing View-Related Changes
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAlpha:(CGFloat)alpha
|
||||
{
|
||||
[super setAlpha:alpha];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Auto Layout
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
// Default to let UIImageView handle the sizing of its image, and anything else it might consider.
|
||||
CGSize intrinsicContentSize = [super intrinsicContentSize];
|
||||
|
||||
// If we have have an animated image, use its image size.
|
||||
// UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
|
||||
// (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
|
||||
if (self.animatedImage) {
|
||||
intrinsicContentSize = self.image.size;
|
||||
}
|
||||
|
||||
return intrinsicContentSize;
|
||||
}
|
||||
|
||||
#pragma mark Smart Invert Colors
|
||||
|
||||
#pragma mark - UIImageView Method Overrides
|
||||
#pragma mark Image Data
|
||||
|
||||
- (UIImage *)image
|
||||
{
|
||||
UIImage *image = nil;
|
||||
if (self.animatedImage) {
|
||||
// Initially set to the poster image.
|
||||
image = self.currentFrame;
|
||||
} else {
|
||||
image = super.image;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
if (image) {
|
||||
// Clear out the animated image and implicitly pause animation playback.
|
||||
self.animatedImage = nil;
|
||||
}
|
||||
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Animating Images
|
||||
|
||||
- (NSTimeInterval)frameDelayGreatestCommonDivisor
|
||||
{
|
||||
// Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
|
||||
const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues;
|
||||
|
||||
// Scales the frame delays by `kGreatestCommonDivisorPrecision`
|
||||
// then converts it to an UInteger for in order to calculate the GCD.
|
||||
NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
|
||||
for (NSNumber *value in delays) {
|
||||
scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
|
||||
}
|
||||
|
||||
// Reverse to scale to get the value back into seconds.
|
||||
return scaledGCD / kGreatestCommonDivisorPrecision;
|
||||
}
|
||||
|
||||
|
||||
static NSUInteger gcd(NSUInteger a, NSUInteger b)
|
||||
{
|
||||
// http://en.wikipedia.org/wiki/Greatest_common_divisor
|
||||
if (a < b) {
|
||||
return gcd(b, a);
|
||||
} else if (a == b) {
|
||||
return b;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
NSUInteger remainder = a % b;
|
||||
if (remainder == 0) {
|
||||
return b;
|
||||
}
|
||||
a = b;
|
||||
b = remainder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)startAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
// Lazily create the display link.
|
||||
if (!self.displayLink) {
|
||||
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
|
||||
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
|
||||
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
|
||||
// link which will lead to the deallocation of both the display link and the weak proxy.
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
|
||||
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
|
||||
|
||||
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
|
||||
}
|
||||
|
||||
// Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
|
||||
// Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
|
||||
const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
|
||||
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
|
||||
|
||||
self.displayLink.paused = NO;
|
||||
} else {
|
||||
[super startAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRunLoopMode:(NSString *)runLoopMode
|
||||
{
|
||||
if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
|
||||
NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
|
||||
_runLoopMode = [[self class] defaultRunLoopMode];
|
||||
} else {
|
||||
_runLoopMode = runLoopMode;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
self.displayLink.paused = YES;
|
||||
} else {
|
||||
[super stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isAnimating
|
||||
{
|
||||
BOOL isAnimating = NO;
|
||||
if (self.animatedImage) {
|
||||
isAnimating = self.displayLink && !self.displayLink.isPaused;
|
||||
} else {
|
||||
isAnimating = [super isAnimating];
|
||||
}
|
||||
return isAnimating;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Highlighted Image Unsupport
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
// Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
|
||||
if (!self.animatedImage) {
|
||||
[super setHighlighted:highlighted];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Animation
|
||||
|
||||
// Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
|
||||
// Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
|
||||
- (void)updateShouldAnimate
|
||||
{
|
||||
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
|
||||
self.shouldAnimate = self.animatedImage && isVisible;
|
||||
}
|
||||
|
||||
|
||||
- (void)displayDidRefresh:(CADisplayLink *)displayLink
|
||||
{
|
||||
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
|
||||
// Early return!
|
||||
if (!self.shouldAnimate) {
|
||||
FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
|
||||
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
|
||||
if (delayTimeNumber) {
|
||||
NSTimeInterval delayTime = [delayTimeNumber floatValue];
|
||||
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
|
||||
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
|
||||
if (image) {
|
||||
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
self.currentFrame = image;
|
||||
if (self.needsDisplayWhenImageBecomesAvailable) {
|
||||
[self.layer setNeedsDisplay];
|
||||
self.needsDisplayWhenImageBecomesAvailable = NO;
|
||||
}
|
||||
|
||||
self.accumulator += displayLink.duration * displayLink.frameInterval;
|
||||
|
||||
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
|
||||
while (self.accumulator >= delayTime) {
|
||||
self.accumulator -= delayTime;
|
||||
self.currentFrameIndex++;
|
||||
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
|
||||
// If we've looped the number of times that this animated image describes, stop looping.
|
||||
self.loopCountdown--;
|
||||
if (self.loopCompletionBlock) {
|
||||
self.loopCompletionBlock(self.loopCountdown);
|
||||
}
|
||||
|
||||
if (self.loopCountdown == 0) {
|
||||
[self stopAnimating];
|
||||
return;
|
||||
}
|
||||
self.currentFrameIndex = 0;
|
||||
}
|
||||
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
|
||||
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
|
||||
self.needsDisplayWhenImageBecomesAvailable = YES;
|
||||
}
|
||||
} else {
|
||||
FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
|
||||
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
self.currentFrameIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)defaultRunLoopMode
|
||||
{
|
||||
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
|
||||
return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - CALayerDelegate (Informal)
|
||||
#pragma mark Providing the Layer's Content
|
||||
|
||||
- (void)displayLayer:(CALayer *)layer
|
||||
{
|
||||
layer.contents = (__bridge id)self.image.CGImage;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en_US</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>访问相机以拍照</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>允许定位以把位置保存到照片中</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>允许定位以把位置保存到照片中</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>访问麦克风以录像</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>访问相册以选择照片</string>
|
||||
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// TZLocationManager.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 2017/06/03.
|
||||
// Copyright © 2017年 谭真. All rights reserved.
|
||||
// 定位管理类
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
@interface TZLocationManager : NSObject
|
||||
|
||||
+ (instancetype)manager NS_SWIFT_NAME(default());
|
||||
|
||||
/// 开始定位
|
||||
- (void)startLocation;
|
||||
- (void)startLocationWithSuccessBlock:(void (^)(NSArray<CLLocation *> *))successBlock failureBlock:(void (^)(NSError *error))failureBlock;
|
||||
- (void)startLocationWithGeocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock;
|
||||
- (void)startLocationWithSuccessBlock:(void (^)(NSArray<CLLocation *> *))successBlock failureBlock:(void (^)(NSError *error))failureBlock geocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock;
|
||||
|
||||
/// 结束定位
|
||||
- (void)stopUpdatingLocation;
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// TZLocationManager.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 2017/06/03.
|
||||
// Copyright © 2017年 谭真. All rights reserved.
|
||||
// 定位管理类
|
||||
|
||||
#import "TZLocationManager.h"
|
||||
|
||||
@interface TZLocationManager ()<CLLocationManagerDelegate>
|
||||
@property (nonatomic, strong) CLLocationManager *locationManager;
|
||||
/// 定位成功的回调block
|
||||
@property (nonatomic, copy) void (^successBlock)(NSArray<CLLocation *> *);
|
||||
/// 编码成功的回调block
|
||||
@property (nonatomic, copy) void (^geocodeBlock)(NSArray *geocodeArray);
|
||||
/// 定位失败的回调block
|
||||
@property (nonatomic, copy) void (^failureBlock)(NSError *error);
|
||||
@end
|
||||
|
||||
@implementation TZLocationManager
|
||||
|
||||
+ (instancetype)manager {
|
||||
static TZLocationManager *manager;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
manager = [[self alloc] init];
|
||||
manager.locationManager = [[CLLocationManager alloc] init];
|
||||
manager.locationManager.delegate = manager;
|
||||
[manager.locationManager requestWhenInUseAuthorization];
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (void)startLocation {
|
||||
[self startLocationWithSuccessBlock:nil failureBlock:nil geocoderBlock:nil];
|
||||
}
|
||||
|
||||
- (void)startLocationWithSuccessBlock:(void (^)(NSArray<CLLocation *> *))successBlock failureBlock:(void (^)(NSError *error))failureBlock {
|
||||
[self startLocationWithSuccessBlock:successBlock failureBlock:failureBlock geocoderBlock:nil];
|
||||
}
|
||||
|
||||
- (void)startLocationWithGeocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock {
|
||||
[self startLocationWithSuccessBlock:nil failureBlock:nil geocoderBlock:geocoderBlock];
|
||||
}
|
||||
|
||||
- (void)startLocationWithSuccessBlock:(void (^)(NSArray<CLLocation *> *))successBlock failureBlock:(void (^)(NSError *error))failureBlock geocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock {
|
||||
[self.locationManager startUpdatingLocation];
|
||||
_successBlock = successBlock;
|
||||
_geocodeBlock = geocoderBlock;
|
||||
_failureBlock = failureBlock;
|
||||
}
|
||||
|
||||
- (void)stopUpdatingLocation {
|
||||
[self.locationManager stopUpdatingLocation];
|
||||
}
|
||||
|
||||
#pragma mark - CLLocationManagerDelegate
|
||||
|
||||
/// 地理位置发生改变时触发
|
||||
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
[manager stopUpdatingLocation];
|
||||
|
||||
if (_successBlock) {
|
||||
_successBlock(locations);
|
||||
}
|
||||
|
||||
if (_geocodeBlock && locations.count) {
|
||||
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
|
||||
[geocoder reverseGeocodeLocation:[locations firstObject] completionHandler:^(NSArray *array, NSError *error) {
|
||||
self->_geocodeBlock(array);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/// 定位失败回调方法
|
||||
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
|
||||
NSLog(@"定位失败, 错误: %@",error);
|
||||
switch([error code]) {
|
||||
case kCLErrorDenied: { // 用户禁止了定位权限
|
||||
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
if (_failureBlock) {
|
||||
_failureBlock(error);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// LxGridViewFlowLayout.h
|
||||
// LxGridView
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/*
|
||||
此类来源于DeveloperLx的优秀开源项目:LxGridView
|
||||
github链接:https://github.com/DeveloperLx/LxGridView
|
||||
我对这个类的代码做了一些修改;
|
||||
感谢DeveloperLx的优秀代码~
|
||||
*/
|
||||
|
||||
@interface LxGridViewFlowLayout : UICollectionViewFlowLayout
|
||||
|
||||
@property (nonatomic,assign) BOOL panGestureRecognizerEnable;
|
||||
|
||||
@end
|
||||
|
||||
@protocol LxGridViewDataSource <UICollectionViewDataSource>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
itemAtIndexPath:(NSIndexPath *)sourceIndexPath
|
||||
willMoveToIndexPath:(NSIndexPath *)destinationIndexPath;
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
itemAtIndexPath:(NSIndexPath *)sourceIndexPath
|
||||
didMoveToIndexPath:(NSIndexPath *)destinationIndexPath;
|
||||
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView
|
||||
canMoveItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView
|
||||
itemAtIndexPath:(NSIndexPath *)sourceIndexPath
|
||||
canMoveToIndexPath:(NSIndexPath *)destinationIndexPath;
|
||||
|
||||
@end
|
||||
|
||||
@protocol LxGridViewDelegateFlowLayout <UICollectionViewDelegateFlowLayout>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
willBeginDraggingItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
didBeginDraggingItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
willEndDraggingItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
- (void)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
didEndDraggingItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
|
||||
@end
|
|
@ -0,0 +1,408 @@
|
|||
//
|
||||
// LxGridViewFlowLayout.m
|
||||
// LxGridView
|
||||
//
|
||||
|
||||
#import "LxGridViewFlowLayout.h"
|
||||
#import "TZTestCell.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
|
||||
#define stringify __STRING
|
||||
|
||||
static CGFloat const PRESS_TO_MOVE_MIN_DURATION = 0.1;
|
||||
static CGFloat const MIN_PRESS_TO_BEGIN_EDITING_DURATION = 0.6;
|
||||
|
||||
CG_INLINE CGPoint CGPointOffset(CGPoint point, CGFloat dx, CGFloat dy)
|
||||
{
|
||||
return CGPointMake(point.x + dx, point.y + dy);
|
||||
}
|
||||
|
||||
/*
|
||||
此类来源于DeveloperLx的优秀开源项目:LxGridView
|
||||
github链接:https://github.com/DeveloperLx/LxGridView
|
||||
我对这个类的代码做了一些修改;
|
||||
感谢DeveloperLx的优秀代码~
|
||||
*/
|
||||
|
||||
@interface LxGridViewFlowLayout () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic,readonly) id<LxGridViewDataSource> dataSource;
|
||||
@property (nonatomic,readonly) id<LxGridViewDelegateFlowLayout> delegate;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LxGridViewFlowLayout
|
||||
{
|
||||
UILongPressGestureRecognizer * _longPressGestureRecognizer;
|
||||
UIPanGestureRecognizer * _panGestureRecognizer;
|
||||
NSIndexPath * _movingItemIndexPath;
|
||||
UIView * _beingMovedPromptView;
|
||||
CGPoint _sourceItemCollectionViewCellCenter;
|
||||
|
||||
CADisplayLink * _displayLink;
|
||||
CFTimeInterval _remainSecondsToBeginEditing;
|
||||
}
|
||||
|
||||
#pragma mark - setup
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
|
||||
[self removeGestureRecognizers];
|
||||
[self removeObserver:self forKeyPath:@stringify(collectionView)];
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
[self setup];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if (self = [super initWithCoder:coder]) {
|
||||
[self setup];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setup
|
||||
{
|
||||
[self addObserver:self forKeyPath:@stringify(collectionView) options:NSKeyValueObservingOptionNew context:nil];
|
||||
}
|
||||
|
||||
- (void)addGestureRecognizers
|
||||
{
|
||||
self.collectionView.userInteractionEnabled = YES;
|
||||
|
||||
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressGestureRecognizerTriggerd:)];
|
||||
_longPressGestureRecognizer.cancelsTouchesInView = NO;
|
||||
_longPressGestureRecognizer.minimumPressDuration = PRESS_TO_MOVE_MIN_DURATION;
|
||||
_longPressGestureRecognizer.delegate = self;
|
||||
|
||||
for (UIGestureRecognizer * gestureRecognizer in self.collectionView.gestureRecognizers) {
|
||||
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
|
||||
[gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer];
|
||||
}
|
||||
}
|
||||
|
||||
[self.collectionView addGestureRecognizer:_longPressGestureRecognizer];
|
||||
|
||||
_panGestureRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizerTriggerd:)];
|
||||
_panGestureRecognizer.delegate = self;
|
||||
[self.collectionView addGestureRecognizer:_panGestureRecognizer];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)removeGestureRecognizers
|
||||
{
|
||||
if (_longPressGestureRecognizer) {
|
||||
if (_longPressGestureRecognizer.view) {
|
||||
[_longPressGestureRecognizer.view removeGestureRecognizer:_longPressGestureRecognizer];
|
||||
}
|
||||
_longPressGestureRecognizer = nil;
|
||||
}
|
||||
|
||||
if (_panGestureRecognizer) {
|
||||
if (_panGestureRecognizer.view) {
|
||||
[_panGestureRecognizer.view removeGestureRecognizer:_panGestureRecognizer];
|
||||
}
|
||||
_panGestureRecognizer = nil;
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
#pragma mark - getter and setter implementation
|
||||
|
||||
- (id<LxGridViewDataSource>)dataSource
|
||||
{
|
||||
return (id<LxGridViewDataSource>)self.collectionView.dataSource;
|
||||
}
|
||||
|
||||
- (id<LxGridViewDelegateFlowLayout>)delegate
|
||||
{
|
||||
return (id<LxGridViewDelegateFlowLayout>)self.collectionView.delegate;
|
||||
}
|
||||
|
||||
#pragma mark - override UICollectionViewLayout methods
|
||||
|
||||
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
|
||||
{
|
||||
NSArray * layoutAttributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
|
||||
|
||||
for (UICollectionViewLayoutAttributes * layoutAttributes in layoutAttributesForElementsInRect) {
|
||||
|
||||
if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
|
||||
layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath];
|
||||
}
|
||||
}
|
||||
return layoutAttributesForElementsInRect;
|
||||
}
|
||||
|
||||
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UICollectionViewLayoutAttributes * layoutAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
|
||||
if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
|
||||
layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath];
|
||||
}
|
||||
return layoutAttributes;
|
||||
}
|
||||
|
||||
#pragma mark - gesture
|
||||
|
||||
- (void)setPanGestureRecognizerEnable:(BOOL)panGestureRecognizerEnable
|
||||
{
|
||||
_panGestureRecognizer.enabled = panGestureRecognizerEnable;
|
||||
}
|
||||
|
||||
- (BOOL)panGestureRecognizerEnable
|
||||
{
|
||||
return _panGestureRecognizer.enabled;
|
||||
}
|
||||
|
||||
- (void)longPressGestureRecognizerTriggerd:(UILongPressGestureRecognizer *)longPress
|
||||
{
|
||||
switch (longPress.state) {
|
||||
case UIGestureRecognizerStatePossible:
|
||||
break;
|
||||
case UIGestureRecognizerStateBegan:
|
||||
{
|
||||
if (_displayLink == nil) {
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTriggered:)];
|
||||
_displayLink.frameInterval = 6;
|
||||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
|
||||
_remainSecondsToBeginEditing = MIN_PRESS_TO_BEGIN_EDITING_DURATION;
|
||||
}
|
||||
|
||||
_movingItemIndexPath = [self.collectionView indexPathForItemAtPoint:[longPress locationInView:self.collectionView]];
|
||||
if ([self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] && [self.dataSource collectionView:self.collectionView canMoveItemAtIndexPath:_movingItemIndexPath] == NO) {
|
||||
_movingItemIndexPath = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(collectionView:layout:willBeginDraggingItemAtIndexPath:)]) {
|
||||
[self.delegate collectionView:self.collectionView layout:self willBeginDraggingItemAtIndexPath:_movingItemIndexPath];
|
||||
}
|
||||
|
||||
UICollectionViewCell *sourceCollectionViewCell = [self.collectionView cellForItemAtIndexPath:_movingItemIndexPath];
|
||||
TZTestCell *sourceCell = (TZTestCell *)sourceCollectionViewCell;
|
||||
|
||||
_beingMovedPromptView = [[UIView alloc]initWithFrame:CGRectOffset(sourceCollectionViewCell.frame, -10, -10)];
|
||||
_beingMovedPromptView.tz_width += 20;
|
||||
_beingMovedPromptView.tz_height += 20;
|
||||
|
||||
sourceCollectionViewCell.highlighted = YES;
|
||||
UIView * highlightedSnapshotView = [sourceCell snapshotView];
|
||||
highlightedSnapshotView.frame = _beingMovedPromptView.bounds;
|
||||
highlightedSnapshotView.alpha = 1;
|
||||
|
||||
sourceCollectionViewCell.highlighted = NO;
|
||||
UIView * snapshotView = [sourceCell snapshotView];
|
||||
snapshotView.frame = _beingMovedPromptView.bounds;
|
||||
snapshotView.alpha = 0;
|
||||
|
||||
[_beingMovedPromptView addSubview:snapshotView];
|
||||
[_beingMovedPromptView addSubview:highlightedSnapshotView];
|
||||
[self.collectionView addSubview:_beingMovedPromptView];
|
||||
|
||||
_sourceItemCollectionViewCellCenter = sourceCollectionViewCell.center;
|
||||
|
||||
typeof(self) __weak weakSelf = self;
|
||||
[UIView animateWithDuration:0
|
||||
delay:0
|
||||
options:UIViewAnimationOptionBeginFromCurrentState
|
||||
animations:^{
|
||||
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
highlightedSnapshotView.alpha = 0;
|
||||
snapshotView.alpha = 1;
|
||||
}
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
[highlightedSnapshotView removeFromSuperview];
|
||||
|
||||
if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didBeginDraggingItemAtIndexPath:)]) {
|
||||
[strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didBeginDraggingItemAtIndexPath:self->_movingItemIndexPath];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
[self invalidateLayout];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged:
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
|
||||
NSIndexPath * movingItemIndexPath = _movingItemIndexPath;
|
||||
|
||||
if (movingItemIndexPath) {
|
||||
if ([self.delegate respondsToSelector:@selector(collectionView:layout:willEndDraggingItemAtIndexPath:)]) {
|
||||
[self.delegate collectionView:self.collectionView layout:self willEndDraggingItemAtIndexPath:movingItemIndexPath];
|
||||
}
|
||||
|
||||
_movingItemIndexPath = nil;
|
||||
_sourceItemCollectionViewCellCenter = CGPointZero;
|
||||
|
||||
UICollectionViewLayoutAttributes * movingItemCollectionViewLayoutAttributes = [self layoutAttributesForItemAtIndexPath:movingItemIndexPath];
|
||||
|
||||
_longPressGestureRecognizer.enabled = NO;
|
||||
|
||||
typeof(self) __weak weakSelf = self;
|
||||
[UIView animateWithDuration:0.2
|
||||
delay:0
|
||||
options:UIViewAnimationOptionBeginFromCurrentState
|
||||
animations:^{
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
self->_beingMovedPromptView.center = movingItemCollectionViewLayoutAttributes.center;
|
||||
}
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
|
||||
self->_longPressGestureRecognizer.enabled = YES;
|
||||
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
[self->_beingMovedPromptView removeFromSuperview];
|
||||
self->_beingMovedPromptView = nil;
|
||||
[strongSelf invalidateLayout];
|
||||
|
||||
if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didEndDraggingItemAtIndexPath:)]) {
|
||||
[strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didEndDraggingItemAtIndexPath:movingItemIndexPath];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateFailed:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)panGestureRecognizerTriggerd:(UIPanGestureRecognizer *)pan
|
||||
{
|
||||
switch (pan.state) {
|
||||
case UIGestureRecognizerStatePossible:
|
||||
break;
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged:
|
||||
{
|
||||
CGPoint panTranslation = [pan translationInView:self.collectionView];
|
||||
_beingMovedPromptView.center = CGPointOffset(_sourceItemCollectionViewCellCenter, panTranslation.x, panTranslation.y);
|
||||
|
||||
NSIndexPath * sourceIndexPath = _movingItemIndexPath;
|
||||
NSIndexPath * destinationIndexPath = [self.collectionView indexPathForItemAtPoint:_beingMovedPromptView.center];
|
||||
|
||||
if ((destinationIndexPath == nil) || [destinationIndexPath isEqual:sourceIndexPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:canMoveToIndexPath:)] && [self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath canMoveToIndexPath:destinationIndexPath] == NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:willMoveToIndexPath:)]) {
|
||||
[self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath];
|
||||
}
|
||||
|
||||
_movingItemIndexPath = destinationIndexPath;
|
||||
|
||||
typeof(self) __weak weakSelf = self;
|
||||
[self.collectionView performBatchUpdates:^{
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
if (sourceIndexPath && destinationIndexPath) {
|
||||
[strongSelf.collectionView deleteItemsAtIndexPaths:@[sourceIndexPath]];
|
||||
[strongSelf.collectionView insertItemsAtIndexPaths:@[destinationIndexPath]];
|
||||
}
|
||||
}
|
||||
} completion:^(BOOL finished) {
|
||||
typeof(self) __strong strongSelf = weakSelf;
|
||||
if ([strongSelf.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)]) {
|
||||
[strongSelf.dataSource collectionView:strongSelf.collectionView itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath];
|
||||
}
|
||||
}];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
break;
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
break;
|
||||
case UIGestureRecognizerStateFailed:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if ([_panGestureRecognizer isEqual:gestureRecognizer]) {
|
||||
return _movingItemIndexPath != nil;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
// only _longPressGestureRecognizer and _panGestureRecognizer can recognize simultaneously
|
||||
if ([_longPressGestureRecognizer isEqual:gestureRecognizer]) {
|
||||
return [_panGestureRecognizer isEqual:otherGestureRecognizer];
|
||||
}
|
||||
if ([_panGestureRecognizer isEqual:gestureRecognizer]) {
|
||||
return [_longPressGestureRecognizer isEqual:otherGestureRecognizer];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - displayLink
|
||||
|
||||
- (void)displayLinkTriggered:(CADisplayLink *)displayLink
|
||||
{
|
||||
if (_remainSecondsToBeginEditing <= 0) {
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
_remainSecondsToBeginEditing = _remainSecondsToBeginEditing - 0.1;
|
||||
}
|
||||
|
||||
#pragma mark - KVO and notification
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
if ([keyPath isEqualToString:@stringify(collectionView)]) {
|
||||
if (self.collectionView) {
|
||||
[self addGestureRecognizers];
|
||||
}
|
||||
else {
|
||||
[self removeGestureRecognizers];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notificaiton
|
||||
{
|
||||
_panGestureRecognizer.enabled = NO;
|
||||
_panGestureRecognizer.enabled = YES;
|
||||
}
|
||||
|
||||
@end
|
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 326 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 107 KiB |
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// NSBundle+TZImagePicker.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 16/08/18.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface NSBundle (TZImagePicker)
|
||||
|
||||
+ (NSBundle *)tz_imagePickerBundle;
|
||||
|
||||
+ (NSString *)tz_localizedStringForKey:(NSString *)key value:(NSString *)value;
|
||||
+ (NSString *)tz_localizedStringForKey:(NSString *)key;
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// NSBundle+TZImagePicker.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 16/08/18.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSBundle+TZImagePicker.h"
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@implementation NSBundle (TZImagePicker)
|
||||
|
||||
+ (NSBundle *)tz_imagePickerBundle {
|
||||
#ifdef SWIFT_PACKAGE
|
||||
NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
|
||||
#else
|
||||
NSBundle *bundle = [NSBundle bundleForClass:[TZImagePickerController class]];
|
||||
#endif
|
||||
NSURL *url = [bundle URLForResource:@"TZImagePickerController" withExtension:@"bundle"];
|
||||
bundle = [NSBundle bundleWithURL:url];
|
||||
return bundle;
|
||||
}
|
||||
|
||||
+ (NSString *)tz_localizedStringForKey:(NSString *)key {
|
||||
return [self tz_localizedStringForKey:key value:@""];
|
||||
}
|
||||
|
||||
+ (NSString *)tz_localizedStringForKey:(NSString *)key value:(NSString *)value {
|
||||
NSBundle *bundle = [TZImagePickerConfig sharedInstance].languageBundle;
|
||||
NSString *value1 = [bundle localizedStringForKey:key value:value table:nil];
|
||||
return value1;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// TZAssetCell.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
TZAssetCellTypePhoto = 0,
|
||||
TZAssetCellTypeLivePhoto,
|
||||
TZAssetCellTypePhotoGif,
|
||||
TZAssetCellTypeVideo,
|
||||
TZAssetCellTypeAudio,
|
||||
} TZAssetCellType;
|
||||
|
||||
@class TZAssetModel;
|
||||
@interface TZAssetCell : UICollectionViewCell
|
||||
@property (weak, nonatomic) UIButton *selectPhotoButton;
|
||||
@property (weak, nonatomic) UIButton *cannotSelectLayerButton;
|
||||
@property (nonatomic, strong) TZAssetModel *model;
|
||||
@property (assign, nonatomic) NSInteger index;
|
||||
@property (nonatomic, copy) void (^didSelectPhotoBlock)(BOOL);
|
||||
@property (nonatomic, assign) TZAssetCellType type;
|
||||
@property (nonatomic, assign) BOOL allowPickingGif;
|
||||
@property (nonatomic, assign) BOOL allowPickingMultipleVideo;
|
||||
@property (nonatomic, copy) NSString *representedAssetIdentifier;
|
||||
@property (nonatomic, assign) int32_t imageRequestID;
|
||||
|
||||
@property (nonatomic, strong) UIImage *photoSelImage;
|
||||
@property (nonatomic, strong) UIImage *photoDefImage;
|
||||
|
||||
@property (nonatomic, assign) BOOL showSelectBtn;
|
||||
@property (assign, nonatomic) BOOL allowPreview;
|
||||
|
||||
@property (nonatomic, copy) void (^assetCellDidSetModelBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView);
|
||||
@property (nonatomic, copy) void (^assetCellDidLayoutSubviewsBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView);
|
||||
@end
|
||||
|
||||
|
||||
@class TZAlbumModel;
|
||||
@interface TZAlbumCell : UITableViewCell
|
||||
@property (nonatomic, strong) TZAlbumModel *model;
|
||||
@property (weak, nonatomic) UIButton *selectedCountButton;
|
||||
|
||||
@property (nonatomic, copy) void (^albumCellDidSetModelBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel);
|
||||
@property (nonatomic, copy) void (^albumCellDidLayoutSubviewsBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel);
|
||||
@end
|
||||
|
||||
|
||||
@interface TZAssetCameraCell : UICollectionViewCell
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZAssetAddMoreCell : TZAssetCameraCell
|
||||
@property (nonatomic, strong) UILabel *tipLabel;
|
||||
@end
|
|
@ -0,0 +1,548 @@
|
|||
//
|
||||
// TZAssetCell.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZAssetCell.h"
|
||||
#import "TZAssetModel.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
#import "TZImageManager.h"
|
||||
#import "TZImagePickerController.h"
|
||||
#import "TZProgressView.h"
|
||||
|
||||
@interface TZAssetCell ()
|
||||
@property (weak, nonatomic) UIImageView *imageView; // The photo / 照片
|
||||
@property (weak, nonatomic) UIImageView *selectImageView;
|
||||
@property (weak, nonatomic) UILabel *indexLabel;
|
||||
@property (weak, nonatomic) UIView *bottomView;
|
||||
@property (weak, nonatomic) UILabel *timeLength;
|
||||
@property (strong, nonatomic) UITapGestureRecognizer *tapGesture;
|
||||
|
||||
@property (nonatomic, weak) UIImageView *videoImgView;
|
||||
@property (nonatomic, strong) TZProgressView *progressView;
|
||||
@property (nonatomic, assign) int32_t bigImageRequestID;
|
||||
@end
|
||||
|
||||
@implementation TZAssetCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload:) name:@"TZ_PHOTO_PICKER_RELOAD_NOTIFICATION" object:nil];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAssetModel *)model {
|
||||
_model = model;
|
||||
self.representedAssetIdentifier = model.asset.localIdentifier;
|
||||
int32_t imageRequestID = [[TZImageManager manager] getPhotoWithAsset:model.asset photoWidth:self.tz_width completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
// Set the cell's thumbnail image if it's still showing the same asset.
|
||||
if ([self.representedAssetIdentifier isEqualToString:model.asset.localIdentifier]) {
|
||||
self.imageView.image = photo;
|
||||
[self setNeedsLayout];
|
||||
} else {
|
||||
// NSLog(@"this cell is showing other asset");
|
||||
[[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID];
|
||||
}
|
||||
if (!isDegraded) {
|
||||
[self hideProgressView];
|
||||
self.imageRequestID = 0;
|
||||
}
|
||||
} progressHandler:nil networkAccessAllowed:NO];
|
||||
if (imageRequestID && self.imageRequestID && imageRequestID != self.imageRequestID) {
|
||||
[[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID];
|
||||
// NSLog(@"cancelImageRequest %d",self.imageRequestID);
|
||||
}
|
||||
self.imageRequestID = imageRequestID;
|
||||
self.selectPhotoButton.selected = model.isSelected;
|
||||
self.selectImageView.image = self.selectPhotoButton.isSelected ? self.photoSelImage : self.photoDefImage;
|
||||
self.indexLabel.hidden = !self.selectPhotoButton.isSelected;
|
||||
|
||||
self.type = (NSInteger)model.type;
|
||||
// 让宽度/高度小于 最小可选照片尺寸 的图片不能选中
|
||||
if (![[TZImageManager manager] isPhotoSelectableWithAsset:model.asset]) {
|
||||
if (_selectImageView.hidden == NO) {
|
||||
self.selectPhotoButton.hidden = YES;
|
||||
_selectImageView.hidden = YES;
|
||||
}
|
||||
}
|
||||
// 如果用户选中了该图片,提前获取一下大图
|
||||
if (model.isSelected) {
|
||||
[self requestBigImage];
|
||||
} else {
|
||||
[self cancelBigImageRequest];
|
||||
}
|
||||
[self setNeedsLayout];
|
||||
|
||||
if (self.assetCellDidSetModelBlock) {
|
||||
self.assetCellDidSetModelBlock(self, _imageView, _selectImageView, _indexLabel, _bottomView, _timeLength, _videoImgView);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setIndex:(NSInteger)index {
|
||||
_index = index;
|
||||
self.indexLabel.text = [NSString stringWithFormat:@"%zd", index];
|
||||
[self.contentView bringSubviewToFront:self.indexLabel];
|
||||
}
|
||||
|
||||
- (void)setShowSelectBtn:(BOOL)showSelectBtn {
|
||||
_showSelectBtn = showSelectBtn;
|
||||
BOOL selectable = [[TZImageManager manager] isPhotoSelectableWithAsset:self.model.asset];
|
||||
if (!self.selectPhotoButton.hidden) {
|
||||
self.selectPhotoButton.hidden = !showSelectBtn || !selectable;
|
||||
}
|
||||
if (!self.selectImageView.hidden) {
|
||||
self.selectImageView.hidden = !showSelectBtn || !selectable;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setType:(TZAssetCellType)type {
|
||||
_type = type;
|
||||
if (type == TZAssetCellTypePhoto || type == TZAssetCellTypeLivePhoto || (type == TZAssetCellTypePhotoGif && !self.allowPickingGif) || self.allowPickingMultipleVideo) {
|
||||
_selectImageView.hidden = NO;
|
||||
_selectPhotoButton.hidden = NO;
|
||||
_bottomView.hidden = YES;
|
||||
} else { // Video of Gif
|
||||
_selectImageView.hidden = YES;
|
||||
_selectPhotoButton.hidden = YES;
|
||||
}
|
||||
|
||||
if (type == TZAssetCellTypeVideo) {
|
||||
self.bottomView.hidden = NO;
|
||||
self.timeLength.text = _model.timeLength;
|
||||
self.videoImgView.hidden = NO;
|
||||
_timeLength.tz_left = self.videoImgView.tz_right;
|
||||
_timeLength.textAlignment = NSTextAlignmentRight;
|
||||
} else if (type == TZAssetCellTypePhotoGif && self.allowPickingGif) {
|
||||
self.bottomView.hidden = NO;
|
||||
self.timeLength.text = @"GIF";
|
||||
self.videoImgView.hidden = YES;
|
||||
_timeLength.tz_left = 5;
|
||||
_timeLength.textAlignment = NSTextAlignmentLeft;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAllowPreview:(BOOL)allowPreview {
|
||||
_allowPreview = allowPreview;
|
||||
if (allowPreview) {
|
||||
_imageView.userInteractionEnabled = NO;
|
||||
_tapGesture.enabled = NO;
|
||||
} else {
|
||||
_imageView.userInteractionEnabled = YES;
|
||||
_tapGesture.enabled = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)selectPhotoButtonClick:(UIButton *)sender {
|
||||
if (self.didSelectPhotoBlock) {
|
||||
self.didSelectPhotoBlock(sender.isSelected);
|
||||
}
|
||||
self.selectImageView.image = sender.isSelected ? self.photoSelImage : self.photoDefImage;
|
||||
if (sender.isSelected) {
|
||||
[UIView showOscillatoryAnimationWithLayer:_selectImageView.layer type:TZOscillatoryAnimationToBigger];
|
||||
// 用户选中了该图片,提前获取一下大图
|
||||
[self requestBigImage];
|
||||
} else { // 取消选中,取消大图的获取
|
||||
[self cancelBigImageRequest];
|
||||
}
|
||||
}
|
||||
|
||||
/// 只在单选状态且allowPreview为NO时会有该事件
|
||||
- (void)didTapImageView {
|
||||
if (self.didSelectPhotoBlock) {
|
||||
self.didSelectPhotoBlock(NO);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hideProgressView {
|
||||
if (_progressView) {
|
||||
self.progressView.hidden = YES;
|
||||
self.imageView.alpha = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)requestBigImage {
|
||||
if (_bigImageRequestID) {
|
||||
[[PHImageManager defaultManager] cancelImageRequest:_bigImageRequestID];
|
||||
}
|
||||
|
||||
_bigImageRequestID = [[TZImageManager manager] requestImageDataForAsset:_model.asset completion:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
|
||||
BOOL iCloudSyncFailed = !imageData && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.model.iCloudFailed = iCloudSyncFailed;
|
||||
if (iCloudSyncFailed && self.didSelectPhotoBlock) {
|
||||
self.didSelectPhotoBlock(YES);
|
||||
self.selectImageView.image = self.photoDefImage;
|
||||
}
|
||||
[self hideProgressView];
|
||||
} progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
if (self.model.isSelected) {
|
||||
progress = progress > 0.02 ? progress : 0.02;;
|
||||
self.progressView.progress = progress;
|
||||
self.progressView.hidden = NO;
|
||||
self.imageView.alpha = 0.4;
|
||||
if (progress >= 1) {
|
||||
[self hideProgressView];
|
||||
}
|
||||
} else {
|
||||
// 快速连续点几次,会EXC_BAD_ACCESS...
|
||||
// *stop = YES;
|
||||
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
|
||||
[self cancelBigImageRequest];
|
||||
}
|
||||
}];
|
||||
if (_model.type == TZAssetCellTypeVideo) {
|
||||
[[TZImageManager manager] getVideoWithAsset:_model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) {
|
||||
BOOL iCloudSyncFailed = !playerItem && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.model.iCloudFailed = iCloudSyncFailed;
|
||||
if (iCloudSyncFailed && self.didSelectPhotoBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.didSelectPhotoBlock(YES);
|
||||
self.selectImageView.image = self.photoDefImage;
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelBigImageRequest {
|
||||
if (_bigImageRequestID) {
|
||||
[[PHImageManager defaultManager] cancelImageRequest:_bigImageRequestID];
|
||||
}
|
||||
[self hideProgressView];
|
||||
}
|
||||
|
||||
#pragma mark - Notification
|
||||
|
||||
- (void)reload:(NSNotification *)noti {
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)noti.object;
|
||||
|
||||
UIViewController *parentViewController = nil;
|
||||
UIResponder *responder = self.nextResponder;
|
||||
do {
|
||||
if ([responder isKindOfClass:[UIViewController class]]) {
|
||||
parentViewController = (UIViewController *)responder;
|
||||
break;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
} while (responder);
|
||||
|
||||
if (parentViewController.navigationController != tzImagePickerVc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.model.isSelected && tzImagePickerVc.showSelectedIndex) {
|
||||
self.index = [tzImagePickerVc.selectedAssetIds indexOfObject:self.model.asset.localIdentifier] + 1;
|
||||
}
|
||||
self.indexLabel.hidden = !self.selectPhotoButton.isSelected;
|
||||
BOOL notSelectable = [TZCommonTools isAssetNotSelectable:self.model tzImagePickerVc:tzImagePickerVc];
|
||||
if (notSelectable && tzImagePickerVc.showPhotoCannotSelectLayer && !self.model.isSelected) {
|
||||
self.cannotSelectLayerButton.backgroundColor = tzImagePickerVc.cannotSelectLayerColor;
|
||||
self.cannotSelectLayerButton.hidden = NO;
|
||||
} else {
|
||||
self.cannotSelectLayerButton.hidden = YES;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy load
|
||||
|
||||
- (UIButton *)selectPhotoButton {
|
||||
if (_selectPhotoButton == nil) {
|
||||
UIButton *selectPhotoButton = [[UIButton alloc] init];
|
||||
[selectPhotoButton addTarget:self action:@selector(selectPhotoButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.contentView addSubview:selectPhotoButton];
|
||||
_selectPhotoButton = selectPhotoButton;
|
||||
}
|
||||
return _selectPhotoButton;
|
||||
}
|
||||
|
||||
- (UIImageView *)imageView {
|
||||
if (_imageView == nil) {
|
||||
UIImageView *imageView = [[UIImageView alloc] init];
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
imageView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:imageView];
|
||||
_imageView = imageView;
|
||||
|
||||
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapImageView)];
|
||||
[_imageView addGestureRecognizer:_tapGesture];
|
||||
self.allowPreview = self.allowPreview;
|
||||
}
|
||||
return _imageView;
|
||||
}
|
||||
|
||||
- (UIImageView *)selectImageView {
|
||||
if (_selectImageView == nil) {
|
||||
UIImageView *selectImageView = [[UIImageView alloc] init];
|
||||
selectImageView.contentMode = UIViewContentModeCenter;
|
||||
selectImageView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:selectImageView];
|
||||
_selectImageView = selectImageView;
|
||||
}
|
||||
return _selectImageView;
|
||||
}
|
||||
|
||||
- (UIView *)bottomView {
|
||||
if (_bottomView == nil) {
|
||||
UIView *bottomView = [[UIView alloc] init];
|
||||
static NSInteger rgb = 0;
|
||||
bottomView.userInteractionEnabled = NO;
|
||||
bottomView.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.8];
|
||||
[self.contentView addSubview:bottomView];
|
||||
_bottomView = bottomView;
|
||||
}
|
||||
return _bottomView;
|
||||
}
|
||||
|
||||
- (UIButton *)cannotSelectLayerButton {
|
||||
if (_cannotSelectLayerButton == nil) {
|
||||
UIButton *cannotSelectLayerButton = [[UIButton alloc] init];
|
||||
[self.contentView addSubview:cannotSelectLayerButton];
|
||||
_cannotSelectLayerButton = cannotSelectLayerButton;
|
||||
}
|
||||
return _cannotSelectLayerButton;
|
||||
}
|
||||
|
||||
- (UIImageView *)videoImgView {
|
||||
if (_videoImgView == nil) {
|
||||
UIImageView *videoImgView = [[UIImageView alloc] init];
|
||||
[videoImgView setImage:[UIImage tz_imageNamedFromMyBundle:@"VideoSendIcon"]];
|
||||
[self.bottomView addSubview:videoImgView];
|
||||
_videoImgView = videoImgView;
|
||||
}
|
||||
return _videoImgView;
|
||||
}
|
||||
|
||||
- (UILabel *)timeLength {
|
||||
if (_timeLength == nil) {
|
||||
UILabel *timeLength = [[UILabel alloc] init];
|
||||
timeLength.font = [UIFont boldSystemFontOfSize:11];
|
||||
timeLength.textColor = [UIColor whiteColor];
|
||||
timeLength.textAlignment = NSTextAlignmentRight;
|
||||
[self.bottomView addSubview:timeLength];
|
||||
_timeLength = timeLength;
|
||||
}
|
||||
return _timeLength;
|
||||
}
|
||||
|
||||
- (UILabel *)indexLabel {
|
||||
if (_indexLabel == nil) {
|
||||
UILabel *indexLabel = [[UILabel alloc] init];
|
||||
indexLabel.font = [UIFont systemFontOfSize:14];
|
||||
indexLabel.adjustsFontSizeToFitWidth = YES;
|
||||
indexLabel.textColor = [UIColor whiteColor];
|
||||
indexLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[self.contentView addSubview:indexLabel];
|
||||
_indexLabel = indexLabel;
|
||||
}
|
||||
return _indexLabel;
|
||||
}
|
||||
|
||||
- (TZProgressView *)progressView {
|
||||
if (_progressView == nil) {
|
||||
_progressView = [[TZProgressView alloc] init];
|
||||
_progressView.hidden = YES;
|
||||
[self addSubview:_progressView];
|
||||
}
|
||||
return _progressView;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_cannotSelectLayerButton.frame = self.bounds;
|
||||
if (self.allowPreview) {
|
||||
_selectPhotoButton.frame = CGRectMake(self.tz_width - 44, 0, 44, 44);
|
||||
} else {
|
||||
_selectPhotoButton.frame = self.bounds;
|
||||
}
|
||||
_selectImageView.frame = CGRectMake(self.tz_width - 27, 3, 24, 24);
|
||||
if (_selectImageView.image.size.width <= 27) {
|
||||
_selectImageView.contentMode = UIViewContentModeCenter;
|
||||
} else {
|
||||
_selectImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
_indexLabel.frame = _selectImageView.frame;
|
||||
_imageView.frame = self.bounds;
|
||||
|
||||
static CGFloat progressWH = 20;
|
||||
CGFloat progressXY = (self.tz_width - progressWH) / 2;
|
||||
_progressView.frame = CGRectMake(progressXY, progressXY, progressWH, progressWH);
|
||||
|
||||
_bottomView.frame = CGRectMake(0, self.tz_height - 17, self.tz_width, 17);
|
||||
_videoImgView.frame = CGRectMake(8, 0, 17, 17);
|
||||
_timeLength.frame = CGRectMake(self.videoImgView.tz_right, 0, self.tz_width - self.videoImgView.tz_right - 5, 17);
|
||||
|
||||
self.type = (NSInteger)self.model.type;
|
||||
self.showSelectBtn = self.showSelectBtn;
|
||||
|
||||
[self.contentView bringSubviewToFront:_bottomView];
|
||||
[self.contentView bringSubviewToFront:_cannotSelectLayerButton];
|
||||
[self.contentView bringSubviewToFront:_selectPhotoButton];
|
||||
[self.contentView bringSubviewToFront:_selectImageView];
|
||||
[self.contentView bringSubviewToFront:_indexLabel];
|
||||
|
||||
if (self.assetCellDidLayoutSubviewsBlock) {
|
||||
self.assetCellDidLayoutSubviewsBlock(self, _imageView, _selectImageView, _indexLabel, _bottomView, _timeLength, _videoImgView);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TZAlbumCell ()
|
||||
@property (weak, nonatomic) UIImageView *posterImageView;
|
||||
@property (weak, nonatomic) UILabel *titleLabel;
|
||||
@end
|
||||
|
||||
@implementation TZAlbumCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAlbumModel *)model {
|
||||
_model = model;
|
||||
|
||||
UIColor *nameColor = UIColor.blackColor;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
nameColor = UIColor.labelColor;
|
||||
}
|
||||
NSMutableAttributedString *nameString = [[NSMutableAttributedString alloc] initWithString:model.name attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:nameColor}];
|
||||
NSAttributedString *countString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" (%zd)",model.count] attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor lightGrayColor]}];
|
||||
[nameString appendAttributedString:countString];
|
||||
self.titleLabel.attributedText = nameString;
|
||||
[[TZImageManager manager] getPostImageWithAlbumModel:model completion:^(UIImage *postImage) {
|
||||
self.posterImageView.image = postImage;
|
||||
[self setNeedsLayout];
|
||||
}];
|
||||
if (model.selectedCount) {
|
||||
self.selectedCountButton.hidden = NO;
|
||||
[self.selectedCountButton setTitle:[NSString stringWithFormat:@"%zd",model.selectedCount] forState:UIControlStateNormal];
|
||||
} else {
|
||||
self.selectedCountButton.hidden = YES;
|
||||
}
|
||||
|
||||
if (self.albumCellDidSetModelBlock) {
|
||||
self.albumCellDidSetModelBlock(self, _posterImageView, _titleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_selectedCountButton.frame = CGRectMake(self.contentView.tz_width - 24, 23, 24, 24);
|
||||
NSInteger titleHeight = ceil(self.titleLabel.font.lineHeight);
|
||||
self.titleLabel.frame = CGRectMake(80, (self.tz_height - titleHeight) / 2, self.tz_width - 80 - 50, titleHeight);
|
||||
self.posterImageView.frame = CGRectMake(0, 0, 70, 70);
|
||||
|
||||
if (self.albumCellDidLayoutSubviewsBlock) {
|
||||
self.albumCellDidLayoutSubviewsBlock(self, _posterImageView, _titleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSublayersOfLayer:(CALayer *)layer {
|
||||
[super layoutSublayersOfLayer:layer];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy load
|
||||
|
||||
- (UIImageView *)posterImageView {
|
||||
if (_posterImageView == nil) {
|
||||
UIImageView *posterImageView = [[UIImageView alloc] init];
|
||||
posterImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
posterImageView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:posterImageView];
|
||||
_posterImageView = posterImageView;
|
||||
}
|
||||
return _posterImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (_titleLabel == nil) {
|
||||
UILabel *titleLabel = [[UILabel alloc] init];
|
||||
titleLabel.font = [UIFont boldSystemFontOfSize:17];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
titleLabel.textColor = UIColor.labelColor;
|
||||
} else {
|
||||
titleLabel.textColor = [UIColor blackColor];
|
||||
}
|
||||
titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
[self.contentView addSubview:titleLabel];
|
||||
_titleLabel = titleLabel;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)selectedCountButton {
|
||||
if (_selectedCountButton == nil) {
|
||||
UIButton *selectedCountButton = [[UIButton alloc] init];
|
||||
selectedCountButton.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
selectedCountButton.layer.cornerRadius = 12;
|
||||
selectedCountButton.clipsToBounds = YES;
|
||||
selectedCountButton.backgroundColor = [UIColor redColor];
|
||||
[selectedCountButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
selectedCountButton.titleLabel.font = [UIFont systemFontOfSize:15];
|
||||
[self.contentView addSubview:selectedCountButton];
|
||||
_selectedCountButton = selectedCountButton;
|
||||
}
|
||||
return _selectedCountButton;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation TZAssetCameraCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
_imageView = [[UIImageView alloc] init];
|
||||
_imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
[self.contentView addSubview:_imageView];
|
||||
self.clipsToBounds = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_imageView.frame = self.bounds;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TZAssetAddMoreCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_tipLabel = [[UILabel alloc] init];
|
||||
_tipLabel.numberOfLines = 2;
|
||||
_tipLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_tipLabel.font = [UIFont systemFontOfSize:12];
|
||||
_tipLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
CGFloat rgb = 156 / 255.0;
|
||||
_tipLabel.textColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0];
|
||||
[self.contentView addSubview:_tipLabel];
|
||||
|
||||
self.clipsToBounds = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_tipLabel.frame = CGRectMake(5, self.tz_height / 2, self.tz_width - 10, self.tz_height / 2 - 5);
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// TZAssetModel.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
TZAssetModelMediaTypePhoto = 0,
|
||||
TZAssetModelMediaTypeLivePhoto,
|
||||
TZAssetModelMediaTypePhotoGif,
|
||||
TZAssetModelMediaTypeVideo,
|
||||
TZAssetModelMediaTypeAudio
|
||||
} TZAssetModelMediaType;
|
||||
|
||||
@class PHAsset;
|
||||
@interface TZAssetModel : NSObject
|
||||
|
||||
@property (nonatomic, strong) PHAsset *asset;
|
||||
@property (nonatomic, assign) BOOL isSelected; ///< The select status of a photo, default is No
|
||||
@property (nonatomic, assign) TZAssetModelMediaType type;
|
||||
@property (nonatomic, copy) NSString *timeLength;
|
||||
@property (nonatomic, assign) BOOL iCloudFailed;
|
||||
|
||||
/// Init a photo dataModel With a PHAsset
|
||||
/// 用一个PHAsset实例,初始化一个照片模型
|
||||
+ (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type;
|
||||
+ (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type timeLength:(NSString *)timeLength;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@class PHFetchResult;
|
||||
@interface TZAlbumModel : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSString *name; ///< The album name
|
||||
@property (nonatomic, assign) NSInteger count; ///< Count of photos the album contain
|
||||
@property (nonatomic, strong) PHFetchResult *result;
|
||||
@property (nonatomic, strong) PHAssetCollection *collection;
|
||||
@property (nonatomic, strong) PHFetchOptions *options;
|
||||
|
||||
@property (nonatomic, strong) NSArray *models;
|
||||
@property (nonatomic, strong) NSArray *selectedModels;
|
||||
@property (nonatomic, assign) NSUInteger selectedCount;
|
||||
|
||||
@property (nonatomic, assign) BOOL isCameraRoll;
|
||||
|
||||
- (void)setResult:(PHFetchResult *)result needFetchAssets:(BOOL)needFetchAssets;
|
||||
- (void)refreshFetchResult;
|
||||
|
||||
@end
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// TZAssetModel.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZAssetModel.h"
|
||||
#import "TZImageManager.h"
|
||||
|
||||
@implementation TZAssetModel
|
||||
|
||||
+ (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type{
|
||||
TZAssetModel *model = [[TZAssetModel alloc] init];
|
||||
model.asset = asset;
|
||||
model.isSelected = NO;
|
||||
model.type = type;
|
||||
return model;
|
||||
}
|
||||
|
||||
+ (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type timeLength:(NSString *)timeLength {
|
||||
TZAssetModel *model = [self modelWithAsset:asset type:type];
|
||||
model.timeLength = timeLength;
|
||||
return model;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation TZAlbumModel
|
||||
|
||||
- (void)setResult:(PHFetchResult *)result needFetchAssets:(BOOL)needFetchAssets {
|
||||
_result = result;
|
||||
if (needFetchAssets) {
|
||||
[[TZImageManager manager] getAssetsFromFetchResult:result completion:^(NSArray<TZAssetModel *> *models) {
|
||||
self->_models = models;
|
||||
if (self->_selectedModels) {
|
||||
[self checkSelectedModels];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshFetchResult {
|
||||
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:self.collection options:self.options];
|
||||
self.count = fetchResult.count;
|
||||
[self setResult:fetchResult];
|
||||
}
|
||||
|
||||
- (void)setSelectedModels:(NSArray *)selectedModels {
|
||||
_selectedModels = selectedModels;
|
||||
if (_models) {
|
||||
[self checkSelectedModels];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkSelectedModels {
|
||||
self.selectedCount = 0;
|
||||
NSMutableSet *selectedAssets = [NSMutableSet setWithCapacity:_selectedModels.count];
|
||||
for (TZAssetModel *model in _selectedModels) {
|
||||
[selectedAssets addObject:model.asset];
|
||||
}
|
||||
for (TZAssetModel *model in _models) {
|
||||
if ([selectedAssets containsObject:model.asset]) {
|
||||
self.selectedCount ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)name {
|
||||
if (_name) {
|
||||
return _name;
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// TZAuthLimitedFooterTipView.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by qiaoxy on 2021/8/24.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TZAuthLimitedFooterTipView : UIView
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// TZAuthLimitedFooterTipView.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by qiaoxy on 2021/8/24.
|
||||
//
|
||||
|
||||
#import "TZAuthLimitedFooterTipView.h"
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@interface TZAuthLimitedFooterTipView()
|
||||
@property (nonatomic,strong) UIImageView *tipImgView;
|
||||
@property (nonatomic,strong) UILabel *tipLable;
|
||||
@property (nonatomic,strong) UIImageView *detailImgView;
|
||||
@end
|
||||
|
||||
@implementation TZAuthLimitedFooterTipView
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self initSubViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self initSubViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initSubViews {
|
||||
[self addSubview:self.tipImgView];
|
||||
[self addSubview:self.tipLable];
|
||||
[self addSubview:self.detailImgView];
|
||||
CGFloat margin = 15;
|
||||
CGFloat tipImgViewWH = 20;
|
||||
CGFloat detailImgViewWH = 12;
|
||||
CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
|
||||
|
||||
self.tipImgView.frame = CGRectMake(margin, 0, tipImgViewWH, tipImgViewWH);
|
||||
self.detailImgView.frame = CGRectMake(screenW - margin - detailImgViewWH, 0, detailImgViewWH, detailImgViewWH);
|
||||
|
||||
CGFloat tipLabelX = CGRectGetMaxX(self.tipImgView.frame) + 10;
|
||||
CGFloat tipLabelW = screenW - tipLabelX - detailImgViewWH - margin - 4;
|
||||
self.tipLable.frame = CGRectMake(tipLabelX, 0, tipLabelW, self.bounds.size.height);
|
||||
|
||||
self.tipImgView.center = CGPointMake(self.tipImgView.center.x, self.tipLable.center.y);
|
||||
self.detailImgView.center = CGPointMake(self.detailImgView.center.x, self.tipLable.center.y);
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (UIImageView *)tipImgView {
|
||||
if (!_tipImgView) {
|
||||
_tipImgView = [[UIImageView alloc] init];
|
||||
_tipImgView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_tipImgView.image = [UIImage tz_imageNamedFromMyBundle:@"tip"];
|
||||
}
|
||||
return _tipImgView;
|
||||
}
|
||||
|
||||
- (UILabel *)tipLable {
|
||||
if (!_tipLable) {
|
||||
_tipLable = [[UILabel alloc] init];
|
||||
NSString *appName = [TZCommonTools tz_getAppName];
|
||||
_tipLable.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Allow %@ to access your all photos"], appName];
|
||||
_tipLable.numberOfLines = 0;
|
||||
_tipLable.font = [UIFont systemFontOfSize:14];
|
||||
_tipLable.textColor = [UIColor colorWithRed:0.40 green:0.40 blue:0.40 alpha:1.0];
|
||||
}
|
||||
return _tipLable;
|
||||
}
|
||||
|
||||
- (UIImageView *)detailImgView {
|
||||
if (!_detailImgView) {
|
||||
_detailImgView = [[UIImageView alloc] init];
|
||||
_detailImgView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_detailImgView.image = [UIImage tz_imageNamedFromMyBundle:@"right_arrow"];
|
||||
}
|
||||
return _detailImgView;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// TZGifPhotoPreviewController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by ttouch on 2016/12/13.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class TZAssetModel;
|
||||
@interface TZGifPhotoPreviewController : UIViewController
|
||||
|
||||
@property (nonatomic, strong) TZAssetModel *model;
|
||||
|
||||
@end
|
|
@ -0,0 +1,173 @@
|
|||
//
|
||||
// TZGifPhotoPreviewController.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by ttouch on 2016/12/13.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZGifPhotoPreviewController.h"
|
||||
#import "TZImagePickerController.h"
|
||||
#import "TZAssetModel.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
#import "TZPhotoPreviewCell.h"
|
||||
#import "TZImageManager.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
@interface TZGifPhotoPreviewController () {
|
||||
UIView *_toolBar;
|
||||
UIButton *_doneButton;
|
||||
UIProgressView *_progress;
|
||||
|
||||
TZPhotoPreviewView *_previewView;
|
||||
|
||||
UIStatusBarStyle _originStatusBarStyle;
|
||||
}
|
||||
@property (assign, nonatomic) BOOL needShowStatusBar;
|
||||
@end
|
||||
|
||||
@implementation TZGifPhotoPreviewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.needShowStatusBar = ![UIApplication sharedApplication].statusBarHidden;
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePickerVc) {
|
||||
self.navigationItem.title = [NSString stringWithFormat:@"GIF %@",tzImagePickerVc.previewBtnTitleStr];
|
||||
}
|
||||
[self configPreviewView];
|
||||
[self configBottomToolBar];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
_originStatusBarStyle = [UIApplication sharedApplication].statusBarStyle;
|
||||
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
if (self.needShowStatusBar) {
|
||||
[UIApplication sharedApplication].statusBarHidden = NO;
|
||||
}
|
||||
[UIApplication sharedApplication].statusBarStyle = _originStatusBarStyle;
|
||||
}
|
||||
|
||||
- (void)configPreviewView {
|
||||
_previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero];
|
||||
_previewView.model = self.model;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_previewView setSingleTapGestureBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf signleTapAction];
|
||||
}];
|
||||
[self.view addSubview:_previewView];
|
||||
}
|
||||
|
||||
- (void)configBottomToolBar {
|
||||
_toolBar = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
CGFloat rgb = 34 / 255.0;
|
||||
_toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7];
|
||||
|
||||
_doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_doneButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePickerVc) {
|
||||
[_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal];
|
||||
} else {
|
||||
[_doneButton setTitle:[NSBundle tz_localizedStringForKey:@"Done"] forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:[UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0] forState:UIControlStateNormal];
|
||||
}
|
||||
[_toolBar addSubview:_doneButton];
|
||||
|
||||
UILabel *byteLabel = [[UILabel alloc] init];
|
||||
byteLabel.textColor = [UIColor whiteColor];
|
||||
byteLabel.font = [UIFont systemFontOfSize:13];
|
||||
byteLabel.frame = CGRectMake(10, 0, 100, 44);
|
||||
[[TZImageManager manager] getPhotosBytesWithArray:@[_model] completion:^(NSString *totalBytes) {
|
||||
byteLabel.text = totalBytes;
|
||||
}];
|
||||
[_toolBar addSubview:byteLabel];
|
||||
|
||||
[self.view addSubview:_toolBar];
|
||||
|
||||
if (tzImagePickerVc.gifPreviewPageUIConfigBlock) {
|
||||
tzImagePickerVc.gifPreviewPageUIConfigBlock(_toolBar, _doneButton);
|
||||
}
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle {
|
||||
TZImagePickerController *tzImagePicker = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePicker && [tzImagePicker isKindOfClass:[TZImagePickerController class]]) {
|
||||
return tzImagePicker.statusBarStyle;
|
||||
}
|
||||
return [super preferredStatusBarStyle];
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
_previewView.frame = self.view.bounds;
|
||||
_previewView.scrollView.frame = self.view.bounds;
|
||||
CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom;
|
||||
_toolBar.frame = CGRectMake(0, self.view.tz_height - toolBarHeight, self.view.tz_width, toolBarHeight);
|
||||
[_doneButton sizeToFit];
|
||||
_doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44);
|
||||
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePickerVc.gifPreviewPageDidLayoutSubviewsBlock) {
|
||||
tzImagePickerVc.gifPreviewPageDidLayoutSubviewsBlock(_toolBar, _doneButton);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)signleTapAction {
|
||||
_toolBar.hidden = !_toolBar.isHidden;
|
||||
[self.navigationController setNavigationBarHidden:_toolBar.isHidden];
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (_toolBar.isHidden) {
|
||||
[UIApplication sharedApplication].statusBarHidden = YES;
|
||||
} else if (tzImagePickerVc.needShowStatusBar) {
|
||||
[UIApplication sharedApplication].statusBarHidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doneButtonClick {
|
||||
if (self.navigationController) {
|
||||
TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (imagePickerVc.autoDismiss) {
|
||||
[self.navigationController dismissViewControllerAnimated:YES completion:^{
|
||||
[self callDelegateMethod];
|
||||
}];
|
||||
} else {
|
||||
[self callDelegateMethod];
|
||||
}
|
||||
} else {
|
||||
[self dismissViewControllerAnimated:YES completion:^{
|
||||
[self callDelegateMethod];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)callDelegateMethod {
|
||||
TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
UIImage *animatedImage = _previewView.imageView.image;
|
||||
if ([imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingGifImage:sourceAssets:)]) {
|
||||
[imagePickerVc.pickerDelegate imagePickerController:imagePickerVc didFinishPickingGifImage:animatedImage sourceAssets:_model.asset];
|
||||
}
|
||||
if (imagePickerVc.didFinishPickingGifImageHandle) {
|
||||
imagePickerVc.didFinishPickingGifImageHandle(animatedImage,_model.asset);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// TZImageCropManager.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 2016/12/5.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
// 图片裁剪管理类
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TZImageCropManager : NSObject
|
||||
|
||||
/// 裁剪框背景的处理
|
||||
+ (void)overlayClippingWithView:(UIView *)view cropRect:(CGRect)cropRect containerView:(UIView *)containerView needCircleCrop:(BOOL)needCircleCrop;
|
||||
|
||||
/*
|
||||
1.7.2 为了解决多位同学对于图片裁剪的需求,我这两天有空便在研究图片裁剪
|
||||
幸好有tuyou的PhotoTweaks库做参考,裁剪的功能实现起来简单许多
|
||||
该方法和其内部引用的方法基本来自于tuyou的PhotoTweaks库,我做了稍许删减和修改
|
||||
感谢tuyou同学在github开源了优秀的裁剪库PhotoTweaks,表示感谢
|
||||
PhotoTweaks库的github链接:https://github.com/itouch2/PhotoTweaks
|
||||
*/
|
||||
/// 获得裁剪后的图片
|
||||
+ (UIImage *)cropImageView:(UIImageView *)imageView toRect:(CGRect)rect zoomScale:(double)zoomScale containerView:(UIView *)containerView;
|
||||
|
||||
/// 获取圆形图片
|
||||
+ (UIImage *)circularClipImage:(UIImage *)image;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/// 该分类的代码来自SDWebImage:https://github.com/rs/SDWebImage
|
||||
/// 为了防止冲突,我将分类名字和方法名字做了修改
|
||||
@interface UIImage (TZGif)
|
||||
|
||||
+ (UIImage *)sd_tz_animatedGIFWithData:(NSData *)data;
|
||||
|
||||
@end
|
|
@ -0,0 +1,199 @@
|
|||
//
|
||||
// TZImageCropManager.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 2016/12/5.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZImageCropManager.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import "TZImageManager.h"
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@implementation TZImageCropManager
|
||||
|
||||
/// 裁剪框背景的处理
|
||||
+ (void)overlayClippingWithView:(UIView *)view cropRect:(CGRect)cropRect containerView:(UIView *)containerView needCircleCrop:(BOOL)needCircleCrop {
|
||||
UIBezierPath *path= [UIBezierPath bezierPathWithRect:[UIScreen mainScreen].bounds];
|
||||
CAShapeLayer *layer = [CAShapeLayer layer];
|
||||
if (needCircleCrop) { // 圆形裁剪框
|
||||
[path appendPath:[UIBezierPath bezierPathWithRoundedRect:cropRect cornerRadius:cropRect.size.width / 2]];
|
||||
} else { // 矩形裁剪框
|
||||
[path appendPath:[UIBezierPath bezierPathWithRect:cropRect]];
|
||||
}
|
||||
layer.path = path.CGPath;
|
||||
layer.fillRule = kCAFillRuleEvenOdd;
|
||||
layer.fillColor = [[UIColor blackColor] CGColor];
|
||||
layer.opacity = 0.5;
|
||||
[view.layer addSublayer:layer];
|
||||
}
|
||||
|
||||
/// 获得裁剪后的图片
|
||||
+ (UIImage *)cropImageView:(UIImageView *)imageView toRect:(CGRect)rect zoomScale:(double)zoomScale containerView:(UIView *)containerView {
|
||||
CGAffineTransform transform = CGAffineTransformIdentity;
|
||||
// 平移的处理
|
||||
CGRect imageViewRect = [imageView convertRect:imageView.bounds toView:containerView];
|
||||
CGPoint point = CGPointMake(imageViewRect.origin.x + imageViewRect.size.width / 2, imageViewRect.origin.y + imageViewRect.size.height / 2);
|
||||
CGFloat xMargin = containerView.tz_width - CGRectGetMaxX(rect) - rect.origin.x;
|
||||
CGPoint zeroPoint = CGPointMake((CGRectGetWidth(containerView.frame) - xMargin) / 2, containerView.center.y);
|
||||
CGPoint translation = CGPointMake(point.x - zeroPoint.x, point.y - zeroPoint.y);
|
||||
transform = CGAffineTransformTranslate(transform, translation.x, translation.y);
|
||||
// 缩放的处理
|
||||
transform = CGAffineTransformScale(transform, zoomScale, zoomScale);
|
||||
|
||||
CGImageRef imageRef = [self newTransformedImage:transform
|
||||
sourceImage:imageView.image.CGImage
|
||||
sourceSize:imageView.image.size
|
||||
outputWidth:rect.size.width * [UIScreen mainScreen].scale
|
||||
cropSize:rect.size
|
||||
imageViewSize:imageView.frame.size];
|
||||
UIImage *cropedImage = [UIImage imageWithCGImage:imageRef];
|
||||
cropedImage = [[TZImageManager manager] fixOrientation:cropedImage];
|
||||
CGImageRelease(imageRef);
|
||||
return cropedImage;
|
||||
}
|
||||
|
||||
+ (CGImageRef)newTransformedImage:(CGAffineTransform)transform sourceImage:(CGImageRef)sourceImage sourceSize:(CGSize)sourceSize outputWidth:(CGFloat)outputWidth cropSize:(CGSize)cropSize imageViewSize:(CGSize)imageViewSize {
|
||||
CGImageRef source = [self newScaledImage:sourceImage toSize:sourceSize];
|
||||
|
||||
CGFloat aspect = cropSize.height/cropSize.width;
|
||||
CGSize outputSize = CGSizeMake(outputWidth, outputWidth*aspect);
|
||||
|
||||
CGContextRef context = CGBitmapContextCreate(NULL, outputSize.width, outputSize.height, CGImageGetBitsPerComponent(source), 0, CGImageGetColorSpace(source), CGImageGetBitmapInfo(source));
|
||||
CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]);
|
||||
CGContextFillRect(context, CGRectMake(0, 0, outputSize.width, outputSize.height));
|
||||
|
||||
CGAffineTransform uiCoords = CGAffineTransformMakeScale(outputSize.width / cropSize.width, outputSize.height / cropSize.height);
|
||||
uiCoords = CGAffineTransformTranslate(uiCoords, cropSize.width/2.0, cropSize.height / 2.0);
|
||||
uiCoords = CGAffineTransformScale(uiCoords, 1.0, -1.0);
|
||||
CGContextConcatCTM(context, uiCoords);
|
||||
|
||||
CGContextConcatCTM(context, transform);
|
||||
CGContextScaleCTM(context, 1.0, -1.0);
|
||||
|
||||
CGContextDrawImage(context, CGRectMake(-imageViewSize.width/2, -imageViewSize.height/2.0, imageViewSize.width, imageViewSize.height), source);
|
||||
CGImageRef resultRef = CGBitmapContextCreateImage(context);
|
||||
CGContextRelease(context);
|
||||
CGImageRelease(source);
|
||||
return resultRef;
|
||||
}
|
||||
|
||||
+ (CGImageRef)newScaledImage:(CGImageRef)source toSize:(CGSize)size {
|
||||
CGSize srcSize = size;
|
||||
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, rgbColorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
||||
CGColorSpaceRelease(rgbColorSpace);
|
||||
|
||||
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
|
||||
CGContextTranslateCTM(context, size.width/2, size.height/2);
|
||||
|
||||
CGContextDrawImage(context, CGRectMake(-srcSize.width/2, -srcSize.height/2, srcSize.width, srcSize.height), source);
|
||||
|
||||
CGImageRef resultRef = CGBitmapContextCreateImage(context);
|
||||
CGContextRelease(context);
|
||||
return resultRef;
|
||||
}
|
||||
|
||||
/// 获取圆形图片
|
||||
+ (UIImage *)circularClipImage:(UIImage *)image {
|
||||
UIGraphicsBeginImageContextWithOptions(image.size, NO, [UIScreen mainScreen].scale);
|
||||
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
|
||||
CGContextAddEllipseInRect(ctx, rect);
|
||||
CGContextClip(ctx);
|
||||
|
||||
[image drawInRect:rect];
|
||||
UIImage *circleImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
|
||||
UIGraphicsEndImageContext();
|
||||
return circleImage;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation UIImage (TZGif)
|
||||
|
||||
+ (UIImage *)sd_tz_animatedGIFWithData:(NSData *)data {
|
||||
if (!data) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
||||
|
||||
size_t count = CGImageSourceGetCount(source);
|
||||
|
||||
UIImage *animatedImage;
|
||||
|
||||
if (count <= 1) {
|
||||
animatedImage = [[UIImage alloc] initWithData:data];
|
||||
}
|
||||
else {
|
||||
// images数组过大时内存会飙升,在这里限制下最大count
|
||||
NSInteger maxCount = [TZImagePickerConfig sharedInstance].gifPreviewMaxImagesCount ?: 50;
|
||||
NSInteger interval = MAX((count + maxCount / 2) / maxCount, 1);
|
||||
|
||||
NSMutableArray *images = [NSMutableArray array];
|
||||
|
||||
NSTimeInterval duration = 0.0f;
|
||||
|
||||
for (size_t i = 0; i < count; i+=interval) {
|
||||
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
|
||||
if (!image) {
|
||||
continue;
|
||||
}
|
||||
|
||||
duration += [self sd_frameDurationAtIndex:i source:source] * MIN(interval, 3);
|
||||
|
||||
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
|
||||
|
||||
CGImageRelease(image);
|
||||
}
|
||||
|
||||
if (!duration) {
|
||||
duration = (1.0f / 10.0f) * count;
|
||||
}
|
||||
|
||||
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
|
||||
}
|
||||
|
||||
CFRelease(source);
|
||||
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
|
||||
float frameDuration = 0.1f;
|
||||
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
|
||||
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
|
||||
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
|
||||
|
||||
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
|
||||
if (delayTimeUnclampedProp) {
|
||||
frameDuration = [delayTimeUnclampedProp floatValue];
|
||||
}
|
||||
else {
|
||||
|
||||
NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
|
||||
if (delayTimeProp) {
|
||||
frameDuration = [delayTimeProp floatValue];
|
||||
}
|
||||
}
|
||||
|
||||
// Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
|
||||
// We follow 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.
|
||||
|
||||
if (frameDuration < 0.011f) {
|
||||
frameDuration = 0.100f;
|
||||
}
|
||||
|
||||
CFRelease(cfFrameProperties);
|
||||
return frameDuration;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// TZImageManager.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 16/1/4.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
// 图片资源获取管理类
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import "TZAssetModel.h"
|
||||
|
||||
@class TZAlbumModel,TZAssetModel;
|
||||
@protocol TZImagePickerControllerDelegate;
|
||||
@interface TZImageManager : NSObject
|
||||
|
||||
@property (nonatomic, strong) PHCachingImageManager *cachingImageManager;
|
||||
|
||||
+ (instancetype)manager NS_SWIFT_NAME(default());
|
||||
+ (void)deallocManager;
|
||||
|
||||
@property (weak, nonatomic) id<TZImagePickerControllerDelegate> pickerDelegate;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldFixOrientation;
|
||||
|
||||
@property (nonatomic, assign) BOOL isPreviewNetworkImage;
|
||||
|
||||
/// Default is 600px / 默认600像素宽
|
||||
@property (nonatomic, assign) CGFloat photoPreviewMaxWidth;
|
||||
/// The pixel width of output image, Default is 828px / 导出图片的宽度,默认828像素宽
|
||||
@property (nonatomic, assign) CGFloat photoWidth;
|
||||
|
||||
/// Default is 4, Use in photos collectionView in TZPhotoPickerController
|
||||
/// 默认4列, TZPhotoPickerController中的照片collectionView
|
||||
@property (nonatomic, assign) NSInteger columnNumber;
|
||||
|
||||
/// Sort photos ascending by modificationDate,Default is YES
|
||||
/// 对照片排序,按修改时间升序,默认是YES。如果设置为NO,最新的照片会显示在最前面,内部的拍照按钮会排在第一个
|
||||
@property (nonatomic, assign) BOOL sortAscendingByModificationDate;
|
||||
|
||||
/// Minimum selectable photo width, Default is 0
|
||||
/// 最小可选中的图片宽度,默认是0,小于这个宽度的图片不可选中
|
||||
@property (nonatomic, assign) NSInteger minPhotoWidthSelectable;
|
||||
@property (nonatomic, assign) NSInteger minPhotoHeightSelectable;
|
||||
@property (nonatomic, assign) BOOL hideWhenCanNotSelect;
|
||||
|
||||
/// Return YES if Authorized 返回YES如果得到了授权
|
||||
- (BOOL)authorizationStatusAuthorized;
|
||||
- (void)requestAuthorizationWithCompletion:(void (^)(void))completion;
|
||||
- (BOOL)isPHAuthorizationStatusLimited;
|
||||
|
||||
/// Get Album 获得相册/相册数组
|
||||
- (void)getCameraRollAlbumWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion;
|
||||
- (void)getCameraRollAlbum:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion __attribute__((deprecated("Use -getCameraRollAlbumWithFetchAssets:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig")));
|
||||
- (void)getAllAlbums:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray<TZAlbumModel *> *models))completion __attribute__((deprecated("Use -getAllAlbumsWithFetchAssets:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig")));
|
||||
- (void)getAllAlbumsWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray<TZAlbumModel *> *models))completion;
|
||||
|
||||
/// Get Assets 获得Asset数组
|
||||
- (void)getAssetsFromFetchResult:(PHFetchResult *)result completion:(void (^)(NSArray<TZAssetModel *> *models))completion;
|
||||
- (void)getAssetsFromFetchResult:(PHFetchResult *)result allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(NSArray<TZAssetModel *> *models))completion __attribute__((deprecated("Use -getAssetsFromFetchResult:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig")));
|
||||
- (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(TZAssetModel *model))completion __attribute__((deprecated("Use -getAssetFromFetchResult:atIndex:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig")));
|
||||
- (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index completion:(void (^)(TZAssetModel *model))completion;
|
||||
/**
|
||||
* 从FetchResult中获取更多资源
|
||||
* @param result PHFetchResult对象
|
||||
* @param currentCount 当前已加载的数量
|
||||
* @param completion 完成回调,返回新加载的资源数组
|
||||
*/
|
||||
- (void)getMoreAssetsFromFetchResult:(PHFetchResult *)result
|
||||
currentCount:(NSInteger)currentCount
|
||||
completion:(void (^)(NSArray<TZAssetModel *> *models))completion;
|
||||
/// Get photo 获得照片
|
||||
- (PHImageRequestID)getPostImageWithAlbumModel:(TZAlbumModel *)model completion:(void (^)(UIImage *postImage))completion;
|
||||
|
||||
- (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion;
|
||||
- (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion;
|
||||
- (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed;
|
||||
- (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed;
|
||||
- (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset completion:(void (^)(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler;
|
||||
|
||||
/// Get full Image 获取原图
|
||||
/// 如下两个方法completion一般会调多次,一般会先返回缩略图,再返回原图(详见方法内部使用的系统API的说明),如果info[PHImageResultIsDegradedKey] 为 YES,则表明当前返回的是缩略图,否则是原图。
|
||||
- (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info))completion;
|
||||
- (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion;
|
||||
- (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion;
|
||||
// 该方法中,completion只会走一次
|
||||
- (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion;
|
||||
- (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion;
|
||||
|
||||
/// Get Image For VideoURL
|
||||
- (UIImage *)getImageWithVideoURL:(NSURL *)videoURL;
|
||||
|
||||
/// Save photo 保存照片
|
||||
- (void)savePhotoWithImage:(UIImage *)image completion:(void (^)(PHAsset *asset, NSError *error))completion;
|
||||
- (void)savePhotoWithImage:(UIImage *)image location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion;
|
||||
- (void)savePhotoWithImage:(UIImage *)image meta:(NSDictionary *)meta location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion;
|
||||
|
||||
/// Save video 保存视频
|
||||
- (void)saveVideoWithUrl:(NSURL *)url completion:(void (^)(PHAsset *asset, NSError *error))completion;
|
||||
- (void)saveVideoWithUrl:(NSURL *)url location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion;
|
||||
|
||||
/// Get video 获得视频
|
||||
- (void)getVideoWithAsset:(PHAsset *)asset completion:(void (^)(AVPlayerItem * playerItem, NSDictionary * info))completion;
|
||||
- (void)getVideoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(AVPlayerItem *, NSDictionary *))completion;
|
||||
|
||||
/// Export video 导出视频 presetName: 预设名字,默认值是AVAssetExportPreset640x480
|
||||
- (void)getVideoOutputPathWithAsset:(PHAsset *)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
|
||||
- (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
|
||||
- (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName timeRange:(CMTimeRange)timeRange success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
|
||||
/// 新的导出视频API,解决iOS14 iCloud视频导出失败的问题,未大量测试,请大家多多测试,有问题群里反馈
|
||||
- (void)requestVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
|
||||
/// 得到视频原始文件地址
|
||||
- (void)requestVideoURLWithAsset:(PHAsset *)asset success:(void (^)(NSURL *videoURL))success failure:(void (^)(NSDictionary* info))failure;
|
||||
|
||||
/// Get photo bytes 获得一组照片的大小
|
||||
- (void)getPhotosBytesWithArray:(NSArray *)photos completion:(void (^)(NSString *totalBytes))completion;
|
||||
|
||||
- (BOOL)isCameraRollAlbum:(PHAssetCollection *)metadata;
|
||||
|
||||
/// 检查照片大小是否满足最小要求
|
||||
- (BOOL)isPhotoSelectableWithAsset:(PHAsset *)asset;
|
||||
|
||||
/// 检查照片能否被选中
|
||||
- (BOOL)isAssetCannotBeSelected:(PHAsset *)asset;
|
||||
|
||||
/// 修正图片转向
|
||||
- (UIImage *)fixOrientation:(UIImage *)aImage;
|
||||
|
||||
/// 获取asset的资源类型
|
||||
- (TZAssetModelMediaType)getAssetType:(PHAsset *)asset;
|
||||
/// 缩放图片至新尺寸
|
||||
- (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size;
|
||||
|
||||
/// 判断asset是否是视频
|
||||
- (BOOL)isVideo:(PHAsset *)asset;
|
||||
|
||||
/// for TZImagePreviewController
|
||||
- (NSString *)getNewTimeFromDurationSecond:(NSInteger)duration;
|
||||
|
||||
- (TZAssetModel *)createModelWithAsset:(PHAsset *)asset;
|
||||
|
||||
@end
|
||||
|
||||
//@interface TZSortDescriptor : NSSortDescriptor
|
||||
//
|
||||
//@end
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 223 B |
After Width: | Height: | Size: 643 B |
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "阿拉伯语";
|
||||
"OK" = "حسنا";
|
||||
"Back" = "الى الخلف";
|
||||
"Done" = "فعله";
|
||||
"Edit" = "تعديل";
|
||||
"Sorry" = "آسف";
|
||||
"Cancel" = "إلغاء";
|
||||
"Setting" = "ضبط";
|
||||
"Photos" = "الصور";
|
||||
"Videos" = "أشرطة فيديو";
|
||||
"Preview" = "معاينة";
|
||||
"Full image" = "الصورة كاملة";
|
||||
"Processing..." = "معالجة...";
|
||||
"No Photos or Videos" = "لا توجد صور أو مقاطع فيديو";
|
||||
"Synchronizing photos from iCloud" = "مزامنة الصور من iCloud";
|
||||
"iCloud sync failed" = "iCloud فشلت المزامنة";
|
||||
"Can not use camera" = "لا يمكن استخدام الكاميرا";
|
||||
"Can not choose both video and photo" = "لا يمكن اختيار كل من الفيديو والصور";
|
||||
"Can not choose both photo and GIF" = "لا يمكن اختيار كل من الصور و GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "حدد مقطع الفيديو عندما يكون في حالة متعددة، وسنعمل على معالجة مقطع الفيديو كصورة";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "إذا تعذّر الانتقال إلى صفحة \"إعدادات الخصوصية\"، فيرجى الانتقال إلى صفحة \"الإعدادات\" بنفسك، شكرًا لك";
|
||||
"Select a maximum of %zd photos" = "حدد فقط ما يصل إلى %zd صورة";
|
||||
"Select a minimum of %zd photos" = "الرجاء تحديد %zd صورة على الأقل";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "السماح لـ %@ بالوصول إلى الألبوم في \"الإعدادات > الخصوصية > الصور\"";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "الرجاء السماح لـ %@ بالوصول إلى الكاميرا في \"الإعدادات > الخصوصية > الكاميرا\"";
|
||||
"Selected for %ld seconds" = "محدد لمدة %ld ثانية";
|
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "德语";
|
||||
"OK" = "OK";
|
||||
"Back" = "Zurück";
|
||||
"Done" = "Erledigt";
|
||||
"Edit" = "Bearbeiten";
|
||||
"Sorry" = "Es tut uns leid";
|
||||
"Cancel" = "Stornieren";
|
||||
"Setting" = "Rahmen";
|
||||
"Photos" = "Fotos";
|
||||
"Videos" = "Videos";
|
||||
"Preview" = "Vorschau";
|
||||
"Full image" = "Vollbild";
|
||||
"Processing..." = "Wird bearbeitet...";
|
||||
"No Photos or Videos" = "Keine Fotos oder Videos";
|
||||
"Synchronizing photos from iCloud" = "Fotos aus iCloud synchronisieren";
|
||||
"iCloud sync failed" = "iCloud Synchronisierung fehlgeschlagen";
|
||||
"Can not use camera" = "Kann die Kamera nicht benutzen";
|
||||
"Can not choose both video and photo" = "Video und Foto können nicht ausgewählt werden";
|
||||
"Can not choose both photo and GIF" = "Foto und GIF können nicht ausgewählt werden";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "Wenn Sie das Video im Multi-Status auswählen, wird es als Foto behandelt";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Sie können nicht zur Seite mit den Datenschutz-Einstellungen springen; bitte navigieren Sie selbst zur Einstellungsseite. Vielen Dank.";
|
||||
"Select a maximum of %zd photos" = "Wählen Sie maximal %zd Bilder aus";
|
||||
"Select a minimum of %zd photos" = "Bitte wählen Sie mindestens %zd Fotos aus";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Erlauben Sie %@ den Zugriff auf Ihr Album unter: „Einstellungen > Datenschutz > Fotos“";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Erlauben Sie %@ den Zugriff auf Ihre Kamera unter: „Einstellungen > Datenschutz > Kamera“";
|
||||
"Selected for %ld seconds" = "Ausgewählt für %ld Sekunden";
|
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "西班牙语";
|
||||
"OK" = "DE ACUERDO";
|
||||
"Back" = "Espalda";
|
||||
"Done" = "Confirma";
|
||||
"Edit" = "επεξεργασία";
|
||||
"Sorry" = "Lo siento";
|
||||
"Cancel" = "Cancelar";
|
||||
"Setting" = "Ajuste";
|
||||
"Photos" = "Las fotos";
|
||||
"Videos" = "Videos";
|
||||
"Preview" = "Vista previa";
|
||||
"Full image" = "Imagen completa";
|
||||
"Processing..." = "Tratamiento...";
|
||||
"No Photos or Videos" = "No hay fotos o videos";
|
||||
"Synchronizing photos from iCloud" = "Sincronizando fotos desde iCloud";
|
||||
"iCloud sync failed" = "la sincronización falló";
|
||||
"Can not use camera" = "No puedo usar la camara";
|
||||
"Can not choose both video and photo" = "No se puede elegir tanto el video como la foto.";
|
||||
"Can not choose both photo and GIF" = "No se puede elegir tanto foto como GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "Seleccione el vídeo en estado múltiple, trataremos el vídeo como una fotografía";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "No se puede saltar a la página de ajustes de privacidad, vaya a la página de ajustes manualmente, muchas gracias";
|
||||
"Select a maximum of %zd photos" = "Seleccione solamente hasta %zd imágenes";
|
||||
"Select a minimum of %zd photos" = "Seleccione al menos %zd fotografías";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Permita que %@ acceda a su galería en \"Ajustes > Privacidad > Fotografías\"";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Permita que %@ acceda a su cámara en \"Ajustes > Privacidad > Cámara\"";
|
||||
"Selected for %ld seconds" = "Seleccionado para %ld segundos";
|
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "法语";
|
||||
"OK" = "D'accord";
|
||||
"Back" = "Retour";
|
||||
"Done" = "Terminé";
|
||||
"Edit" = "Éditer";
|
||||
"Sorry" = "Pardon";
|
||||
"Cancel" = "Annuler";
|
||||
"Setting" = "Réglage";
|
||||
"Photos" = "Photos";
|
||||
"Videos" = "Vidéos";
|
||||
"Preview" = "Aperçu";
|
||||
"Full image" = "Image complète";
|
||||
"Processing..." = "En traitement...";
|
||||
"No Photos or Videos" = "Aucune photo ou vidéo";
|
||||
"Synchronizing photos from iCloud" = "Synchroniser des photos depuis iCloud";
|
||||
"iCloud sync failed" = "iCloud échec de la synchronisation";
|
||||
"Can not use camera" = "Impossible d'utiliser la caméra";
|
||||
"Can not choose both video and photo" = "Impossible de choisir à la fois la vidéo et la photo";
|
||||
"Can not choose both photo and GIF" = "Impossible de choisir à la fois photo et GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "Sélectionnez la vidéo lorsqu’elle est en état multiple, nous la traiterons comme une photo";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Impossible d'ouvrir la page des paramètres de confidentialité, veuillez accéder vous-même à la page des paramètres, merci";
|
||||
"Select a maximum of %zd photos" = "Vous pouvez uniquement sélectionner un maximum de %zd images";
|
||||
"Select a minimum of %zd photos" = "Veuillez sélectionner un minimum de %zd photos";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Autorisez %@ à accéder à votre album dans « Paramètres > Confidentialité > Photos »";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Autorisez %@ à accéder à votre appareil photo dans « Paramètres > Confidentialité > Appareil photo »";
|
||||
"Selected for %ld seconds" = "Sélectionné pendant %ld secondes";
|
After Width: | Height: | Size: 4.3 KiB |
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "日语";
|
||||
"OK" = "OK";
|
||||
"Back" = "バック";
|
||||
"Done" = "完了";
|
||||
"Edit" = "編集する";
|
||||
"Sorry" = "ごめんなさい";
|
||||
"Cancel" = "キャンセル";
|
||||
"Setting" = "設定";
|
||||
"Photos" = "写真";
|
||||
"Videos" = "動画";
|
||||
"Preview" = "プレビュー";
|
||||
"Full image" = "フルイメージ";
|
||||
"Processing..." = "処理...";
|
||||
"No Photos or Videos" = "写真やビデオはありません";
|
||||
"Synchronizing photos from iCloud" = "iCloudから写真を同期する";
|
||||
"iCloud sync failed" = "iCloud同期に失敗しました";
|
||||
"Can not use camera" = "カメラが使えない";
|
||||
"Can not choose both video and photo" = "ビデオと写真の両方を選択することはできません";
|
||||
"Can not choose both photo and GIF" = "写真とGIFの両方を選択することはできません";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "多肢選択の状態で、ビデオを選択すると、ビデオをデフォルトに画像として送信します。";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "プライバシー設定画面にジャンプできません。手動で設定画面を表示してください。";
|
||||
"Select a maximum of %zd photos" = "写真は多くとも%zd 枚選択できます。";
|
||||
"Select a minimum of %zd photos" = "少なくとも %zd 枚の写真を選択してください。";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "iPhoneの「設定-プライバシー-写真」のオプションで、r%@の携帯電話のアルバムへのアクセス権限を許可してください。";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "iPhoneの「設定-プライバシー-カメラ」で、%@のカメラへのアクセス権限を許可してください。";
|
||||
"Selected for %ld seconds" = "%ld 秒間選択されました";
|
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "朝鲜语";
|
||||
"OK" = "그래";
|
||||
"Back" = "뒤로";
|
||||
"Done" = "완료";
|
||||
"Edit" = "편집하다";
|
||||
"Sorry" = "미안해요";
|
||||
"Cancel" = "취소";
|
||||
"Setting" = "설정";
|
||||
"Photos" = "사진";
|
||||
"Videos" = "동영상";
|
||||
"Preview" = "미리 보기";
|
||||
"Full image" = "전체 이미지";
|
||||
"Processing..." = "처리...";
|
||||
"No Photos or Videos" = "아무 사진이 나 동영상";
|
||||
"Synchronizing photos from iCloud" = "ICloud에서 사진을 동기화";
|
||||
"iCloud sync failed" = "iCloud동기화 실패";
|
||||
"Can not use camera" = "카메라를 사용할 수 없습니다.";
|
||||
"Can not choose both video and photo" = "비디오와 사진 둘 다를 선택할 수 없습니다.";
|
||||
"Can not choose both photo and GIF" = "사진 및 GIF를 선택할 수 없습니다.";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "다중 선택 모드에서 비디오를 선택하면 비디오를 사진으로 처리합니다.";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "개인 정보 보호 설정 페이지로 바로 이동할 수 없습니다. 설정 페이지로 직접 이동해 주세요. 감사합니다.";
|
||||
"Select a maximum of %zd photos" = "최대 %zd장의 이미지만 선택할 수 있습니다.";
|
||||
"Select a minimum of %zd photos" = "최소 %zd장의 사진을 선택해 주세요.";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "\"설정 > 개인 정보 보호 > 사진\"에서 %@이(가) 앨범에 접근할 수 있도록 허용하세요.";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "\"설정 > 개인 정보 보호 > 카메라\"에서 %@이(가) 카메라에 접근할 수 있도록 허용하세요.";
|
||||
"Selected for %ld seconds" = "%ld 초 동안 선택됨";
|
After Width: | Height: | Size: 116 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 501 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 620 B |
After Width: | Height: | Size: 1006 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 501 B |
After Width: | Height: | Size: 392 B |
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "葡萄牙语";
|
||||
"OK" = "Está bem";
|
||||
"Back" = "De volta";
|
||||
"Done" = "Feito";
|
||||
"Edit" = "editar";
|
||||
"Sorry" = "Desculpa";
|
||||
"Cancel" = "Cancelar";
|
||||
"Setting" = "Configuração";
|
||||
"Photos" = "Fotos";
|
||||
"Videos" = "Vídeos";
|
||||
"Preview" = "Visualizar";
|
||||
"Full image" = "Imagem Completa";
|
||||
"Processing..." = "Em processamento...";
|
||||
"No Photos or Videos" = "Sem fotos ou vídeos";
|
||||
"Synchronizing photos from iCloud" = "Sincronizando fotos do iCloud";
|
||||
"iCloud sync failed" = "iCloud falha na sincronização";
|
||||
"Can not use camera" = "Não pode usar a câmera";
|
||||
"Can not choose both video and photo" = "Não é possível escolher vídeo e foto";
|
||||
"Can not choose both photo and GIF" = "Não é possível escolher foto e GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "Se estiver em estado múltiplo, selecione a opção vídeo; iremos utilizar o vídeo como uma foto";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Não é possível avançar para a página de definições de privacidade, aceda à página de definições você mesmo, obrigado";
|
||||
"Select a maximum of %zd photos" = "Selecione apenas %zd imagens,no máximo";
|
||||
"Select a minimum of %zd photos" = "Selecione %zd fotos,no mínimo";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Permita a %@ aceder ao seu álbum em “Definições > Privacidade > Fotos”";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Permita a %@ aceder à sua câmara em “Definições > Privacidade > Câmara”";
|
||||
"Selected for %ld seconds" = "Selecionado por %ld segundos";
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "俄语";
|
||||
"OK" = "Хорошо";
|
||||
"Back" = "назад";
|
||||
"Done" = "Готово";
|
||||
"Edit" = "редактировать";
|
||||
"Sorry" = "сожалею";
|
||||
"Cancel" = "отменить";
|
||||
"Setting" = "настройка";
|
||||
"Photos" = "Фото";
|
||||
"Videos" = "Видео";
|
||||
"Preview" = "предварительный просмотр";
|
||||
"Full image" = "Полное изображение";
|
||||
"Processing..." = "Обработка ...";
|
||||
"No Photos or Videos" = "Нет фото или видео";
|
||||
"Synchronizing photos from iCloud" = "Синхронизация фотографий из iCloud";
|
||||
"iCloud sync failed" = "iCloud сбой синхронизации";
|
||||
"Can not use camera" = "Не могу использовать камеру";
|
||||
"Can not choose both video and photo" = "Не могу выбрать как видео,так и фото";
|
||||
"Can not choose both photo and GIF" = "Не могу выбрать фото и GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "В случае выбора видео при нахождении в мультирежиме видео будет обработано как фотография";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Не удается перейти на страницу настроек конфиденциальности. Перейдите на эту страницу самостоятельно";
|
||||
"Select a maximum of %zd photos" = "Вы можете выбрать до %zd изображений";
|
||||
"Select a minimum of %zd photos" = "Вы можете выбрать не менее %zd изображений";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Разрешите доступ %@ к вашему альбому,перейдя в Настройки > Конфиденциальность > Фото";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Разрешите доступ %@ к камере вашего устройства,перейдя в Настройки > Конфиденциальность > Камера";
|
||||
"Selected for %ld seconds" = "Выбрано для %ld секунд";
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 671 B |
|
@ -0,0 +1,26 @@
|
|||
"KEY" = "越南语";
|
||||
"OK" = "Xác nhận";
|
||||
"Back" = "Quay lại";
|
||||
"Done" = "Hoàn thành";
|
||||
"Edit" = "biên tập";
|
||||
"Sorry" = "Xin lỗi";
|
||||
"Cancel" = "Hủy";
|
||||
"Setting" = "Cài đặt";
|
||||
"Photos" = "Hình";
|
||||
"Videos" = "Clip";
|
||||
"Preview" = "Xem trước";
|
||||
"Full image" = "Hình gốc";
|
||||
"Processing..." = "Đang xử lý...";
|
||||
"No Photos or Videos" = "Không có ảnh hoặc video";
|
||||
"Can not use camera" = "Máy chụp hình không khả dụng";
|
||||
"Synchronizing photos from iCloud" = "Đang đồng bộ hình ảnh từ ICloud";
|
||||
"iCloud sync failed" = "iCloud đồng bộ hóa không thành công";
|
||||
"Can not choose both video and photo" = "Trong lúc chọn hình ảnh không cùng lúc chọn video";
|
||||
"Can not choose both photo and GIF" = "Trong lúc chọn hình ảnh không cùng lúc chọn hình GIF";
|
||||
"Select the video when in multi state, we will handle the video as a photo" = "Chọn hình ảnh cùng video, video sẽ bị mặc nhận thành hình ảnh và gửi đi.";
|
||||
"Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Không thể chuyển tự động qua trang cài đặt riêng tư, bạn hãy thoát ra cà điều chỉnh lại, cám ơn bạn.";
|
||||
"Select a maximum of %zd photos" = "Bạn chỉ được chọn nhiều nhất %zd tấm hình";
|
||||
"Select a minimum of %zd photos" = "Chọn ít nhất %zd tấm hình";
|
||||
"Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Vui lòng tại mục iPhone \" Cài đặt – quyền riêng tư - Ảnh\" mở quyền cho phép %@ truy cập ảnh.";
|
||||
"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Vui lòng tại mục iPhone \" Cài đặt – quyền riêng tư - Ảnh\" mở quyền cho phép %@ truy cập máy ảnh";
|
||||
"Selected for %ld seconds" = "Đã chọn cho %ld giây";
|
|
@ -0,0 +1,396 @@
|
|||
//
|
||||
// TZImagePickerController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
// version 3.8.8 - 2024.10.27
|
||||
// 更多信息,请前往项目的github地址:https://github.com/banchichen/TZImagePickerController
|
||||
|
||||
/*
|
||||
经过测试,比起xib的方式,把TZAssetCell改用纯代码的方式来写,滑动帧数明显提高了(约提高10帧左右)
|
||||
|
||||
最初发现这个问题并修复的是@小鱼周凌宇同学,她的博客地址: http://zhoulingyu.com/
|
||||
表示感谢~
|
||||
|
||||
原来xib确实会导致性能问题啊...大家也要注意了...
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TZAssetModel.h"
|
||||
#import "NSBundle+TZImagePicker.h"
|
||||
#import "TZImageManager.h"
|
||||
#import "TZVideoPlayerController.h"
|
||||
#import "TZGifPhotoPreviewController.h"
|
||||
#import "TZPhotoPreviewController.h"
|
||||
#import "TZPhotoPreviewCell.h"
|
||||
|
||||
#if __has_include("TZLocationManager.h")
|
||||
#define TZ_HAVE_LOCATION_CODE 1
|
||||
#import "TZLocationManager.h"
|
||||
#else
|
||||
#undef TZ_HAVE_LOCATION_CODE
|
||||
#endif
|
||||
|
||||
#define CURRENT_SYSTEM_VERSION [[UIDevice currentDevice] systemVersion]
|
||||
#define SYSTEM_VERSION_GREATER_THAN_15 ([CURRENT_SYSTEM_VERSION floatValue] >= 15.0)
|
||||
|
||||
@class TZAlbumCell, TZAssetCell;
|
||||
@protocol TZImagePickerControllerDelegate;
|
||||
@interface TZImagePickerController : UINavigationController
|
||||
|
||||
#pragma mark -
|
||||
/// Use this init method / 用这个初始化方法
|
||||
- (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount delegate:(id<TZImagePickerControllerDelegate>)delegate;
|
||||
- (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id<TZImagePickerControllerDelegate>)delegate;
|
||||
- (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id<TZImagePickerControllerDelegate>)delegate pushPhotoPickerVc:(BOOL)pushPhotoPickerVc;
|
||||
/// This init method just for previewing photos / 用这个初始化方法以预览图片
|
||||
- (instancetype)initWithSelectedAssets:(NSMutableArray *)selectedAssets selectedPhotos:(NSMutableArray *)selectedPhotos index:(NSInteger)index;
|
||||
/// This init method for crop photo / 用这个初始化方法以裁剪图片
|
||||
- (instancetype)initCropTypeWithAsset:(PHAsset *)asset photo:(UIImage *)photo completion:(void (^)(UIImage *cropImage,PHAsset *asset))completion;
|
||||
|
||||
#pragma mark -
|
||||
/// Default is 9 / 默认最大可选9张图片
|
||||
@property (nonatomic, assign) NSInteger maxImagesCount;
|
||||
|
||||
/// The minimum count photos user must pick, Default is 0
|
||||
/// 最小照片必选张数,默认是0
|
||||
@property (nonatomic, assign) NSInteger minImagesCount;
|
||||
|
||||
/// If the user does not select any pictures, the current picture is automatically selected when the Finish button is clicked, Default is YES
|
||||
/// 如果用户未选择任何图片,在点击完成按钮时自动选中当前图片,默认YES
|
||||
@property (nonatomic, assign) BOOL autoSelectCurrentWhenDone;
|
||||
|
||||
/// Always enale the done button, not require minimum 1 photo be picked
|
||||
/// 让完成按钮一直可以点击,无须最少选择一张图片
|
||||
@property (nonatomic, assign) BOOL alwaysEnableDoneBtn;
|
||||
|
||||
/// Sort photos ascending by modificationDate,Default is YES
|
||||
/// 对照片排序,按修改时间升序,默认是YES。如果设置为NO,最新的照片会显示在最前面,内部的拍照按钮会排在第一个
|
||||
@property (nonatomic, assign) BOOL sortAscendingByModificationDate;
|
||||
|
||||
/// The pixel width of output image, Default is 828px,you need to set photoPreviewMaxWidth at the same time
|
||||
/// 导出图片的宽度,默认828像素宽,你需要同时设置photoPreviewMaxWidth的值
|
||||
@property (nonatomic, assign) CGFloat photoWidth;
|
||||
|
||||
/// Default is 600px / 默认600像素宽
|
||||
@property (nonatomic, assign) CGFloat photoPreviewMaxWidth;
|
||||
|
||||
/// Default is 30, While fetching photo, HUD will dismiss automatic if timeout;
|
||||
/// 超时时间,默认为30秒,当取图片时间超过30秒还没有取成功时,会自动dismiss HUD;
|
||||
@property (nonatomic, assign) NSInteger timeout;
|
||||
|
||||
/// Default is YES, if set NO, the original photo button will hide. user can't picking original photo.
|
||||
/// 默认为YES,如果设置为NO,原图按钮将隐藏,用户不能选择发送原图
|
||||
@property (nonatomic, assign) BOOL allowPickingOriginalPhoto;
|
||||
|
||||
/// Default is YES, if set NO, user can't picking video.
|
||||
/// 默认为YES,如果设置为NO,用户将不能选择视频
|
||||
@property (nonatomic, assign) BOOL allowPickingVideo;
|
||||
|
||||
/// Default is NO, if set YES, user can edit video.
|
||||
/// 默认为NO,如果设置为YES, 用户能编辑视频
|
||||
@property (nonatomic, assign) BOOL allowEditVideo;
|
||||
|
||||
/// Export quality of cropped video, Default is AVAssetExportPresetMediumQuality
|
||||
/// 裁剪视频的导出质量,默认是 AVAssetExportPresetMediumQuality
|
||||
@property (nonatomic, copy) NSString *presetName;
|
||||
|
||||
/// Default is 30s. If it exceeds the video duration, it is the video duration.The minimum duration of video crop is 1s.
|
||||
/// 默认是30s,如果超过视频时长,则为视频时长,小于1s不裁剪
|
||||
@property (nonatomic, assign) NSInteger maxCropVideoDuration;
|
||||
|
||||
/// Default is NO, if set YES, The edited video will be automatically saved to the album.
|
||||
/// 默认为NO,如果设置为YES,编辑后的视频会自动保存到相册
|
||||
@property (nonatomic, assign) BOOL saveEditedVideoToAlbum;
|
||||
|
||||
/// Default is NO / 默认为NO,为YES时可以多选视频/gif/图片,和照片共享最大可选张数maxImagesCount的限制
|
||||
@property (nonatomic, assign) BOOL allowPickingMultipleVideo;
|
||||
|
||||
/// Default is NO, if set YES, user can picking gif image. When NO, gif will be treated as a regular image. If want not displayed, please refer to isAssetCanBeDisplayed
|
||||
/// 默认为NO,如果设置为YES,用户可以选择gif图片。为NO时gif会被当成普通图片,若要不显示,请参考isAssetCanBeDisplayed
|
||||
@property (nonatomic, assign) BOOL allowPickingGif;
|
||||
|
||||
/// Default is YES, if set NO, user can't picking image.
|
||||
/// 默认为YES,如果设置为NO,用户将不能选择发送图片
|
||||
@property (nonatomic, assign) BOOL allowPickingImage;
|
||||
|
||||
/// Default is YES, if set NO, user can't take picture.
|
||||
/// 默认为YES,如果设置为NO, 用户将不能拍摄照片
|
||||
@property (nonatomic, assign) BOOL allowTakePicture;
|
||||
|
||||
#ifdef TZ_HAVE_LOCATION_CODE
|
||||
@property (nonatomic, assign) BOOL allowCameraLocation;
|
||||
#endif
|
||||
|
||||
/// Default is YES, if set NO, user can't take video.
|
||||
/// 默认为YES,如果设置为NO, 用户将不能拍摄视频
|
||||
@property(nonatomic, assign) BOOL allowTakeVideo;
|
||||
/// Default value is 10 minutes / 视频最大拍摄时间,默认是10分钟,单位是秒
|
||||
@property (assign, nonatomic) NSTimeInterval videoMaximumDuration;
|
||||
/// Customizing UIImagePickerController's other properties, such as videoQuality / 定制UIImagePickerController的其它属性,比如视频拍摄质量videoQuality
|
||||
@property (nonatomic, copy) void(^uiImagePickerControllerSettingBlock)(UIImagePickerController *imagePickerController);
|
||||
|
||||
/// 首选语言,如果设置了就用该语言,不设则取当前系统语言。
|
||||
/// 支持zh-Hans、zh-Hant、en、vi等值,详见TZImagePickerController.bundle内的语言资源
|
||||
@property (copy, nonatomic) NSString *preferredLanguage;
|
||||
|
||||
/// 语言bundle,preferredLanguage变化时languageBundle会变化
|
||||
/// 可通过手动设置bundle,让选择器支持新的的语言(需要在设置preferredLanguage后设置languageBundle)。欢迎提交PR把语言文件提交上来~
|
||||
@property (strong, nonatomic) NSBundle *languageBundle;
|
||||
|
||||
/// Default is YES, if set NO, user can't preview photo.
|
||||
/// 默认为YES,如果设置为NO,预览按钮将隐藏,用户将不能去预览照片
|
||||
@property (nonatomic, assign) BOOL allowPreview;
|
||||
|
||||
/// Default is YES, if set NO, the picker don't dismiss itself.
|
||||
/// 默认为YES,如果设置为NO, 选择器将不会自己dismiss
|
||||
@property(nonatomic, assign) BOOL autoDismiss;
|
||||
|
||||
/// Default is NO, if set YES, in the delegate method the photos and infos will be nil, only assets hava value.
|
||||
/// 默认为NO,如果设置为YES,代理方法里photos和infos会是nil,只返回assets
|
||||
@property (assign, nonatomic) BOOL onlyReturnAsset;
|
||||
|
||||
/// Default is NO, if set YES, will show the image's selected index.
|
||||
/// 默认为NO,如果设置为YES,会显示照片的选中序号
|
||||
@property (assign, nonatomic) BOOL showSelectedIndex;
|
||||
|
||||
/// Default is NO, if set YES, when selected photos's count up to maxImagesCount, other photo will show float layer what's color is cannotSelectLayerColor.
|
||||
/// 默认是NO,如果设置为YES,当照片选择张数达到maxImagesCount时,其它照片会显示颜色为cannotSelectLayerColor的浮层
|
||||
@property (assign, nonatomic) BOOL showPhotoCannotSelectLayer;
|
||||
/// Default is white color with 0.8 alpha;
|
||||
@property (strong, nonatomic) UIColor *cannotSelectLayerColor;
|
||||
|
||||
/// Default is YES, if set NO, the result photo will be scaled to photoWidth pixel width. The photoWidth default is 828px
|
||||
/// 默认是YES,如果设置为NO,内部会缩放图片到photoWidth像素宽
|
||||
@property (assign, nonatomic) BOOL notScaleImage;
|
||||
|
||||
/// 默认是NO,如果设置为YES,导出视频时会修正转向(慎重设为YES,可能导致部分安卓下拍的视频导出失败)
|
||||
@property (assign, nonatomic) BOOL needFixComposition;
|
||||
|
||||
/// The photos user have selected
|
||||
/// 用户选中过的图片数组
|
||||
@property (nonatomic, strong) NSMutableArray *selectedAssets;
|
||||
@property (nonatomic, strong) NSMutableArray<TZAssetModel *> *selectedModels;
|
||||
@property (nonatomic, strong) NSMutableArray *selectedAssetIds;
|
||||
- (void)addSelectedModel:(TZAssetModel *)model;
|
||||
- (void)removeSelectedModel:(TZAssetModel *)model;
|
||||
|
||||
/// Minimum selectable photo width, Default is 0
|
||||
/// 最小可选中的图片宽度,默认是0,小于这个宽度的图片不可选中
|
||||
@property (nonatomic, assign) NSInteger minPhotoWidthSelectable;
|
||||
@property (nonatomic, assign) NSInteger minPhotoHeightSelectable;
|
||||
/// Hide the photo what can not be selected, Default is NO
|
||||
/// 隐藏不可以选中的图片,默认是NO,不推荐将其设置为YES
|
||||
@property (nonatomic, assign) BOOL hideWhenCanNotSelect;
|
||||
/// Deprecated, Use statusBarStyle (顶部statusBar 是否为系统默认的黑色,默认为NO)
|
||||
@property (nonatomic, assign) BOOL isStatusBarDefault __attribute__((deprecated("Use -statusBarStyle.")));
|
||||
/// statusBar的样式,默认为UIStatusBarStyleLightContent
|
||||
@property (assign, nonatomic) UIStatusBarStyle statusBarStyle;
|
||||
|
||||
#pragma mark -
|
||||
/// Single selection mode, valid when maxImagesCount = 1
|
||||
/// 单选模式,maxImagesCount为1时才生效
|
||||
@property (nonatomic, assign) BOOL showSelectBtn; ///< 在单选模式下,照片列表页中,显示选择按钮,默认为NO
|
||||
@property (nonatomic, assign) BOOL allowCrop; ///< 允许裁剪,默认为YES,showSelectBtn为NO才生效
|
||||
@property (nonatomic, assign) BOOL scaleAspectFillCrop; ///< 是否图片等比缩放填充cropRect区域,开启后预览页面无法左右滑动切换图片
|
||||
@property (nonatomic, assign) CGRect cropRect; ///< 裁剪框的尺寸
|
||||
@property (nonatomic, assign) CGRect cropRectPortrait; ///< 裁剪框的尺寸(竖屏)
|
||||
@property (nonatomic, assign) CGRect cropRectLandscape; ///< 裁剪框的尺寸(横屏)
|
||||
@property (nonatomic, assign) BOOL needCircleCrop; ///< 需要圆形裁剪框
|
||||
@property (nonatomic, assign) NSInteger circleCropRadius; ///< 圆形裁剪框半径大小
|
||||
@property (nonatomic, copy) void (^cropViewSettingBlock)(UIView *cropView); ///< 自定义裁剪框的其他属性
|
||||
@property (nonatomic, copy) void (^navLeftBarButtonSettingBlock)(UIButton *leftButton); ///< 自定义返回按钮样式及其属性
|
||||
|
||||
/// 【自定义各页面/组件的样式】在界面初始化/组件setModel完成后调用,允许外界修改样式等
|
||||
@property (nonatomic, copy) void (^photoPickerPageUIConfigBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine);
|
||||
@property (nonatomic, copy) void (^photoPreviewPageUIConfigBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel);
|
||||
@property (nonatomic, copy) void (^videoPreviewPageUIConfigBlock)(UIButton *playButton, UIView *toolBar, UIButton *editBtn, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^videoEditViewPageUIConfigBlock)(UIButton *playButton,UILabel *cropVideoDurationLabel, UIButton *editButton, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^gifPreviewPageUIConfigBlock)(UIView *toolBar, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^albumPickerPageUIConfigBlock)(UITableView *tableView);
|
||||
@property (nonatomic, copy) void (^assetCellDidSetModelBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView);
|
||||
@property (nonatomic, copy) void (^albumCellDidSetModelBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel);
|
||||
/// 【自定义各页面/组件的frame】在界面viewDidLayoutSubviews/组件layoutSubviews后调用,允许外界修改frame等
|
||||
@property (nonatomic, copy) void (^photoPickerPageDidLayoutSubviewsBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine);
|
||||
@property (nonatomic, copy) void (^photoPreviewPageDidLayoutSubviewsBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel);
|
||||
@property (nonatomic, copy) void (^videoPreviewPageDidLayoutSubviewsBlock)(UIButton *playButton, UIView *toolBar, UIButton *editButton, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^videoEditViewPageDidLayoutSubviewsBlock)(UIButton *playButton, UILabel *cropVideoDurationLabel, UIButton *cancelButton, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^gifPreviewPageDidLayoutSubviewsBlock)(UIView *toolBar, UIButton *doneButton);
|
||||
@property (nonatomic, copy) void (^albumPickerPageDidLayoutSubviewsBlock)(UITableView *tableView);
|
||||
@property (nonatomic, copy) void (^assetCellDidLayoutSubviewsBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView);
|
||||
@property (nonatomic, copy) void (^albumCellDidLayoutSubviewsBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel);
|
||||
/// 自定义各页面/组件的frame】刷新底部状态(refreshNaviBarAndBottomBarState)使用的
|
||||
@property (nonatomic, copy) void (^photoPickerPageDidRefreshStateBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine);
|
||||
|
||||
@property (nonatomic, copy) void (^photoPreviewPageDidRefreshStateBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel);
|
||||
|
||||
#pragma mark -
|
||||
- (UIAlertController *)showAlertWithTitle:(NSString *)title;
|
||||
- (void)showProgressHUD;
|
||||
- (void)hideProgressHUD;
|
||||
@property (nonatomic, assign) BOOL isSelectOriginalPhoto;
|
||||
@property (assign, nonatomic) BOOL needShowStatusBar;
|
||||
|
||||
#pragma mark -
|
||||
@property (nonatomic, copy) NSString *takePictureImageName __attribute__((deprecated("Use -takePictureImage.")));
|
||||
@property (nonatomic, copy) NSString *photoSelImageName __attribute__((deprecated("Use -photoSelImage.")));
|
||||
@property (nonatomic, copy) NSString *photoDefImageName __attribute__((deprecated("Use -photoDefImage.")));
|
||||
@property (nonatomic, copy) NSString *photoOriginSelImageName __attribute__((deprecated("Use -photoOriginSelImage.")));
|
||||
@property (nonatomic, copy) NSString *photoOriginDefImageName __attribute__((deprecated("Use -photoOriginDefImage.")));
|
||||
@property (nonatomic, copy) NSString *photoPreviewOriginDefImageName __attribute__((deprecated("Use -photoPreviewOriginDefImage.")));
|
||||
@property (nonatomic, copy) NSString *photoNumberIconImageName __attribute__((deprecated("Use -photoNumberIconImage.")));
|
||||
@property (nonatomic, strong) UIImage *takePictureImage;
|
||||
@property (nonatomic, strong) UIImage *addMorePhotoImage;
|
||||
@property (nonatomic, strong) UIImage *photoSelImage;
|
||||
@property (nonatomic, strong) UIImage *photoDefImage;
|
||||
@property (nonatomic, strong) UIImage *photoOriginSelImage;
|
||||
@property (nonatomic, strong) UIImage *photoOriginDefImage;
|
||||
@property (nonatomic, strong) UIImage *photoPreviewOriginDefImage;
|
||||
@property (nonatomic, strong) UIImage *photoNumberIconImage;
|
||||
|
||||
#pragma mark -
|
||||
/// Appearance / 外观颜色 + 按钮文字
|
||||
@property (nonatomic, strong) UIColor *oKButtonTitleColorNormal;
|
||||
@property (nonatomic, strong) UIColor *oKButtonTitleColorDisabled;
|
||||
@property (nonatomic, strong) UIColor *naviBgColor;
|
||||
@property (nonatomic, strong) UIColor *naviTitleColor;
|
||||
@property (nonatomic, strong) UIFont *naviTitleFont;
|
||||
@property (nonatomic, strong) UIColor *barItemTextColor;
|
||||
@property (nonatomic, strong) UIFont *barItemTextFont;
|
||||
|
||||
@property (nonatomic, copy) NSString *doneBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *cancelBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *previewBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *fullImageBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *settingBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *processHintStr;
|
||||
@property (nonatomic, copy) NSString *editBtnTitleStr;
|
||||
@property (nonatomic, copy) NSString *editViewCancelBtnTitleStr;
|
||||
|
||||
/// Icon theme color, default is green color like wechat, the value is r:31 g:185 b:34. Currently only support image selection icon when showSelectedIndex is YES. If you need it, please set it as soon as possible
|
||||
/// icon主题色,默认是微信的绿色,值是r:31 g:185 b:34。目前仅支持showSelectedIndex为YES时的图片选中icon。如需要,请尽早设置它。
|
||||
@property (strong, nonatomic) UIColor *iconThemeColor;
|
||||
|
||||
#pragma mark -
|
||||
- (void)cancelButtonClick;
|
||||
|
||||
// For method annotations, see the corresponding method in TZImagePickerControllerDelegate / 方法注释见TZImagePickerControllerDelegate中对应方法
|
||||
@property (nonatomic, copy) void (^didFinishPickingPhotosHandle)(NSArray<UIImage *> *photos,NSArray *assets,BOOL isSelectOriginalPhoto);
|
||||
@property (nonatomic, copy) void (^didFinishPickingPhotosWithInfosHandle)(NSArray<UIImage *> *photos,NSArray *assets,BOOL isSelectOriginalPhoto,NSArray<NSDictionary *> *infos);
|
||||
@property (nonatomic, copy) void (^imagePickerControllerDidCancelHandle)(void);
|
||||
@property (nonatomic, copy) void (^didFinishPickingVideoHandle)(UIImage *coverImage,PHAsset *asset);
|
||||
@property (nonatomic, copy) void (^didFinishPickingAndEditingVideoHandle)(UIImage *coverImage,NSString *outputPath,NSString *errorMsg);
|
||||
@property (nonatomic, copy) void (^didFinishPickingGifImageHandle)(UIImage *animatedImage,id sourceAssets);
|
||||
|
||||
@property (nonatomic, weak) id<TZImagePickerControllerDelegate> pickerDelegate;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TZImagePickerControllerDelegate <NSObject>
|
||||
@optional
|
||||
// The picker should dismiss itself; when it dismissed these callback will be called.
|
||||
// You can also set autoDismiss to NO, then the picker don't dismiss itself.
|
||||
// If isOriginalPhoto is YES, user picked the original photo.
|
||||
// You can get original photo with asset, by the method [[TZImageManager manager] getOriginalPhotoWithAsset:completion:].
|
||||
// The UIImage Object in photos default width is 828px, you can set it by photoWidth property.
|
||||
// 这个照片选择器会自己dismiss,当选择器dismiss的时候,会执行下面的代理方法
|
||||
// 你也可以设置autoDismiss属性为NO,选择器就不会自己dismis了
|
||||
// 如果isSelectOriginalPhoto为YES,表明用户选择了原图
|
||||
// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:]
|
||||
// photos数组里的UIImage对象,默认是828像素宽,你可以通过设置photoWidth属性的值来改变它
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto;
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray<NSDictionary *> *)infos;
|
||||
- (void)tz_imagePickerControllerDidCancel:(TZImagePickerController *)picker;
|
||||
|
||||
/// 如果用户选择了某张照片下面的代理方法会被执行
|
||||
/// 如果isSelectOriginalPhoto为YES,表明用户选择了原图
|
||||
/// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:]
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didSelectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto;
|
||||
|
||||
/// 如果用户取消选择了某张照片下面的代理方法会被执行
|
||||
/// 如果isSelectOriginalPhoto为YES,表明用户选择了原图
|
||||
/// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:]
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didDeselectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto;
|
||||
|
||||
// If user picking a video and allowPickingMultipleVideo is NO, this callback will be called.
|
||||
// If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
// 如果用户选择了一个视频且allowPickingMultipleVideo是NO,下面的代理方法会被执行
|
||||
// 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingVideo:(UIImage *)coverImage sourceAssets:(PHAsset *)asset;
|
||||
|
||||
// If allowEditVideo is YES and allowPickingMultipleVideo is NO, When user picking a video, this callback will be called.
|
||||
// If allowPickingMultipleVideo is YES, video editing is not supported, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
// 当allowEditVideo是YES且allowPickingMultipleVideo是NO是,如果用户选择了一个视频,下面的代理方法会被执行
|
||||
// 如果allowPickingMultipleVideo是YES,则不支持编辑视频,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingAndEditingVideo:(UIImage *)coverImage outputPath:(NSString *)outputPath error:(NSString *)errorMsg;
|
||||
|
||||
// When saving the edited video to the album fails, this callback will be called.
|
||||
// 编辑后的视频自动保存到相册失败时,下面的代理方法会被执行
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFailToSaveEditedVideoWithError:(NSError *)error;
|
||||
|
||||
// If user picking a gif image and allowPickingMultipleVideo is NO, this callback will be called.
|
||||
// If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
// 如果用户选择了一个gif图片且allowPickingMultipleVideo是NO,下面的代理方法会被执行
|
||||
// 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingGifImage:(UIImage *)animatedImage sourceAssets:(PHAsset *)asset;
|
||||
|
||||
// Decide album show or not't
|
||||
// 决定相册显示与否 albumName:相册名字 result:相册原始数据
|
||||
- (BOOL)isAlbumCanSelect:(NSString *)albumName result:(PHFetchResult *)result;
|
||||
|
||||
// Decide asset show or not't
|
||||
// 决定照片显示与否
|
||||
- (BOOL)isAssetCanSelect:(PHAsset *)asset __attribute__((deprecated("Use -isAssetCanBeDisplayed:.")));
|
||||
- (BOOL)isAssetCanBeDisplayed:(PHAsset *)asset;
|
||||
|
||||
// Decide asset can be selected
|
||||
// 决定照片能否被选中
|
||||
- (BOOL)isAssetCanBeSelected:(PHAsset *)asset;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZAlbumPickerController : UIViewController
|
||||
@property (nonatomic, assign) NSInteger columnNumber;
|
||||
@property (assign, nonatomic) BOOL isFirstAppear;
|
||||
- (void)configTableView;
|
||||
@end
|
||||
|
||||
|
||||
@interface UIImage (MyBundle)
|
||||
+ (UIImage *)tz_imageNamedFromMyBundle:(NSString *)name;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZCommonTools : NSObject
|
||||
+ (UIEdgeInsets)tz_safeAreaInsets;
|
||||
+ (BOOL)tz_isIPhoneX;
|
||||
+ (BOOL)tz_isLandscape;
|
||||
+ (CGFloat)tz_statusBarHeight;
|
||||
// 获得Info.plist数据字典
|
||||
+ (NSDictionary *)tz_getInfoDictionary;
|
||||
+ (NSString *)tz_getAppName;
|
||||
+ (BOOL)tz_isRightToLeftLayout;
|
||||
+ (void)configBarButtonItem:(UIBarButtonItem *)item tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc;
|
||||
+ (BOOL)isICloudSyncError:(NSError *)error;
|
||||
+ (BOOL)isAssetNotSelectable:(TZAssetModel *)model tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZImagePickerConfig : NSObject
|
||||
+ (instancetype)sharedInstance;
|
||||
@property (copy, nonatomic) NSString *preferredLanguage;
|
||||
@property(nonatomic, assign) BOOL allowPickingImage;
|
||||
@property (nonatomic, assign) BOOL allowPickingVideo;
|
||||
@property (strong, nonatomic) NSBundle *languageBundle;
|
||||
@property (assign, nonatomic) BOOL showSelectedIndex;
|
||||
@property (assign, nonatomic) BOOL showPhotoCannotSelectLayer;
|
||||
@property (assign, nonatomic) BOOL notScaleImage;
|
||||
@property (assign, nonatomic) BOOL needFixComposition;
|
||||
|
||||
/// 默认是50,如果一个GIF过大,里面图片个数可能超过1000,会导致内存飙升而崩溃
|
||||
@property (assign, nonatomic) NSInteger gifPreviewMaxImagesCount;
|
||||
/// 【自定义GIF播放方案】为了避免内存过大,内部默认限制只播放50帧(平均取),可通过gifPreviewMaxImagesCount属性调整,若对GIF预览有更好的效果要求,可实现这个block采用FLAnimatedImage等三方库来播放,但注意FLAnimatedImage有播放速度较慢问题,自行取舍下。
|
||||
@property (nonatomic, copy) void (^gifImagePlayBlock)(TZPhotoPreviewView *view, UIImageView *imageView, NSData *gifData, NSDictionary *info);
|
||||
@end
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// TZImageRequestOperation.h
|
||||
// TZImagePickerControllerFramework
|
||||
//
|
||||
// Created by 谭真 on 2018/12/20.
|
||||
// Copyright © 2018 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TZImageRequestOperation : NSOperation
|
||||
|
||||
typedef void(^TZImageRequestCompletedBlock)(UIImage *photo, NSDictionary *info, BOOL isDegraded);
|
||||
typedef void(^TZImageRequestProgressBlock)(double progress, NSError *error, BOOL *stop, NSDictionary *info);
|
||||
|
||||
@property (nonatomic, copy, nullable) TZImageRequestCompletedBlock completedBlock;
|
||||
@property (nonatomic, copy, nullable) TZImageRequestProgressBlock progressBlock;
|
||||
@property (nonatomic, strong, nullable) PHAsset *asset;
|
||||
|
||||
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
|
||||
@property (assign, nonatomic, getter = isFinished) BOOL finished;
|
||||
|
||||
- (instancetype)initWithAsset:(PHAsset *)asset completion:(TZImageRequestCompletedBlock)completionBlock progressHandler:(TZImageRequestProgressBlock)progressHandler;
|
||||
- (void)done;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// TZImageRequestOperation.m
|
||||
// TZImagePickerControllerFramework
|
||||
//
|
||||
// Created by 谭真 on 2018/12/20.
|
||||
// Copyright © 2018 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZImageRequestOperation.h"
|
||||
#import "TZImageManager.h"
|
||||
|
||||
@implementation TZImageRequestOperation
|
||||
|
||||
@synthesize executing = _executing;
|
||||
@synthesize finished = _finished;
|
||||
|
||||
- (instancetype)initWithAsset:(PHAsset *)asset completion:(TZImageRequestCompletedBlock)completionBlock progressHandler:(TZImageRequestProgressBlock)progressHandler {
|
||||
self = [super init];
|
||||
self.asset = asset;
|
||||
self.completedBlock = completionBlock;
|
||||
self.progressBlock = progressHandler;
|
||||
_executing = NO;
|
||||
_finished = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
self.executing = YES;
|
||||
[[TZImageManager manager] getPhotoWithAsset:self.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!isDegraded) {
|
||||
if (self.completedBlock) {
|
||||
self.completedBlock(photo, info, isDegraded);
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self done];
|
||||
});
|
||||
}
|
||||
});
|
||||
} progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.progressBlock) {
|
||||
self.progressBlock(progress, error, stop, info);
|
||||
}
|
||||
});
|
||||
} networkAccessAllowed:YES];
|
||||
}
|
||||
|
||||
- (void)done {
|
||||
self.finished = YES;
|
||||
self.executing = NO;
|
||||
[self reset];
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
self.asset = nil;
|
||||
self.completedBlock = nil;
|
||||
self.progressBlock = nil;
|
||||
}
|
||||
|
||||
- (void)setFinished:(BOOL)finished {
|
||||
[self willChangeValueForKey:@"isFinished"];
|
||||
_finished = finished;
|
||||
[self didChangeValueForKey:@"isFinished"];
|
||||
}
|
||||
|
||||
- (void)setExecuting:(BOOL)executing {
|
||||
[self willChangeValueForKey:@"isExecuting"];
|
||||
_executing = executing;
|
||||
[self didChangeValueForKey:@"isExecuting"];
|
||||
}
|
||||
|
||||
- (BOOL)isAsynchronous {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// TZPhotoPickerController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class TZAlbumModel;
|
||||
@interface TZPhotoPickerController : UIViewController
|
||||
|
||||
@property (nonatomic, assign) BOOL isFirstAppear;
|
||||
@property (nonatomic, assign) NSInteger columnNumber;
|
||||
@property (nonatomic, strong) TZAlbumModel *model;
|
||||
/**
|
||||
* 加载更多资源
|
||||
* 当用户下拉时触发加载更多旧照片
|
||||
*/
|
||||
- (void)loadMoreAssets;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZCollectionView : UICollectionView
|
||||
|
||||
@end
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// TZPhotoPreviewCell.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class TZAssetModel;
|
||||
@interface TZAssetPreviewCell : UICollectionViewCell
|
||||
@property (nonatomic, strong) TZAssetModel *model;
|
||||
@property (nonatomic, copy) void (^singleTapGestureBlock)(void);
|
||||
- (void)configSubviews;
|
||||
- (void)photoPreviewCollectionViewDidScroll;
|
||||
@end
|
||||
|
||||
|
||||
@class TZAssetModel,TZProgressView,TZPhotoPreviewView;
|
||||
@interface TZPhotoPreviewCell : TZAssetPreviewCell
|
||||
|
||||
@property (nonatomic, copy) void (^imageProgressUpdateBlock)(double progress);
|
||||
|
||||
@property (nonatomic, strong) TZPhotoPreviewView *previewView;
|
||||
|
||||
@property (nonatomic, assign) BOOL allowCrop;
|
||||
@property (nonatomic, assign) CGRect cropRect;
|
||||
@property (nonatomic, assign) BOOL scaleAspectFillCrop;
|
||||
|
||||
- (void)recoverSubviews;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface TZPhotoPreviewView : UIView
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic, strong) UIView *imageContainerView;
|
||||
@property (nonatomic, strong) TZProgressView *progressView;
|
||||
@property (nonatomic, strong) UIImageView *iCloudErrorIcon;
|
||||
@property (nonatomic, strong) UILabel *iCloudErrorLabel;
|
||||
@property (nonatomic, copy) void (^iCloudSyncFailedHandle)(id asset, BOOL isSyncFailed);
|
||||
|
||||
|
||||
@property (nonatomic, assign) BOOL allowCrop;
|
||||
@property (nonatomic, assign) CGRect cropRect;
|
||||
@property (nonatomic, assign) BOOL scaleAspectFillCrop;
|
||||
@property (nonatomic, strong) TZAssetModel *model;
|
||||
@property (nonatomic, strong) id asset;
|
||||
@property (nonatomic, copy) void (^singleTapGestureBlock)(void);
|
||||
@property (nonatomic, copy) void (^imageProgressUpdateBlock)(double progress);
|
||||
|
||||
@property (nonatomic, assign) int32_t imageRequestID;
|
||||
|
||||
- (void)recoverSubviews;
|
||||
@end
|
||||
|
||||
|
||||
@class AVPlayer, AVPlayerLayer;
|
||||
@interface TZVideoPreviewCell : TZAssetPreviewCell
|
||||
@property (strong, nonatomic) AVPlayer *player;
|
||||
@property (strong, nonatomic) AVPlayerLayer *playerLayer;
|
||||
@property (strong, nonatomic) UIButton *playButton;
|
||||
@property (strong, nonatomic) UIImage *cover;
|
||||
@property (nonatomic, strong) NSURL *videoURL;
|
||||
@property (nonatomic, strong) UIImageView *iCloudErrorIcon;
|
||||
@property (nonatomic, strong) UILabel *iCloudErrorLabel;
|
||||
@property (nonatomic, copy) void (^iCloudSyncFailedHandle)(id asset, BOOL isSyncFailed);
|
||||
- (void)pausePlayerAndShowNaviBar;
|
||||
@end
|
||||
|
||||
|
||||
@interface TZGifPreviewCell : TZAssetPreviewCell
|
||||
@property (strong, nonatomic) TZPhotoPreviewView *previewView;
|
||||
@end
|
|
@ -0,0 +1,576 @@
|
|||
//
|
||||
// TZPhotoPreviewCell.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZPhotoPreviewCell.h"
|
||||
#import "TZAssetModel.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
#import "TZImageManager.h"
|
||||
#import "TZProgressView.h"
|
||||
#import "TZImageCropManager.h"
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@implementation TZAssetPreviewCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor blackColor];
|
||||
[self configSubviews];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(photoPreviewCollectionViewDidScroll) name:@"photoPreviewCollectionViewDidScroll" object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configSubviews {
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - Notification
|
||||
|
||||
- (void)photoPreviewCollectionViewDidScroll {
|
||||
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TZPhotoPreviewCell
|
||||
|
||||
- (void)configSubviews {
|
||||
self.previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.previewView setSingleTapGestureBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (strongSelf.singleTapGestureBlock) {
|
||||
strongSelf.singleTapGestureBlock();
|
||||
}
|
||||
}];
|
||||
[self.previewView setImageProgressUpdateBlock:^(double progress) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (strongSelf.imageProgressUpdateBlock) {
|
||||
strongSelf.imageProgressUpdateBlock(progress);
|
||||
}
|
||||
}];
|
||||
[self.contentView addSubview:self.previewView];
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAssetModel *)model {
|
||||
[super setModel:model];
|
||||
_previewView.model = model;
|
||||
}
|
||||
|
||||
- (void)recoverSubviews {
|
||||
[_previewView recoverSubviews];
|
||||
}
|
||||
|
||||
- (void)setAllowCrop:(BOOL)allowCrop {
|
||||
_allowCrop = allowCrop;
|
||||
_previewView.allowCrop = allowCrop;
|
||||
}
|
||||
|
||||
- (void)setScaleAspectFillCrop:(BOOL)scaleAspectFillCrop {
|
||||
_scaleAspectFillCrop = scaleAspectFillCrop;
|
||||
_previewView.scaleAspectFillCrop = scaleAspectFillCrop;
|
||||
}
|
||||
|
||||
- (void)setCropRect:(CGRect)cropRect {
|
||||
_cropRect = cropRect;
|
||||
_previewView.cropRect = cropRect;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.previewView.frame = self.bounds;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface TZPhotoPreviewView ()<UIScrollViewDelegate>
|
||||
@property (assign, nonatomic) BOOL isRequestingGIF;
|
||||
@end
|
||||
|
||||
@implementation TZPhotoPreviewView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_scrollView = [[UIScrollView alloc] init];
|
||||
_scrollView.bouncesZoom = YES;
|
||||
_scrollView.maximumZoomScale = 4;
|
||||
_scrollView.minimumZoomScale = 1.0;
|
||||
_scrollView.multipleTouchEnabled = YES;
|
||||
_scrollView.delegate = self;
|
||||
_scrollView.scrollsToTop = NO;
|
||||
_scrollView.showsHorizontalScrollIndicator = NO;
|
||||
_scrollView.showsVerticalScrollIndicator = YES;
|
||||
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_scrollView.delaysContentTouches = NO;
|
||||
_scrollView.canCancelContentTouches = YES;
|
||||
_scrollView.alwaysBounceVertical = NO;
|
||||
if (@available(iOS 11, *)) {
|
||||
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
}
|
||||
[self addSubview:_scrollView];
|
||||
|
||||
_imageContainerView = [[UIView alloc] init];
|
||||
_imageContainerView.clipsToBounds = YES;
|
||||
_imageContainerView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
[_scrollView addSubview:_imageContainerView];
|
||||
|
||||
_imageView = [[UIImageView alloc] init];
|
||||
_imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_imageView.clipsToBounds = YES;
|
||||
[_imageContainerView addSubview:_imageView];
|
||||
|
||||
_iCloudErrorIcon = [[UIImageView alloc] init];
|
||||
_iCloudErrorIcon.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"];
|
||||
_iCloudErrorIcon.hidden = YES;
|
||||
[self addSubview:_iCloudErrorIcon];
|
||||
_iCloudErrorLabel = [[UILabel alloc] init];
|
||||
_iCloudErrorLabel.font = [UIFont systemFontOfSize:10];
|
||||
_iCloudErrorLabel.textColor = [UIColor whiteColor];
|
||||
_iCloudErrorLabel.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"];
|
||||
_iCloudErrorLabel.hidden = YES;
|
||||
[self addSubview:_iCloudErrorLabel];
|
||||
|
||||
UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
|
||||
[self addGestureRecognizer:tap1];
|
||||
UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
|
||||
tap2.numberOfTapsRequired = 2;
|
||||
[tap1 requireGestureRecognizerToFail:tap2];
|
||||
[self addGestureRecognizer:tap2];
|
||||
|
||||
[self configProgressView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configProgressView {
|
||||
_progressView = [[TZProgressView alloc] init];
|
||||
_progressView.hidden = YES;
|
||||
[self addSubview:_progressView];
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAssetModel *)model {
|
||||
_model = model;
|
||||
self.isRequestingGIF = NO;
|
||||
[_scrollView setZoomScale:1.0 animated:NO];
|
||||
if (model.type == TZAssetModelMediaTypePhotoGif) {
|
||||
// 先显示缩略图
|
||||
[[TZImageManager manager] getPhotoWithAsset:model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
if (photo) {
|
||||
self.imageView.image = photo;
|
||||
}
|
||||
[self resizeSubviews];
|
||||
if (self.isRequestingGIF) {
|
||||
return;
|
||||
}
|
||||
// 再显示gif动图
|
||||
self.isRequestingGIF = YES;
|
||||
[[TZImageManager manager] getOriginalPhotoDataWithAsset:model.asset progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
progress = progress > 0.02 ? progress : 0.02;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
BOOL iCloudSyncFailed = [TZCommonTools isICloudSyncError:error];
|
||||
self.iCloudErrorLabel.hidden = !iCloudSyncFailed;
|
||||
self.iCloudErrorIcon.hidden = !iCloudSyncFailed;
|
||||
if (self.iCloudSyncFailedHandle) {
|
||||
self.iCloudSyncFailedHandle(model.asset, iCloudSyncFailed);
|
||||
}
|
||||
|
||||
self.progressView.progress = progress;
|
||||
if (progress >= 1) {
|
||||
self.progressView.hidden = YES;
|
||||
} else {
|
||||
self.progressView.hidden = NO;
|
||||
}
|
||||
});
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[TZImagePickerController] getOriginalPhotoDataWithAsset:%f error:%@", progress, error);
|
||||
#endif
|
||||
} completion:^(NSData *data, NSDictionary *info, BOOL isDegraded) {
|
||||
if (!isDegraded) {
|
||||
self.isRequestingGIF = NO;
|
||||
self.progressView.hidden = YES;
|
||||
if ([TZImagePickerConfig sharedInstance].gifImagePlayBlock) {
|
||||
[TZImagePickerConfig sharedInstance].gifImagePlayBlock(self, self.imageView, data, info);
|
||||
} else {
|
||||
self.imageView.image = [UIImage sd_tz_animatedGIFWithData:data];
|
||||
}
|
||||
[self resizeSubviews];
|
||||
}
|
||||
}];
|
||||
} progressHandler:nil networkAccessAllowed:NO];
|
||||
} else {
|
||||
self.asset = model.asset;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAsset:(PHAsset *)asset {
|
||||
if (_asset && self.imageRequestID) {
|
||||
[[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID];
|
||||
}
|
||||
|
||||
_asset = asset;
|
||||
self.imageRequestID = [[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.iCloudErrorLabel.hidden = !iCloudSyncFailed;
|
||||
self.iCloudErrorIcon.hidden = !iCloudSyncFailed;
|
||||
if (self.iCloudSyncFailedHandle) {
|
||||
self.iCloudSyncFailedHandle(asset, iCloudSyncFailed);
|
||||
}
|
||||
if (![asset isEqual:self->_asset]) return;
|
||||
if (photo) {
|
||||
self.imageView.image = photo;
|
||||
}
|
||||
[self resizeSubviews];
|
||||
if (self.imageView.tz_height && self.allowCrop) {
|
||||
CGFloat scale = MAX(self.cropRect.size.width / self.imageView.tz_width, self.cropRect.size.height / self.imageView.tz_height);
|
||||
if (self.scaleAspectFillCrop && scale > 1) { // 如果设置图片缩放裁剪并且图片需要缩放
|
||||
CGFloat multiple = self.scrollView.maximumZoomScale / self.scrollView.minimumZoomScale;
|
||||
self.scrollView.minimumZoomScale = scale;
|
||||
self.scrollView.maximumZoomScale = scale * MAX(multiple, 2);
|
||||
[self.scrollView setZoomScale:scale animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
self->_progressView.hidden = YES;
|
||||
if (self.imageProgressUpdateBlock) {
|
||||
self.imageProgressUpdateBlock(1);
|
||||
}
|
||||
if (!isDegraded) {
|
||||
self.imageRequestID = 0;
|
||||
}
|
||||
} progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
if (![asset isEqual:self->_asset]) return;
|
||||
self->_progressView.hidden = NO;
|
||||
[self bringSubviewToFront:self->_progressView];
|
||||
progress = progress > 0.02 ? progress : 0.02;
|
||||
self->_progressView.progress = progress;
|
||||
if (self.imageProgressUpdateBlock && progress < 1) {
|
||||
self.imageProgressUpdateBlock(progress);
|
||||
}
|
||||
|
||||
if (progress >= 1) {
|
||||
self->_progressView.hidden = YES;
|
||||
self.imageRequestID = 0;
|
||||
}
|
||||
} networkAccessAllowed:YES];
|
||||
|
||||
[self configMaximumZoomScale];
|
||||
}
|
||||
|
||||
- (void)recoverSubviews {
|
||||
[_scrollView setZoomScale:_scrollView.minimumZoomScale animated:NO];
|
||||
[self resizeSubviews];
|
||||
}
|
||||
|
||||
- (void)resizeSubviews {
|
||||
_imageContainerView.tz_origin = CGPointZero;
|
||||
_imageContainerView.tz_width = self.scrollView.tz_width;
|
||||
|
||||
UIImage *image = _imageView.image;
|
||||
if (image.size.height / image.size.width > self.tz_height / self.scrollView.tz_width) {
|
||||
CGFloat width = image.size.width / image.size.height * self.scrollView.tz_height;
|
||||
if (width < 1 || isnan(width)) width = self.tz_width;
|
||||
width = floor(width);
|
||||
|
||||
_imageContainerView.tz_width = width;
|
||||
_imageContainerView.tz_height = self.tz_height;
|
||||
_imageContainerView.tz_centerX = self.scrollView.tz_width / 2;
|
||||
} else {
|
||||
CGFloat height = image.size.height / image.size.width * self.scrollView.tz_width;
|
||||
if (height < 1 || isnan(height)) height = self.tz_height;
|
||||
height = floor(height);
|
||||
_imageContainerView.tz_height = height;
|
||||
_imageContainerView.tz_centerY = self.tz_height / 2;
|
||||
}
|
||||
if (_imageContainerView.tz_height > self.tz_height && _imageContainerView.tz_height - self.tz_height <= 1) {
|
||||
_imageContainerView.tz_height = self.tz_height;
|
||||
}
|
||||
CGFloat contentSizeH = MAX(_imageContainerView.tz_height, self.tz_height);
|
||||
_scrollView.contentSize = CGSizeMake(self.scrollView.tz_width, contentSizeH);
|
||||
[_scrollView scrollRectToVisible:self.bounds animated:NO];
|
||||
_scrollView.alwaysBounceVertical = _imageContainerView.tz_height <= self.tz_height ? NO : YES;
|
||||
_imageView.frame = _imageContainerView.bounds;
|
||||
|
||||
[self refreshScrollViewContentSize];
|
||||
}
|
||||
|
||||
- (void)configMaximumZoomScale {
|
||||
_scrollView.maximumZoomScale = _allowCrop ? 6.0 : 4.0;
|
||||
|
||||
if ([self.asset isKindOfClass:[PHAsset class]]) {
|
||||
PHAsset *phAsset = (PHAsset *)self.asset;
|
||||
CGFloat aspectRatio = phAsset.pixelWidth / (CGFloat)phAsset.pixelHeight;
|
||||
// 优化超宽图片的显示
|
||||
if (aspectRatio > 1.5) {
|
||||
self.scrollView.maximumZoomScale *= aspectRatio / 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshScrollViewContentSize {
|
||||
if (_allowCrop) {
|
||||
// 1.7.2 如果允许裁剪,需要让图片的任意部分都能在裁剪框内,于是对_scrollView做了如下处理:
|
||||
// 1.让contentSize增大(裁剪框右下角的图片部分)
|
||||
CGFloat contentWidthAdd = (MIN(_imageContainerView.tz_width, self.scrollView.tz_width) - _cropRect.size.width) / 2;
|
||||
CGFloat contentHeightAdd = (MIN(_imageContainerView.tz_height, self.scrollView.tz_height) - _cropRect.size.height) / 2;
|
||||
CGFloat newSizeW = MAX(self.scrollView.contentSize.width, self.scrollView.tz_width) + contentWidthAdd;
|
||||
CGFloat newSizeH = MAX(self.scrollView.contentSize.height, self.scrollView.tz_height) + contentHeightAdd;
|
||||
_scrollView.contentSize = CGSizeMake(newSizeW, newSizeH);
|
||||
_scrollView.alwaysBounceVertical = YES;
|
||||
// 2.让scrollView新增滑动区域(裁剪框左上角的图片部分)
|
||||
if (contentHeightAdd > 0 || contentWidthAdd > 0) {
|
||||
_scrollView.contentInset = UIEdgeInsetsMake(MAX(contentHeightAdd, 0), MAX(contentWidthAdd, 0), 0, 0);
|
||||
} else {
|
||||
_scrollView.contentInset = UIEdgeInsetsZero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_scrollView.frame = CGRectMake(10, 0, self.tz_width - 20, self.tz_height);
|
||||
static CGFloat progressWH = 40;
|
||||
CGFloat progressX = (self.tz_width - progressWH) / 2;
|
||||
CGFloat progressY = (self.tz_height - progressWH) / 2;
|
||||
_progressView.frame = CGRectMake(progressX, progressY, progressWH, progressWH);
|
||||
[self recoverSubviews];
|
||||
_iCloudErrorIcon.frame = CGRectMake(20, [TZCommonTools tz_statusBarHeight] + 44 + 10, 28, 28);
|
||||
_iCloudErrorLabel.frame = CGRectMake(53, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.tz_width - 63, 28);
|
||||
}
|
||||
|
||||
#pragma mark - UITapGestureRecognizer Event
|
||||
|
||||
- (void)doubleTap:(UITapGestureRecognizer *)tap {
|
||||
if (_scrollView.zoomScale > _scrollView.minimumZoomScale) {
|
||||
_scrollView.contentInset = UIEdgeInsetsZero;
|
||||
[_scrollView setZoomScale:_scrollView.minimumZoomScale animated:YES];
|
||||
} else {
|
||||
CGPoint touchPoint = [tap locationInView:self.imageView];
|
||||
CGFloat newZoomScale = MIN(_scrollView.maximumZoomScale, 2.5);
|
||||
CGFloat xsize = self.frame.size.width / newZoomScale;
|
||||
CGFloat ysize = self.frame.size.height / newZoomScale;
|
||||
[_scrollView zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)singleTap:(UITapGestureRecognizer *)tap {
|
||||
if (self.singleTapGestureBlock) {
|
||||
self.singleTapGestureBlock();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
|
||||
return _imageContainerView;
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
|
||||
scrollView.contentInset = UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
|
||||
[self refreshImageContainerViewCenter];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
|
||||
[self refreshScrollViewContentSize];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)refreshImageContainerViewCenter {
|
||||
CGFloat offsetX = (_scrollView.tz_width > _scrollView.contentSize.width) ? ((_scrollView.tz_width - _scrollView.contentSize.width) * 0.5) : 0.0;
|
||||
CGFloat offsetY = (_scrollView.tz_height > _scrollView.contentSize.height) ? ((_scrollView.tz_height - _scrollView.contentSize.height) * 0.5) : 0.0;
|
||||
self.imageContainerView.center = CGPointMake(_scrollView.contentSize.width * 0.5 + offsetX, _scrollView.contentSize.height * 0.5 + offsetY);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TZVideoPreviewCell
|
||||
|
||||
- (void)configSubviews {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
_iCloudErrorIcon = [[UIImageView alloc] init];
|
||||
_iCloudErrorIcon.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"];
|
||||
_iCloudErrorIcon.hidden = YES;
|
||||
_iCloudErrorLabel = [[UILabel alloc] init];
|
||||
_iCloudErrorLabel.font = [UIFont systemFontOfSize:10];
|
||||
_iCloudErrorLabel.textColor = [UIColor whiteColor];
|
||||
_iCloudErrorLabel.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"];
|
||||
_iCloudErrorLabel.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)configPlayButton {
|
||||
if (_playButton) {
|
||||
[_playButton removeFromSuperview];
|
||||
}
|
||||
_playButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted];
|
||||
[_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
_playButton.frame = CGRectMake(0, 64, self.tz_width, self.tz_height - 64 - 44);
|
||||
[self.contentView addSubview:_playButton];
|
||||
[self.contentView addSubview:_iCloudErrorIcon];
|
||||
[self.contentView addSubview:_iCloudErrorLabel];
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAssetModel *)model {
|
||||
[super setModel:model];
|
||||
[self configMoviePlayer];
|
||||
}
|
||||
|
||||
- (void)setVideoURL:(NSURL *)videoURL {
|
||||
_videoURL = videoURL;
|
||||
[self configMoviePlayer];
|
||||
}
|
||||
|
||||
- (void)configMoviePlayer {
|
||||
if (_player) {
|
||||
[_playerLayer removeFromSuperlayer];
|
||||
_playerLayer = nil;
|
||||
[_player pause];
|
||||
_player = nil;
|
||||
}
|
||||
|
||||
if (self.model && self.model.asset) {
|
||||
[[TZImageManager manager] getPhotoWithAsset:self.model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.iCloudErrorLabel.hidden = !iCloudSyncFailed;
|
||||
self.iCloudErrorIcon.hidden = !iCloudSyncFailed;
|
||||
if (self.iCloudSyncFailedHandle) {
|
||||
self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed);
|
||||
}
|
||||
if (photo) {
|
||||
self.cover = photo;
|
||||
}
|
||||
}];
|
||||
[[TZImageManager manager] getVideoWithAsset:self.model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
BOOL iCloudSyncFailed = !playerItem && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.iCloudErrorLabel.hidden = !iCloudSyncFailed;
|
||||
self.iCloudErrorIcon.hidden = !iCloudSyncFailed;
|
||||
if (self.iCloudSyncFailedHandle) {
|
||||
self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed);
|
||||
}
|
||||
[self configPlayerWithItem:playerItem];
|
||||
});
|
||||
}];
|
||||
} else {
|
||||
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:self.videoURL];
|
||||
[self configPlayerWithItem:playerItem];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configPlayerWithItem:(AVPlayerItem *)playerItem {
|
||||
self.player = [AVPlayer playerWithPlayerItem:playerItem];
|
||||
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
|
||||
self.playerLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
self.playerLayer.frame = self.bounds;
|
||||
[self.contentView.layer addSublayer:self.playerLayer];
|
||||
[self configPlayButton];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_playerLayer.frame = self.bounds;
|
||||
_playButton.frame = CGRectMake(0, 64, self.tz_width, self.tz_height - 64 - 44);
|
||||
_iCloudErrorIcon.frame = CGRectMake(20, [TZCommonTools tz_statusBarHeight] + 44 + 10, 28, 28);
|
||||
_iCloudErrorLabel.frame = CGRectMake(53, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.tz_width - 63, 28);
|
||||
}
|
||||
|
||||
- (void)photoPreviewCollectionViewDidScroll {
|
||||
if (_player && _player.rate != 0.0) {
|
||||
[self pausePlayerAndShowNaviBar];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notification
|
||||
|
||||
- (void)appWillResignActiveNotification {
|
||||
if (_player && _player.rate != 0.0) {
|
||||
[self pausePlayerAndShowNaviBar];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)playButtonClick {
|
||||
CMTime currentTime = _player.currentItem.currentTime;
|
||||
CMTime durationTime = _player.currentItem.duration;
|
||||
if (_player.rate == 0.0f) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:_player];
|
||||
if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)];
|
||||
[_player play];
|
||||
[_playButton setImage:nil forState:UIControlStateNormal];
|
||||
[UIApplication sharedApplication].statusBarHidden = YES;
|
||||
if (self.singleTapGestureBlock) {
|
||||
self.singleTapGestureBlock();
|
||||
}
|
||||
} else {
|
||||
[self pausePlayerAndShowNaviBar];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pausePlayerAndShowNaviBar {
|
||||
[_player pause];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
if (self.singleTapGestureBlock) {
|
||||
self.singleTapGestureBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TZGifPreviewCell
|
||||
|
||||
- (void)configSubviews {
|
||||
[self configPreviewView];
|
||||
}
|
||||
|
||||
- (void)configPreviewView {
|
||||
_previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_previewView setSingleTapGestureBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf signleTapAction];
|
||||
}];
|
||||
[self.contentView addSubview:_previewView];
|
||||
}
|
||||
|
||||
- (void)setModel:(TZAssetModel *)model {
|
||||
[super setModel:model];
|
||||
_previewView.model = self.model;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
_previewView.frame = self.bounds;
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)signleTapAction {
|
||||
if (self.singleTapGestureBlock) {
|
||||
self.singleTapGestureBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// TZPhotoPreviewController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TZPhotoPreviewController : UIViewController
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *models; ///< All photo models / 所有图片模型数组
|
||||
@property (nonatomic, strong) NSMutableArray *photos; ///< All photos / 所有图片数组
|
||||
@property (nonatomic, assign) NSInteger currentIndex; ///< Index of the photo user click / 用户点击的图片的索引
|
||||
@property (nonatomic, assign) BOOL isSelectOriginalPhoto; ///< If YES,return original photo / 是否返回原图
|
||||
@property (nonatomic, assign) BOOL isCropImage;
|
||||
|
||||
/// Return the new selected photos / 返回最新的选中图片数组
|
||||
@property (nonatomic, copy) void (^backButtonClickBlock)(BOOL isSelectOriginalPhoto);
|
||||
@property (nonatomic, copy) void (^doneButtonClickBlock)(BOOL isSelectOriginalPhoto);
|
||||
@property (nonatomic, copy) void (^doneButtonClickBlockCropMode)(UIImage *cropedImage,id asset);
|
||||
@property (nonatomic, copy) void (^doneButtonClickBlockWithPreviewType)(NSArray<UIImage *> *photos,NSArray *assets,BOOL isSelectOriginalPhoto);
|
||||
|
||||
@end
|
|
@ -0,0 +1,686 @@
|
|||
//
|
||||
// TZPhotoPreviewController.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 谭真 on 15/12/24.
|
||||
// Copyright © 2015年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZPhotoPreviewController.h"
|
||||
#import "TZPhotoPreviewCell.h"
|
||||
#import "TZAssetModel.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
#import "TZImagePickerController.h"
|
||||
#import "TZImageManager.h"
|
||||
#import "TZImageCropManager.h"
|
||||
|
||||
@interface TZPhotoPreviewController ()<UICollectionViewDataSource,UICollectionViewDelegate,UIScrollViewDelegate> {
|
||||
UICollectionView *_collectionView;
|
||||
UICollectionViewFlowLayout *_layout;
|
||||
NSArray *_photosTemp;
|
||||
NSArray *_assetsTemp;
|
||||
|
||||
UIView *_naviBar;
|
||||
UIButton *_backButton;
|
||||
UIButton *_selectButton;
|
||||
UILabel *_indexLabel;
|
||||
|
||||
UIView *_toolBar;
|
||||
UIButton *_doneButton;
|
||||
UIImageView *_numberImageView;
|
||||
UILabel *_numberLabel;
|
||||
UIButton *_originalPhotoButton;
|
||||
UILabel *_originalPhotoLabel;
|
||||
|
||||
CGFloat _offsetItemCount;
|
||||
|
||||
BOOL _didSetIsSelectOriginalPhoto;
|
||||
}
|
||||
@property (nonatomic, assign) BOOL isHideNaviBar;
|
||||
@property (nonatomic, strong) UIView *cropBgView;
|
||||
@property (nonatomic, strong) UIView *cropView;
|
||||
|
||||
@property (nonatomic, assign) double progress;
|
||||
@property (strong, nonatomic) UIAlertController *alertView;
|
||||
@property (nonatomic, strong) UIView *iCloudErrorView;
|
||||
@end
|
||||
|
||||
@implementation TZPhotoPreviewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[TZImageManager manager].shouldFixOrientation = YES;
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (!_didSetIsSelectOriginalPhoto) {
|
||||
_isSelectOriginalPhoto = _tzImagePickerVc.isSelectOriginalPhoto;
|
||||
}
|
||||
if (!self.models.count) {
|
||||
self.models = [NSMutableArray arrayWithArray:_tzImagePickerVc.selectedModels];
|
||||
_assetsTemp = [NSMutableArray arrayWithArray:_tzImagePickerVc.selectedAssets];
|
||||
}
|
||||
[self configCollectionView];
|
||||
[self configCustomNaviBar];
|
||||
[self configBottomToolBar];
|
||||
self.view.clipsToBounds = YES;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarOrientationNotification:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)setIsSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto {
|
||||
_isSelectOriginalPhoto = isSelectOriginalPhoto;
|
||||
_didSetIsSelectOriginalPhoto = YES;
|
||||
}
|
||||
|
||||
- (void)setPhotos:(NSMutableArray *)photos {
|
||||
_photos = photos;
|
||||
_photosTemp = [NSArray arrayWithArray:photos];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
[UIApplication sharedApplication].statusBarHidden = YES;
|
||||
[_collectionView setContentOffset:CGPointMake((self.view.tz_width + 20) * self.currentIndex, 0) animated:NO];
|
||||
[self refreshNaviBarAndBottomBarState];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePickerVc.needShowStatusBar) {
|
||||
[UIApplication sharedApplication].statusBarHidden = NO;
|
||||
}
|
||||
[self.navigationController setNavigationBarHidden:NO animated:YES];
|
||||
[TZImageManager manager].shouldFixOrientation = NO;
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)configCustomNaviBar {
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
|
||||
_naviBar = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
_naviBar.backgroundColor = [UIColor colorWithRed:(34/255.0) green:(34/255.0) blue:(34/255.0) alpha:0.7];
|
||||
|
||||
_backButton = [[UIButton alloc] initWithFrame:CGRectZero];
|
||||
[_backButton setImage:[UIImage tz_imageNamedFromMyBundle:@"navi_back"] forState:UIControlStateNormal];
|
||||
[_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
[_backButton addTarget:self action:@selector(backButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
_selectButton = [[UIButton alloc] initWithFrame:CGRectZero];
|
||||
[_selectButton setImage:tzImagePickerVc.photoDefImage forState:UIControlStateNormal];
|
||||
[_selectButton setImage:tzImagePickerVc.photoSelImage forState:UIControlStateSelected];
|
||||
_selectButton.imageView.clipsToBounds = YES;
|
||||
_selectButton.imageEdgeInsets = UIEdgeInsetsMake(10, 0, 10, 0);
|
||||
_selectButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[_selectButton addTarget:self action:@selector(select:) forControlEvents:UIControlEventTouchUpInside];
|
||||
_selectButton.hidden = !tzImagePickerVc.showSelectBtn;
|
||||
|
||||
_indexLabel = [[UILabel alloc] init];
|
||||
_indexLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_indexLabel.font = [UIFont systemFontOfSize:14];
|
||||
_indexLabel.textColor = [UIColor whiteColor];
|
||||
_indexLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
[_naviBar addSubview:_selectButton];
|
||||
[_naviBar addSubview:_indexLabel];
|
||||
[_naviBar addSubview:_backButton];
|
||||
[self.view addSubview:_naviBar];
|
||||
}
|
||||
|
||||
- (void)configBottomToolBar {
|
||||
_toolBar = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
static CGFloat rgb = 34 / 255.0;
|
||||
_toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7];
|
||||
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (_tzImagePickerVc.allowPickingOriginalPhoto) {
|
||||
_originalPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_originalPhotoButton.imageEdgeInsets = UIEdgeInsetsMake(0, [TZCommonTools tz_isRightToLeftLayout] ? 10 : -10, 0, 0);
|
||||
_originalPhotoButton.backgroundColor = [UIColor clearColor];
|
||||
[_originalPhotoButton addTarget:self action:@selector(originalPhotoButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
_originalPhotoButton.titleLabel.font = [UIFont systemFontOfSize:13];
|
||||
[_originalPhotoButton setTitle:_tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateNormal];
|
||||
[_originalPhotoButton setTitle:_tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateSelected];
|
||||
[_originalPhotoButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
|
||||
[_originalPhotoButton setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
|
||||
[_originalPhotoButton setImage:_tzImagePickerVc.photoPreviewOriginDefImage forState:UIControlStateNormal];
|
||||
[_originalPhotoButton setImage:_tzImagePickerVc.photoOriginSelImage forState:UIControlStateSelected];
|
||||
|
||||
_originalPhotoLabel = [[UILabel alloc] init];
|
||||
_originalPhotoLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_originalPhotoLabel.font = [UIFont systemFontOfSize:13];
|
||||
_originalPhotoLabel.textColor = [UIColor whiteColor];
|
||||
_originalPhotoLabel.backgroundColor = [UIColor clearColor];
|
||||
if (_isSelectOriginalPhoto) [self showPhotoBytes];
|
||||
}
|
||||
|
||||
_doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_doneButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_doneButton setTitle:_tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:_tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal];
|
||||
|
||||
_numberImageView = [[UIImageView alloc] initWithImage:_tzImagePickerVc.photoNumberIconImage];
|
||||
_numberImageView.backgroundColor = [UIColor clearColor];
|
||||
_numberImageView.clipsToBounds = YES;
|
||||
_numberImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_numberImageView.hidden = _tzImagePickerVc.selectedModels.count <= 0;
|
||||
|
||||
_numberLabel = [[UILabel alloc] init];
|
||||
_numberLabel.font = [UIFont systemFontOfSize:15];
|
||||
_numberLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_numberLabel.textColor = [UIColor whiteColor];
|
||||
_numberLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_numberLabel.text = [NSString stringWithFormat:@"%zd",_tzImagePickerVc.selectedModels.count];
|
||||
_numberLabel.hidden = _tzImagePickerVc.selectedModels.count <= 0;
|
||||
_numberLabel.backgroundColor = [UIColor clearColor];
|
||||
_numberLabel.userInteractionEnabled = YES;
|
||||
|
||||
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonClick)];
|
||||
[_numberLabel addGestureRecognizer:tapGesture];
|
||||
|
||||
[_originalPhotoButton addSubview:_originalPhotoLabel];
|
||||
[_toolBar addSubview:_doneButton];
|
||||
[_toolBar addSubview:_originalPhotoButton];
|
||||
[_toolBar addSubview:_numberImageView];
|
||||
[_toolBar addSubview:_numberLabel];
|
||||
[self.view addSubview:_toolBar];
|
||||
|
||||
if (_tzImagePickerVc.photoPreviewPageUIConfigBlock) {
|
||||
_tzImagePickerVc.photoPreviewPageUIConfigBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configCollectionView {
|
||||
_layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
_layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_layout];
|
||||
_collectionView.backgroundColor = [UIColor blackColor];
|
||||
_collectionView.dataSource = self;
|
||||
_collectionView.delegate = self;
|
||||
_collectionView.pagingEnabled = YES;
|
||||
_collectionView.scrollsToTop = NO;
|
||||
_collectionView.showsHorizontalScrollIndicator = NO;
|
||||
_collectionView.contentOffset = CGPointMake(0, 0);
|
||||
_collectionView.contentSize = CGSizeMake(self.models.count * (self.view.tz_width + 20), 0);
|
||||
if (@available(iOS 11, *)) {
|
||||
_collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
}
|
||||
[self.view addSubview:_collectionView];
|
||||
[_collectionView registerClass:[TZPhotoPreviewCell class] forCellWithReuseIdentifier:@"TZPhotoPreviewCell"];
|
||||
[_collectionView registerClass:[TZPhotoPreviewCell class] forCellWithReuseIdentifier:@"TZPhotoPreviewCellGIF"];
|
||||
[_collectionView registerClass:[TZVideoPreviewCell class] forCellWithReuseIdentifier:@"TZVideoPreviewCell"];
|
||||
[_collectionView registerClass:[TZGifPreviewCell class] forCellWithReuseIdentifier:@"TZGifPreviewCell"];
|
||||
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (_tzImagePickerVc.scaleAspectFillCrop && _tzImagePickerVc.allowCrop) {
|
||||
_collectionView.scrollEnabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configCropView {
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (_tzImagePickerVc.maxImagesCount <= 1 && _tzImagePickerVc.allowCrop && _tzImagePickerVc.allowPickingImage) {
|
||||
[_cropView removeFromSuperview];
|
||||
[_cropBgView removeFromSuperview];
|
||||
|
||||
_cropBgView = [UIView new];
|
||||
_cropBgView.userInteractionEnabled = NO;
|
||||
_cropBgView.frame = self.view.bounds;
|
||||
_cropBgView.backgroundColor = [UIColor clearColor];
|
||||
[self.view addSubview:_cropBgView];
|
||||
[TZImageCropManager overlayClippingWithView:_cropBgView cropRect:_tzImagePickerVc.cropRect containerView:self.view needCircleCrop:_tzImagePickerVc.needCircleCrop];
|
||||
|
||||
_cropView = [UIView new];
|
||||
_cropView.userInteractionEnabled = NO;
|
||||
_cropView.frame = _tzImagePickerVc.cropRect;
|
||||
_cropView.backgroundColor = [UIColor clearColor];
|
||||
_cropView.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||
_cropView.layer.borderWidth = 1.0;
|
||||
if (_tzImagePickerVc.needCircleCrop) {
|
||||
_cropView.layer.cornerRadius = _tzImagePickerVc.cropRect.size.width / 2;
|
||||
_cropView.clipsToBounds = YES;
|
||||
}
|
||||
[self.view addSubview:_cropView];
|
||||
if (_tzImagePickerVc.cropViewSettingBlock) {
|
||||
_tzImagePickerVc.cropViewSettingBlock(_cropView);
|
||||
}
|
||||
|
||||
[self.view bringSubviewToFront:_naviBar];
|
||||
[self.view bringSubviewToFront:_toolBar];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
|
||||
BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height;
|
||||
CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0;
|
||||
CGFloat statusBarHeightInterval = isFullScreen ? (statusBarHeight - 20) : 0;
|
||||
CGFloat naviBarHeight = statusBarHeight + _tzImagePickerVc.navigationBar.tz_height;
|
||||
_naviBar.frame = CGRectMake(0, 0, self.view.tz_width, naviBarHeight);
|
||||
_backButton.frame = CGRectMake(10, 10 + statusBarHeightInterval, 44, 44);
|
||||
_selectButton.frame = CGRectMake(self.view.tz_width - 56, 10 + statusBarHeightInterval, 44, 44);
|
||||
_indexLabel.frame = _selectButton.frame;
|
||||
|
||||
_layout.itemSize = CGSizeMake(self.view.tz_width + 20, self.view.tz_height);
|
||||
_layout.minimumInteritemSpacing = 0;
|
||||
_layout.minimumLineSpacing = 0;
|
||||
_collectionView.frame = CGRectMake(-10, 0, self.view.tz_width + 20, self.view.tz_height);
|
||||
[_collectionView setCollectionViewLayout:_layout];
|
||||
if (_offsetItemCount > 0) {
|
||||
CGFloat offsetX = _offsetItemCount * _layout.itemSize.width;
|
||||
[_collectionView setContentOffset:CGPointMake(offsetX, 0)];
|
||||
}
|
||||
if (_tzImagePickerVc.allowCrop) {
|
||||
[_collectionView reloadData];
|
||||
}
|
||||
|
||||
CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom;
|
||||
CGFloat toolBarTop = self.view.tz_height - toolBarHeight;
|
||||
_toolBar.frame = CGRectMake(0, toolBarTop, self.view.tz_width, toolBarHeight);
|
||||
if (_tzImagePickerVc.allowPickingOriginalPhoto) {
|
||||
CGFloat fullImageWidth = [_tzImagePickerVc.fullImageBtnTitleStr boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:13]} context:nil].size.width;
|
||||
_originalPhotoButton.frame = CGRectMake(0, 0, fullImageWidth + 56, 44);
|
||||
_originalPhotoLabel.frame = CGRectMake(fullImageWidth + 42, 0, 80, 44);
|
||||
}
|
||||
[_doneButton sizeToFit];
|
||||
_doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44);
|
||||
_numberImageView.frame = CGRectMake(_doneButton.tz_left - 24 - 5, 10, 24, 24);
|
||||
_numberLabel.frame = _numberImageView.frame;
|
||||
|
||||
[self configCropView];
|
||||
|
||||
if (_tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock) {
|
||||
_tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notification
|
||||
|
||||
- (void)didChangeStatusBarOrientationNotification:(NSNotification *)noti {
|
||||
_offsetItemCount = _collectionView.contentOffset.x / _layout.itemSize.width;
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)select:(UIButton *)selectButton {
|
||||
[self select:selectButton refreshCount:YES];
|
||||
}
|
||||
|
||||
- (void)select:(UIButton *)selectButton refreshCount:(BOOL)refreshCount {
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
TZAssetModel *model = _models[self.currentIndex];
|
||||
if (!selectButton.isSelected) {
|
||||
// 1. select:check if over the maxImagesCount / 选择照片,检查是否超过了最大个数的限制
|
||||
if (_tzImagePickerVc.selectedModels.count >= _tzImagePickerVc.maxImagesCount) {
|
||||
NSString *title = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Select a maximum of %zd photos"], _tzImagePickerVc.maxImagesCount];
|
||||
[_tzImagePickerVc showAlertWithTitle:title];
|
||||
return;
|
||||
// 2. if not over the maxImagesCount / 如果没有超过最大个数限制
|
||||
} else {
|
||||
if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) {
|
||||
return;
|
||||
}
|
||||
[_tzImagePickerVc addSelectedModel:model];
|
||||
[self setAsset:model.asset isSelect:YES];
|
||||
if (self.photos) {
|
||||
[_tzImagePickerVc.selectedAssets addObject:_assetsTemp[self.currentIndex]];
|
||||
[self.photos addObject:_photosTemp[self.currentIndex]];
|
||||
}
|
||||
if (model.type == TZAssetModelMediaTypeVideo && !_tzImagePickerVc.allowPickingMultipleVideo) {
|
||||
[_tzImagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Select the video when in multi state, we will handle the video as a photo"]];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSArray *selectedModels = [NSArray arrayWithArray:_tzImagePickerVc.selectedModels];
|
||||
for (TZAssetModel *model_item in selectedModels) {
|
||||
if ([model.asset.localIdentifier isEqualToString:model_item.asset.localIdentifier]) {
|
||||
// 1.6.7版本更新:防止有多个一样的model,一次性被移除了
|
||||
NSArray *selectedModelsTmp = [NSArray arrayWithArray:_tzImagePickerVc.selectedModels];
|
||||
for (NSInteger i = 0; i < selectedModelsTmp.count; i++) {
|
||||
TZAssetModel *model = selectedModelsTmp[i];
|
||||
if ([model isEqual:model_item]) {
|
||||
[_tzImagePickerVc removeSelectedModel:model];
|
||||
// [_tzImagePickerVc.selectedModels removeObjectAtIndex:i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (self.photos) {
|
||||
// 1.6.7版本更新:防止有多个一样的asset,一次性被移除了
|
||||
NSArray *selectedAssetsTmp = [NSArray arrayWithArray:_tzImagePickerVc.selectedAssets];
|
||||
for (NSInteger i = 0; i < selectedAssetsTmp.count; i++) {
|
||||
id asset = selectedAssetsTmp[i];
|
||||
if ([asset isEqual:_assetsTemp[self.currentIndex]]) {
|
||||
[_tzImagePickerVc.selectedAssets removeObjectAtIndex:i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// [_tzImagePickerVc.selectedAssets removeObject:_assetsTemp[self.currentIndex]];
|
||||
[self.photos removeObject:_photosTemp[self.currentIndex]];
|
||||
}
|
||||
[self setAsset:model.asset isSelect:NO];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
model.isSelected = !selectButton.isSelected;
|
||||
if (refreshCount) {
|
||||
[self refreshNaviBarAndBottomBarState];
|
||||
}
|
||||
if (model.isSelected) {
|
||||
[UIView showOscillatoryAnimationWithLayer:selectButton.imageView.layer type:TZOscillatoryAnimationToBigger];
|
||||
}
|
||||
[UIView showOscillatoryAnimationWithLayer:_numberImageView.layer type:TZOscillatoryAnimationToSmaller];
|
||||
}
|
||||
|
||||
- (void)backButtonClick {
|
||||
if (self.navigationController.childViewControllers.count < 2) {
|
||||
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
|
||||
if ([self.navigationController isKindOfClass: [TZImagePickerController class]]) {
|
||||
TZImagePickerController *nav = (TZImagePickerController *)self.navigationController;
|
||||
if (nav.imagePickerControllerDidCancelHandle) {
|
||||
nav.imagePickerControllerDidCancelHandle();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
if (self.backButtonClickBlock) {
|
||||
self.backButtonClickBlock(_isSelectOriginalPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doneButtonClick {
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
// 如果图片正在从iCloud同步中,提醒用户
|
||||
if (_progress > 0 && _progress < 1 && (_selectButton.isSelected || !_tzImagePickerVc.selectedModels.count )) {
|
||||
_alertView = [_tzImagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Synchronizing photos from iCloud"]];
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有选中过照片 点击确定时选中当前预览的照片
|
||||
if (_tzImagePickerVc.selectedModels.count == 0 && _tzImagePickerVc.minImagesCount <= 0 && _tzImagePickerVc.autoSelectCurrentWhenDone) {
|
||||
TZAssetModel *model = _models[self.currentIndex];
|
||||
if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) {
|
||||
return;
|
||||
}
|
||||
[self select:_selectButton refreshCount:NO];
|
||||
}
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0];
|
||||
TZPhotoPreviewCell *cell = (TZPhotoPreviewCell *)[_collectionView cellForItemAtIndexPath:indexPath];
|
||||
if (_tzImagePickerVc.allowCrop && [cell isKindOfClass:[TZPhotoPreviewCell class]]) { // 裁剪状态
|
||||
_doneButton.enabled = NO;
|
||||
[_tzImagePickerVc showProgressHUD];
|
||||
UIImage *cropedImage = [TZImageCropManager cropImageView:cell.previewView.imageView toRect:_tzImagePickerVc.cropRect zoomScale:cell.previewView.scrollView.zoomScale containerView:self.view];
|
||||
if (_tzImagePickerVc.needCircleCrop) {
|
||||
cropedImage = [TZImageCropManager circularClipImage:cropedImage];
|
||||
}
|
||||
_doneButton.enabled = YES;
|
||||
[_tzImagePickerVc hideProgressHUD];
|
||||
if (self.doneButtonClickBlockCropMode) {
|
||||
TZAssetModel *model = _models[self.currentIndex];
|
||||
self.doneButtonClickBlockCropMode(cropedImage,model.asset);
|
||||
}
|
||||
} else if (self.doneButtonClickBlock) { // 非裁剪状态
|
||||
self.doneButtonClickBlock(_isSelectOriginalPhoto);
|
||||
}
|
||||
if (self.doneButtonClickBlockWithPreviewType) {
|
||||
self.doneButtonClickBlockWithPreviewType(self.photos,_tzImagePickerVc.selectedAssets,self.isSelectOriginalPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)originalPhotoButtonClick {
|
||||
TZAssetModel *model = _models[self.currentIndex];
|
||||
if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) {
|
||||
return;
|
||||
}
|
||||
_originalPhotoButton.selected = !_originalPhotoButton.isSelected;
|
||||
_isSelectOriginalPhoto = _originalPhotoButton.isSelected;
|
||||
_originalPhotoLabel.hidden = !_originalPhotoButton.isSelected;
|
||||
if (_isSelectOriginalPhoto) {
|
||||
[self showPhotoBytes];
|
||||
if (!_selectButton.isSelected) {
|
||||
// 如果当前已选择照片张数 < 最大可选张数 && 最大可选张数大于1,就选中该张图
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (_tzImagePickerVc.selectedModels.count < _tzImagePickerVc.maxImagesCount && _tzImagePickerVc.showSelectBtn) {
|
||||
[self select:_selectButton];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didTapPreviewCell {
|
||||
self.isHideNaviBar = !self.isHideNaviBar;
|
||||
_naviBar.hidden = self.isHideNaviBar;
|
||||
_toolBar.hidden = self.isHideNaviBar;
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
CGFloat offSetWidth = scrollView.contentOffset.x;
|
||||
offSetWidth = offSetWidth + ((self.view.tz_width + 20) * 0.5);
|
||||
|
||||
NSInteger currentIndex = offSetWidth / (self.view.tz_width + 20);
|
||||
if (currentIndex < _models.count && _currentIndex != currentIndex) {
|
||||
_currentIndex = currentIndex;
|
||||
[self refreshNaviBarAndBottomBarState];
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"photoPreviewCollectionViewDidScroll" object:nil];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDataSource && Delegate
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return _models.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
TZAssetModel *model = _models[indexPath.item];
|
||||
|
||||
TZAssetPreviewCell *cell;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
if (_tzImagePickerVc.allowPickingMultipleVideo && model.type == TZAssetModelMediaTypeVideo) {
|
||||
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZVideoPreviewCell" forIndexPath:indexPath];
|
||||
TZVideoPreviewCell *currentCell = (TZVideoPreviewCell *)cell;
|
||||
currentCell.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
|
||||
model.iCloudFailed = isSyncFailed;
|
||||
[weakSelf didICloudSyncStatusChanged:model];
|
||||
};
|
||||
} else if (_tzImagePickerVc.allowPickingMultipleVideo && model.type == TZAssetModelMediaTypePhotoGif && _tzImagePickerVc.allowPickingGif) {
|
||||
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZGifPreviewCell" forIndexPath:indexPath];
|
||||
TZGifPreviewCell *currentCell = (TZGifPreviewCell *)cell;
|
||||
currentCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
|
||||
model.iCloudFailed = isSyncFailed;
|
||||
[weakSelf didICloudSyncStatusChanged:model];
|
||||
};
|
||||
} else {
|
||||
NSString *reuseId = model.type == TZAssetModelMediaTypePhotoGif ? @"TZPhotoPreviewCellGIF" : @"TZPhotoPreviewCell";
|
||||
cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath];
|
||||
TZPhotoPreviewCell *photoPreviewCell = (TZPhotoPreviewCell *)cell;
|
||||
photoPreviewCell.cropRect = _tzImagePickerVc.cropRect;
|
||||
photoPreviewCell.allowCrop = _tzImagePickerVc.allowCrop;
|
||||
photoPreviewCell.scaleAspectFillCrop = _tzImagePickerVc.scaleAspectFillCrop;
|
||||
__weak typeof(_collectionView) weakCollectionView = _collectionView;
|
||||
__weak typeof(photoPreviewCell) weakCell = photoPreviewCell;
|
||||
[photoPreviewCell setImageProgressUpdateBlock:^(double progress) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
__strong typeof(weakCollectionView) strongCollectionView = weakCollectionView;
|
||||
__strong typeof(weakCell) strongCell = weakCell;
|
||||
strongSelf.progress = progress;
|
||||
if (progress >= 1) {
|
||||
if (strongSelf.isSelectOriginalPhoto) [strongSelf showPhotoBytes];
|
||||
if (strongSelf.alertView && [strongCollectionView.visibleCells containsObject:strongCell]) {
|
||||
[strongSelf.alertView dismissViewControllerAnimated:YES completion:^{
|
||||
strongSelf.alertView = nil;
|
||||
[strongSelf doneButtonClick];
|
||||
}];
|
||||
}
|
||||
}
|
||||
}];
|
||||
photoPreviewCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
|
||||
model.iCloudFailed = isSyncFailed;
|
||||
[weakSelf didICloudSyncStatusChanged:model];
|
||||
};
|
||||
}
|
||||
|
||||
cell.model = model;
|
||||
[cell setSingleTapGestureBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf didTapPreviewCell];
|
||||
}];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if ([cell isKindOfClass:[TZPhotoPreviewCell class]]) {
|
||||
[(TZPhotoPreviewCell *)cell recoverSubviews];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if ([cell isKindOfClass:[TZPhotoPreviewCell class]]) {
|
||||
[(TZPhotoPreviewCell *)cell recoverSubviews];
|
||||
} else if ([cell isKindOfClass:[TZVideoPreviewCell class]]) {
|
||||
TZVideoPreviewCell *videoCell = (TZVideoPreviewCell *)cell;
|
||||
if (videoCell.player && videoCell.player.rate != 0.0) {
|
||||
[videoCell pausePlayerAndShowNaviBar];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private Method
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
// NSLog(@"%@ dealloc",NSStringFromClass(self.class));
|
||||
}
|
||||
|
||||
- (void)refreshNaviBarAndBottomBarState {
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
TZAssetModel *model = _models[self.currentIndex];
|
||||
_selectButton.selected = model.isSelected;
|
||||
[self refreshSelectButtonImageViewContentMode];
|
||||
if (_selectButton.isSelected && _tzImagePickerVc.showSelectedIndex && _tzImagePickerVc.showSelectBtn) {
|
||||
NSString *index = [NSString stringWithFormat:@"%d", (int)([_tzImagePickerVc.selectedAssetIds indexOfObject:model.asset.localIdentifier] + 1)];
|
||||
_indexLabel.text = index;
|
||||
_indexLabel.hidden = NO;
|
||||
} else {
|
||||
_indexLabel.hidden = YES;
|
||||
}
|
||||
_numberLabel.text = [NSString stringWithFormat:@"%zd",_tzImagePickerVc.selectedModels.count];
|
||||
_numberImageView.hidden = (_tzImagePickerVc.selectedModels.count <= 0 || _isHideNaviBar || _isCropImage);
|
||||
_numberLabel.hidden = (_tzImagePickerVc.selectedModels.count <= 0 || _isHideNaviBar || _isCropImage);
|
||||
|
||||
_originalPhotoButton.selected = _isSelectOriginalPhoto;
|
||||
_originalPhotoLabel.hidden = !_originalPhotoButton.isSelected;
|
||||
if (_isSelectOriginalPhoto) [self showPhotoBytes];
|
||||
|
||||
// If is previewing video, hide original photo button
|
||||
// 如果正在预览的是视频,隐藏原图按钮
|
||||
if (!_isHideNaviBar) {
|
||||
if (model.type == TZAssetModelMediaTypeVideo) {
|
||||
_originalPhotoButton.hidden = YES;
|
||||
_originalPhotoLabel.hidden = YES;
|
||||
} else {
|
||||
_originalPhotoButton.hidden = NO;
|
||||
if (_isSelectOriginalPhoto) _originalPhotoLabel.hidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
_doneButton.hidden = NO;
|
||||
_selectButton.hidden = !_tzImagePickerVc.showSelectBtn;
|
||||
// 让宽度/高度小于 最小可选照片尺寸 的图片不能选中
|
||||
if (![[TZImageManager manager] isPhotoSelectableWithAsset:model.asset]) {
|
||||
_numberLabel.hidden = YES;
|
||||
_numberImageView.hidden = YES;
|
||||
_selectButton.hidden = YES;
|
||||
_originalPhotoButton.hidden = YES;
|
||||
_originalPhotoLabel.hidden = YES;
|
||||
_doneButton.hidden = YES;
|
||||
}
|
||||
// iCloud同步失败的UI刷新
|
||||
[self didICloudSyncStatusChanged:model];
|
||||
if (_tzImagePickerVc.photoPreviewPageDidRefreshStateBlock) {
|
||||
_tzImagePickerVc.photoPreviewPageDidRefreshStateBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshSelectButtonImageViewContentMode {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (self->_selectButton.imageView.image.size.width <= 27) {
|
||||
self->_selectButton.imageView.contentMode = UIViewContentModeCenter;
|
||||
} else {
|
||||
self->_selectButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didICloudSyncStatusChanged:(TZAssetModel *)model{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
// onlyReturnAsset为NO时,依赖TZ返回大图,所以需要有iCloud同步失败的提示,并且不能选择,
|
||||
if (_tzImagePickerVc.onlyReturnAsset) {
|
||||
return;
|
||||
}
|
||||
TZAssetModel *currentModel = self.models[self.currentIndex];
|
||||
if (_tzImagePickerVc.selectedModels.count <= 0) {
|
||||
self->_doneButton.enabled = !currentModel.iCloudFailed;
|
||||
} else {
|
||||
self->_doneButton.enabled = YES;
|
||||
}
|
||||
self->_selectButton.hidden = currentModel.iCloudFailed || !_tzImagePickerVc.showSelectBtn;
|
||||
if (currentModel.iCloudFailed) {
|
||||
self->_originalPhotoButton.hidden = YES;
|
||||
self->_originalPhotoLabel.hidden = YES;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)showPhotoBytes {
|
||||
[[TZImageManager manager] getPhotosBytesWithArray:@[_models[self.currentIndex]] completion:^(NSString *totalBytes) {
|
||||
self->_originalPhotoLabel.text = [NSString stringWithFormat:@"(%@)",totalBytes];
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSInteger)currentIndex {
|
||||
return [TZCommonTools tz_isRightToLeftLayout] ? self.models.count - _currentIndex - 1 : _currentIndex;
|
||||
}
|
||||
|
||||
/// 选中/取消选中某张照片
|
||||
- (void)setAsset:(PHAsset *)asset isSelect:(BOOL)isSelect {
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didSelectAsset:photo:isSelectOriginalPhoto:)]) {
|
||||
[self callDelegate:asset isSelect:YES];
|
||||
}
|
||||
if (!isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didDeselectAsset:photo:isSelectOriginalPhoto:)]) {
|
||||
[self callDelegate:asset isSelect:NO];
|
||||
}
|
||||
}
|
||||
|
||||
/// 调用选中/取消选中某张照片的代理方法
|
||||
- (void)callDelegate:(PHAsset *)asset isSelect:(BOOL)isSelect {
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
__weak typeof(tzImagePickerVc) weakImagePickerVc= tzImagePickerVc;
|
||||
[[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
if (isDegraded) return;
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
__strong typeof(weakImagePickerVc) strongImagePickerVc = weakImagePickerVc;
|
||||
if (isSelect) {
|
||||
[strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didSelectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto];
|
||||
} else {
|
||||
[strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didDeselectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// TZProgressView.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by ttouch on 2016/12/6.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TZProgressView : UIView
|
||||
|
||||
@property (nonatomic, assign) double progress;
|
||||
|
||||
@end
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// TZProgressView.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by ttouch on 2016/12/6.
|
||||
// Copyright © 2016年 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZProgressView.h"
|
||||
|
||||
@interface TZProgressView ()
|
||||
@property (nonatomic, strong) CAShapeLayer *progressLayer;
|
||||
@end
|
||||
|
||||
@implementation TZProgressView
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
_progressLayer = [CAShapeLayer layer];
|
||||
_progressLayer.fillColor = [[UIColor clearColor] CGColor];
|
||||
_progressLayer.strokeColor = [[UIColor whiteColor] CGColor];
|
||||
_progressLayer.opacity = 1;
|
||||
_progressLayer.lineCap = kCALineCapRound;
|
||||
_progressLayer.lineWidth = 5;
|
||||
|
||||
[_progressLayer setShadowColor:[UIColor blackColor].CGColor];
|
||||
[_progressLayer setShadowOffset:CGSizeMake(1, 1)];
|
||||
[_progressLayer setShadowOpacity:0.5];
|
||||
[_progressLayer setShadowRadius:2];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGPoint center = CGPointMake(rect.size.width / 2, rect.size.height / 2);
|
||||
CGFloat radius = rect.size.width / 2;
|
||||
CGFloat startA = - M_PI_2;
|
||||
CGFloat endA = - M_PI_2 + M_PI * 2 * _progress;
|
||||
_progressLayer.frame = self.bounds;
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES];
|
||||
_progressLayer.path =[path CGPath];
|
||||
|
||||
[_progressLayer removeFromSuperlayer];
|
||||
[self.layer addSublayer:_progressLayer];
|
||||
}
|
||||
|
||||
- (void)setProgress:(double)progress {
|
||||
_progress = progress;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// TZVideoCropController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 肖兰月 on 2021/5/27.
|
||||
// Copyright © 2021 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TZAssetModel,TZImagePickerController;
|
||||
|
||||
@interface TZVideoCropController : UIViewController<UIViewControllerTransitioningDelegate>
|
||||
@property (nonatomic, strong) TZAssetModel *model;
|
||||
@property (nonatomic, weak) TZImagePickerController *imagePickerVc;
|
||||
@end
|
||||
|
||||
@protocol TZVideoEditViewDelegate <NSObject>
|
||||
- (void)editViewCropRectBeginChange;
|
||||
- (void)editViewCropRectEndChange;
|
||||
@end
|
||||
|
||||
@interface TZVideoEditView : UIView
|
||||
@property (strong, nonatomic) UIImageView *beginImgView;
|
||||
@property (strong, nonatomic) UIImageView *endImgView;
|
||||
@property (strong, nonatomic) UIView *indicatorLine;
|
||||
@property (assign, nonatomic) CGFloat videoDuration;
|
||||
@property (assign, nonatomic) NSInteger maxCropVideoDuration;
|
||||
@property (assign, nonatomic) CGRect cropRect;
|
||||
@property (assign, nonatomic) CGFloat allImgWidth;
|
||||
@property (assign, nonatomic) CGFloat minCropRectWidth;
|
||||
|
||||
@property (nonatomic, weak) id<TZVideoEditViewDelegate> delegate;
|
||||
|
||||
- (void)resetIndicatorLine;
|
||||
- (void)indicatorLineAnimateWithDuration:(NSTimeInterval)duration cropRect:(CGRect)cropRect;
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@interface TZVideoPictureCell : UICollectionViewCell
|
||||
@property (strong, nonatomic) UIImageView *imgView;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,673 @@
|
|||
//
|
||||
// TZVideoCropController.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 肖兰月 on 2021/5/27.
|
||||
// Copyright © 2021 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZVideoCropController.h"
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import "UIView+TZLayout.h"
|
||||
#import "TZImageManager.h"
|
||||
#import "TZAssetModel.h"
|
||||
#import "TZImagePickerController.h"
|
||||
|
||||
@interface TZVideoCropController ()<TZVideoEditViewDelegate,UICollectionViewDelegate, UICollectionViewDataSource> {
|
||||
AVPlayer *_player;
|
||||
AVPlayerLayer *_playerLayer;
|
||||
UIButton *_playButton;
|
||||
UIImage *_cover;
|
||||
NSString *_outputPath;
|
||||
NSString *_errorMsg;
|
||||
|
||||
UIButton *_cancelButton;
|
||||
UIButton *_doneButton;
|
||||
UIProgressView *_progress;
|
||||
UILabel *_cropVideoDurationLabel;
|
||||
|
||||
AVAssetImageGenerator *_imageGenerator;
|
||||
AVAsset *_asset;
|
||||
|
||||
CGFloat _collectionViewBeginOffsetX;
|
||||
BOOL _isPlayed;
|
||||
CGFloat _itemW;
|
||||
BOOL _isDraging;
|
||||
|
||||
UIStatusBarStyle _originStatusBarStyle;
|
||||
}
|
||||
|
||||
// iCloud无法同步提示UI
|
||||
@property (nonatomic, strong) UIView *iCloudErrorView;
|
||||
@property (strong, nonatomic) UICollectionView *collectionView;
|
||||
@property (strong, nonatomic) TZVideoEditView *videoEditView;
|
||||
@property (strong, nonatomic) NSMutableArray *videoImgArray;
|
||||
@property (strong, nonatomic) NSArray *imageTimes;
|
||||
@property (strong, nonatomic) NSTimer *timer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TZVideoCropController
|
||||
|
||||
#define VideoEditLeftMargin 40
|
||||
#define PanImageWidth 10
|
||||
#define MinCropVideoDuration 1
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
[self configMoviePlayer];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayer) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
[self stopTimer];
|
||||
}
|
||||
|
||||
- (void)configMoviePlayer {
|
||||
[[TZImageManager manager] getPhotoWithAsset:_model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) {
|
||||
BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]];
|
||||
self.iCloudErrorView.hidden = !iCloudSyncFailed;
|
||||
self->_doneButton.enabled = !iCloudSyncFailed;
|
||||
}];
|
||||
[[TZImageManager manager] getVideoWithAsset:_model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self->_asset = playerItem.asset;
|
||||
self->_player = [AVPlayer playerWithPlayerItem:playerItem];
|
||||
self->_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self->_player];
|
||||
self->_playerLayer.frame = self.view.bounds;
|
||||
[self.view.layer addSublayer:self->_playerLayer];
|
||||
[self configPlayButton];
|
||||
[self configBottomToolBar];
|
||||
if (self.imagePickerVc.allowEditVideo) {
|
||||
[self configVideoImageCollectionView];
|
||||
[self configVideoEditView];
|
||||
[self generateVideoImage];
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayer) name:AVPlayerItemDidPlayToEndTimeNotification object:self->_player.currentItem];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)configPlayButton {
|
||||
_playButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted];
|
||||
[_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:_playButton];
|
||||
}
|
||||
|
||||
- (void)configBottomToolBar {
|
||||
_cropVideoDurationLabel = UILabel.new;
|
||||
_cropVideoDurationLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_cropVideoDurationLabel.textColor = UIColor.whiteColor;
|
||||
_cropVideoDurationLabel.font = [UIFont systemFontOfSize:12];
|
||||
[self.view addSubview:_cropVideoDurationLabel];
|
||||
|
||||
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_cancelButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[_cancelButton setTitle:[NSBundle tz_localizedStringForKey:@"Cancel"] forState:0];
|
||||
[_cancelButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||
[_cancelButton addTarget:self action:@selector(cancelButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:_cancelButton];
|
||||
|
||||
_doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_doneButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_doneButton setTitle:self.imagePickerVc.doneBtnTitleStr forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:self.imagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled];
|
||||
[self.view addSubview:_doneButton];
|
||||
|
||||
if (self.imagePickerVc.videoEditViewPageUIConfigBlock) {
|
||||
self.imagePickerVc.videoEditViewPageUIConfigBlock(_playButton, _cropVideoDurationLabel, _cancelButton, _doneButton);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configVideoImageCollectionView {
|
||||
_itemW = (self.view.tz_width - VideoEditLeftMargin * 2 - 2 * PanImageWidth) / 10.0;
|
||||
UICollectionViewFlowLayout *layout = UICollectionViewFlowLayout.new;
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
layout.itemSize = CGSizeMake(_itemW, _itemW * 2);
|
||||
layout.minimumLineSpacing = 0;
|
||||
layout.minimumInteritemSpacing = 0;
|
||||
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
||||
_collectionView.dataSource = self;
|
||||
_collectionView.delegate = self;
|
||||
_collectionView.contentInset = UIEdgeInsetsMake(0, VideoEditLeftMargin + PanImageWidth, 0, VideoEditLeftMargin + PanImageWidth);
|
||||
_collectionView.clipsToBounds = NO;
|
||||
_collectionView.showsHorizontalScrollIndicator = NO;
|
||||
_collectionView.alwaysBounceHorizontal = YES;
|
||||
[_collectionView registerClass:TZVideoPictureCell.class forCellWithReuseIdentifier:@"TZVideoPictureCell"];
|
||||
[self.view addSubview:_collectionView];
|
||||
}
|
||||
|
||||
- (void)configVideoEditView {
|
||||
_videoEditView = TZVideoEditView.new;
|
||||
_videoEditView.backgroundColor = UIColor.clearColor;
|
||||
_videoEditView.delegate = self;
|
||||
_videoEditView.maxCropVideoDuration = self.imagePickerVc.maxCropVideoDuration;
|
||||
[self.view addSubview:_videoEditView];
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle {
|
||||
if (self.imagePickerVc && [self.imagePickerVc isKindOfClass:[TZImagePickerController class]]) {
|
||||
return self.imagePickerVc.statusBarStyle;
|
||||
}
|
||||
return [super preferredStatusBarStyle];
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height;
|
||||
CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0;
|
||||
CGFloat statusBarAndNaviBarHeight = statusBarHeight + self.navigationController.navigationBar.tz_height;
|
||||
|
||||
CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom;
|
||||
CGFloat doneButtonWidth = [_doneButton.currentTitle boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.width;
|
||||
doneButtonWidth = MAX(44, doneButtonWidth);
|
||||
_cancelButton.frame = CGRectMake(12, self.view.tz_height - toolBarHeight, 44, 44);
|
||||
[_cancelButton sizeToFit];
|
||||
_cancelButton.tz_height = 44;
|
||||
_doneButton.frame = CGRectMake(self.view.tz_width - doneButtonWidth - 12, self.view.tz_height - toolBarHeight, doneButtonWidth, 44);
|
||||
_playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, self.view.tz_height - statusBarAndNaviBarHeight - toolBarHeight);
|
||||
|
||||
CGFloat collectionViewH = (self.view.tz_width - VideoEditLeftMargin * 2 - 2 * PanImageWidth) / 10.0 * 2;
|
||||
_collectionView.frame = CGRectMake(0, self.view.tz_height - collectionViewH - toolBarHeight - statusBarHeight, self.view.tz_width, collectionViewH);
|
||||
_videoEditView.frame = _collectionView.frame;
|
||||
_cropVideoDurationLabel.frame = CGRectMake(0, _videoEditView.tz_bottom, self.view.tz_width, 20);
|
||||
|
||||
CGFloat playerLayerHeight = CGRectGetMinY(_collectionView.frame) - statusBarHeight * 2;
|
||||
CGFloat playerLayerWidth = self.view.tz_width/self.view.tz_height * playerLayerHeight;
|
||||
CGFloat playerLayerLeft = (self.view.tz_width - playerLayerWidth) / 2.0;
|
||||
CGRect playerLayerFrame = CGRectMake(playerLayerLeft, statusBarHeight, playerLayerWidth, playerLayerHeight);
|
||||
_playerLayer.frame = playerLayerFrame;
|
||||
_playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, playerLayerHeight - statusBarAndNaviBarHeight);
|
||||
|
||||
if (self.imagePickerVc.videoEditViewPageDidLayoutSubviewsBlock) {
|
||||
self.imagePickerVc.videoEditViewPageDidLayoutSubviewsBlock(_playButton, _cropVideoDurationLabel, _cancelButton, _doneButton);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)generateVideoImage {
|
||||
_imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset];
|
||||
_imageGenerator.appliesPreferredTrackTransform = YES;
|
||||
_imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;
|
||||
_imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
|
||||
_imageGenerator.maximumSize = CGSizeMake(100, 100);
|
||||
|
||||
NSTimeInterval durationSeconds = self.model.asset.duration;
|
||||
self.videoEditView.videoDuration = durationSeconds;
|
||||
|
||||
NSUInteger imageCount = 10;
|
||||
CGFloat maxCropWidth = self.view.tz_width - (VideoEditLeftMargin + PanImageWidth) * 2;
|
||||
if (durationSeconds <= MinCropVideoDuration) return;
|
||||
if (durationSeconds <= self.imagePickerVc.maxCropVideoDuration) {
|
||||
imageCount = 10;
|
||||
self.videoEditView.allImgWidth = maxCropWidth;
|
||||
_cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"], (NSInteger)durationSeconds];
|
||||
} else {
|
||||
CGFloat singleWidthSecond = maxCropWidth / self.imagePickerVc.maxCropVideoDuration;
|
||||
CGFloat allImgWidth = singleWidthSecond * durationSeconds;
|
||||
self.videoEditView.allImgWidth = allImgWidth;
|
||||
imageCount = allImgWidth / _itemW;
|
||||
_cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"],(long)self.imagePickerVc.maxCropVideoDuration];
|
||||
}
|
||||
NSArray *assetTracks = [_asset tracksWithMediaType:AVMediaTypeVideo];
|
||||
if (!assetTracks.count) {
|
||||
self.iCloudErrorView.hidden = NO;
|
||||
_doneButton.enabled = NO;
|
||||
_cropVideoDurationLabel.hidden = YES;
|
||||
return;
|
||||
};
|
||||
Float64 frameRate = [[_asset tracksWithMediaType:AVMediaTypeVideo][0] nominalFrameRate];;
|
||||
NSMutableArray *times = NSMutableArray.array;
|
||||
NSTimeInterval intervalSecond = durationSeconds/imageCount;
|
||||
CMTime timeFrame;
|
||||
for (NSInteger i = 0; i < imageCount; i++) {
|
||||
timeFrame = CMTimeMake(intervalSecond * i *frameRate, frameRate);
|
||||
NSValue *timeValue = [NSValue valueWithCMTime:timeFrame];
|
||||
[times addObject:timeValue];
|
||||
}
|
||||
self.videoImgArray = NSMutableArray.new;
|
||||
self.imageTimes = times;
|
||||
typeof(self) weakSelf = self;
|
||||
[_imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
|
||||
if (image) {
|
||||
UIImage *img = [[UIImage alloc] initWithCGImage:image];
|
||||
[weakSelf.videoImgArray addObject:img];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[weakSelf.collectionView reloadData];
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectiobViewDataSource & UIcollectionViewDelegate
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.videoImgArray.count;
|
||||
}
|
||||
|
||||
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TZVideoPictureCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZVideoPictureCell" forIndexPath:indexPath];
|
||||
cell.imgView.image = self.videoImgArray[indexPath.item];
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
if (!_isDraging) return;
|
||||
CGFloat offsetX = scrollView.contentOffset.x;
|
||||
if (offsetX - _collectionViewBeginOffsetX >= self.view.tz_width) {
|
||||
[self.collectionView setContentOffset:CGPointMake(self.view.tz_width + _collectionViewBeginOffsetX, 0) animated:NO];
|
||||
} else if (_collectionViewBeginOffsetX - offsetX >= self.view.tz_width) {
|
||||
[self.collectionView setContentOffset:CGPointMake(_collectionViewBeginOffsetX - self.view.tz_width, 0) animated:NO];
|
||||
}
|
||||
|
||||
[self editViewCropRectBeginChange];
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||
_isDraging = YES;
|
||||
_collectionViewBeginOffsetX = scrollView.contentOffset.x;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||
_isDraging = NO;
|
||||
[self editViewCropRectEndChange];
|
||||
}
|
||||
|
||||
#pragma mark - TZVideoEditViewDelegate
|
||||
|
||||
- (void)editViewCropRectBeginChange {
|
||||
[self stopTimer];
|
||||
[_playerLayer.player seekToTime:[self getCropStartTime] toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
|
||||
|
||||
NSTimeInterval second = [self getCropVideoDuration];
|
||||
_cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"], (NSInteger)second];
|
||||
}
|
||||
|
||||
- (void)editViewCropRectEndChange {
|
||||
if (_isPlayed) {
|
||||
[self starTimer];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)playButtonClick {
|
||||
CMTime currentTime = _player.currentItem.currentTime;
|
||||
CMTime durationTime = _player.currentItem.duration;
|
||||
if (_player.rate == 0.0f) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:_player];
|
||||
if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)];
|
||||
_isPlayed = YES;
|
||||
[self starTimer];
|
||||
[_playButton setImage:nil forState:UIControlStateNormal];
|
||||
} else {
|
||||
_isPlayed = NO;
|
||||
[self stopTimer];
|
||||
[self pausePlayer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelButtonClick {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
- (void)doneButtonClick {
|
||||
if ([[TZImageManager manager] isAssetCannotBeSelected:_model.asset]) {
|
||||
return;
|
||||
}
|
||||
[self stopTimer];
|
||||
|
||||
TZImagePickerController *imagePickerVc = self.imagePickerVc;
|
||||
[imagePickerVc showProgressHUD];
|
||||
[[TZImageManager manager] getVideoOutputPathWithAsset:_model.asset presetName:imagePickerVc.presetName timeRange:[self getCropVideoTimeRange] success:^(NSString *outputPath) {
|
||||
[imagePickerVc hideProgressHUD];
|
||||
self->_outputPath = outputPath;
|
||||
[self dismissAndCallDelegateMethod];
|
||||
} failure:^(NSString *errorMessage, NSError *error) {
|
||||
[imagePickerVc hideProgressHUD];
|
||||
self->_errorMsg = errorMessage;
|
||||
[self dismissAndCallDelegateMethod];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAndCallDelegateMethod {
|
||||
[self dismissViewControllerAnimated:NO completion:^{
|
||||
[self callDelegateMethod];
|
||||
}];
|
||||
[self.imagePickerVc dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)callDelegateMethod {
|
||||
if (_outputPath) {
|
||||
NSURL *videoURL = [NSURL fileURLWithPath:_outputPath];
|
||||
if (self.imagePickerVc.saveEditedVideoToAlbum) {
|
||||
[[TZImageManager manager] saveVideoWithUrl:videoURL completion:^(PHAsset *asset, NSError *error) {
|
||||
if (error) { // 视频保存失败
|
||||
if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFailToSaveEditedVideoWithError:)]) {
|
||||
[self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFailToSaveEditedVideoWithError:error];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
UIImage *coverImage = [[TZImageManager manager] getImageWithVideoURL:videoURL];
|
||||
if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) {
|
||||
[self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFinishPickingAndEditingVideo:coverImage outputPath:_outputPath error:nil];
|
||||
}
|
||||
if (self.imagePickerVc.didFinishPickingAndEditingVideoHandle) {
|
||||
self.imagePickerVc.didFinishPickingAndEditingVideoHandle(coverImage, _outputPath, nil);
|
||||
}
|
||||
} else {
|
||||
if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) {
|
||||
[self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFinishPickingAndEditingVideo:nil outputPath:nil error:_errorMsg];
|
||||
}
|
||||
if (self.imagePickerVc.didFinishPickingAndEditingVideoHandle) {
|
||||
self.imagePickerVc.didFinishPickingAndEditingVideoHandle(nil, nil, _errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (CMTime)getCropStartTime {
|
||||
NSTimeInterval second = [self getCropVideoStartSecond];
|
||||
if (second > self.model.asset.duration) {
|
||||
second = roundf(self.model.asset.duration);
|
||||
}
|
||||
return CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale);
|
||||
}
|
||||
|
||||
- (CMTimeRange)getCropVideoTimeRange {
|
||||
NSTimeInterval startSecond = [self getCropVideoStartSecond];
|
||||
CMTime start = CMTimeMakeWithSeconds(startSecond, _playerLayer.player.currentTime.timescale);
|
||||
NSTimeInterval second = [self getCropVideoDuration];
|
||||
CMTime duration = CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale);
|
||||
return CMTimeRangeMake(start, duration);
|
||||
}
|
||||
|
||||
- (NSTimeInterval)getCropVideoDuration {
|
||||
CGFloat rectW = self.videoEditView.cropRect.size.width;
|
||||
CGFloat contentW = self.videoEditView.allImgWidth;
|
||||
CGFloat second = rectW / contentW * roundf(self.model.asset.duration);
|
||||
return roundf(second);
|
||||
}
|
||||
|
||||
- (NSTimeInterval)getCropVideoStartSecond {
|
||||
CGFloat offsetX = self.collectionView.contentOffset.x;
|
||||
CGFloat contentW = self.videoEditView.allImgWidth;
|
||||
CGFloat cropRectX = self.videoEditView.cropRect.origin.x - VideoEditLeftMargin - PanImageWidth;
|
||||
NSTimeInterval second = (offsetX + cropRectX) / contentW * roundf(self.model.asset.duration);
|
||||
if (second < 0) second = 0;
|
||||
return roundf(second);
|
||||
}
|
||||
|
||||
- (CMTime)getTimeOfSeek {
|
||||
NSTimeInterval second = [self getCropVideoStartSecond];
|
||||
if (second > self.model.asset.duration) {
|
||||
second = roundf(self.model.asset.duration);
|
||||
}
|
||||
return CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale);
|
||||
}
|
||||
|
||||
- (void)starTimer {
|
||||
[self stopTimer];
|
||||
NSTimeInterval timeInterval = [self getCropVideoDuration];
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(playCropVideo) userInfo:nil repeats:YES];
|
||||
[self.timer fire];
|
||||
}
|
||||
|
||||
- (void)stopTimer {
|
||||
if (self.timer) {
|
||||
[self.videoEditView resetIndicatorLine];
|
||||
[_player pause];
|
||||
[self.timer invalidate];
|
||||
self.timer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)playCropVideo {
|
||||
[_player seekToTime:[self getCropStartTime] toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
|
||||
[_player play];
|
||||
[self.videoEditView indicatorLineAnimateWithDuration:[self getCropVideoDuration] cropRect:self.videoEditView.cropRect];
|
||||
}
|
||||
|
||||
#pragma mark - Notification Method
|
||||
|
||||
- (void)pausePlayer {
|
||||
[_player pause];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
#pragma mark - lazy
|
||||
- (UIView *)iCloudErrorView{
|
||||
if (!_iCloudErrorView) {
|
||||
_iCloudErrorView = [[UIView alloc] initWithFrame:CGRectMake(0, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.view.tz_width, 28)];
|
||||
UIImageView *icloud = [[UIImageView alloc] init];
|
||||
icloud.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"];
|
||||
icloud.frame = CGRectMake(20, 0, 28, 28);
|
||||
[_iCloudErrorView addSubview:icloud];
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.frame = CGRectMake(53, 0, self.view.tz_width - 63, 28);
|
||||
label.font = [UIFont systemFontOfSize:10];
|
||||
label.textColor = [UIColor whiteColor];
|
||||
label.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"];
|
||||
[_iCloudErrorView addSubview:label];
|
||||
[self.view addSubview:_iCloudErrorView];
|
||||
_iCloudErrorView.hidden = YES;
|
||||
}
|
||||
return _iCloudErrorView;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"%s",__func__);
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TZVideoEditView {
|
||||
UILabel *_dragingLabel;
|
||||
CGFloat _itemWidth;
|
||||
CGFloat _beginOffsetX;
|
||||
CGFloat _endOffsetX;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self initSubViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initSubViews {
|
||||
_indicatorLine = UIView.new;
|
||||
_indicatorLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
[self addSubview:_indicatorLine];
|
||||
|
||||
_beginImgView = UIImageView.new;
|
||||
_beginImgView.image = [UIImage imageNamed:@"leftVideoEdit"];
|
||||
_beginImgView.userInteractionEnabled = YES;
|
||||
_beginImgView.tag = 0;
|
||||
UIPanGestureRecognizer *beginPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
|
||||
[_beginImgView addGestureRecognizer:beginPanGesture];
|
||||
[self addSubview:_beginImgView];
|
||||
|
||||
_endImgView = UIImageView.new;
|
||||
_endImgView.image = [UIImage imageNamed:@"rightVideoEdit"];
|
||||
_endImgView.userInteractionEnabled = YES;
|
||||
_endImgView.tag = 1;
|
||||
UIPanGestureRecognizer *endPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
|
||||
[_endImgView addGestureRecognizer:endPanGesture];
|
||||
[self addSubview:_endImgView];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
_beginImgView.frame = CGRectMake(VideoEditLeftMargin, 0, PanImageWidth, self.tz_height);
|
||||
_indicatorLine.frame = CGRectMake(_beginImgView.tz_right - 2, 2, 2, self.tz_height - 4);
|
||||
_endImgView.frame = CGRectMake(self.tz_width - PanImageWidth - VideoEditLeftMargin, 0, PanImageWidth, self.tz_height);
|
||||
|
||||
self.cropRect = CGRectMake(VideoEditLeftMargin + PanImageWidth, 0, self.tz_width - VideoEditLeftMargin * 2 - PanImageWidth * 2, self.tz_height);
|
||||
}
|
||||
|
||||
- (void)setAllImgWidth:(CGFloat)allImgWidth {
|
||||
_allImgWidth = allImgWidth;
|
||||
if ((NSInteger)roundf(self.videoDuration) <= 0) {
|
||||
self.minCropRectWidth = allImgWidth;
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat scale = MinCropVideoDuration / self.videoDuration;
|
||||
self.minCropRectWidth = scale * allImgWidth;
|
||||
}
|
||||
|
||||
- (void)setCropRect:(CGRect)cropRect {
|
||||
_cropRect = cropRect;
|
||||
self.beginImgView.tz_left = cropRect.origin.x - PanImageWidth;
|
||||
self.indicatorLine.tz_left = cropRect.origin.x - self.indicatorLine.tz_width;
|
||||
self.endImgView.tz_left = CGRectGetMaxX(cropRect);
|
||||
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextClearRect(context, rect);
|
||||
|
||||
CGPoint topPoints[] = {
|
||||
CGPointMake(self.cropRect.origin.x, 0),
|
||||
CGPointMake(CGRectGetMaxX(self.cropRect), 0)
|
||||
};
|
||||
CGPoint bottomPoints[] = {
|
||||
CGPointMake(self.cropRect.origin.x, self.tz_height),
|
||||
CGPointMake(CGRectGetMaxX(self.cropRect), self.tz_height)
|
||||
};
|
||||
|
||||
CGContextAddLines(context, topPoints, 2);
|
||||
CGContextAddLines(context, bottomPoints, 2);
|
||||
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
|
||||
CGContextSetLineWidth(context, 4.0);
|
||||
CGContextDrawPath(context, kCGPathStroke);
|
||||
}
|
||||
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
CGRect beginImgViewFrame = self.beginImgView.frame;
|
||||
beginImgViewFrame.origin.x -= PanImageWidth;
|
||||
beginImgViewFrame.size.width += PanImageWidth * 2;
|
||||
if (CGRectContainsPoint(beginImgViewFrame, point)) return self.beginImgView;
|
||||
|
||||
CGRect endImgViewFrame = self.endImgView.frame;
|
||||
endImgViewFrame.origin.x -= PanImageWidth;
|
||||
endImgViewFrame.size.width += PanImageWidth * 2;
|
||||
if (CGRectContainsPoint(endImgViewFrame, point)) return self.endImgView;
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (void)indicatorLineAnimateWithDuration:(NSTimeInterval)duration cropRect:(CGRect)cropRect {
|
||||
[self resetIndicatorLine];
|
||||
[UIView animateWithDuration:duration delay:.0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
self.indicatorLine.tz_left = CGRectGetMaxX(cropRect);
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (void)resetIndicatorLine {
|
||||
[self.indicatorLine.layer removeAllAnimations];
|
||||
self.indicatorLine.tz_left = CGRectGetMinX(self.cropRect) - self.indicatorLine.tz_width;
|
||||
}
|
||||
|
||||
- (void)panGestureAction:(UIGestureRecognizer *)gesture {
|
||||
CGPoint point = [gesture locationInView:self];
|
||||
CGRect rect = self.cropRect;
|
||||
CGFloat minCropRectLeft = VideoEditLeftMargin + PanImageWidth;
|
||||
|
||||
switch (gesture.view.tag) {
|
||||
case 0: { // 左边拖拽
|
||||
CGFloat maxX = self.endImgView.tz_left - self.minCropRectWidth;
|
||||
point.x = MAX(minCropRectLeft, MIN(point.x, maxX));
|
||||
point.y = 0;
|
||||
|
||||
rect.size.width = CGRectGetMaxX(rect) - point.x;
|
||||
rect.origin.x = point.x;
|
||||
} break;
|
||||
case 1: { // 右边拖拽
|
||||
minCropRectLeft = CGRectGetMaxX(self.beginImgView.frame) + self.minCropRectWidth;
|
||||
CGFloat maxX = self.tz_width - VideoEditLeftMargin - PanImageWidth;
|
||||
|
||||
point.x = MAX(minCropRectLeft, MIN(point.x, maxX));
|
||||
point.y = 0;
|
||||
|
||||
rect.size.width = (point.x - rect.origin.x);
|
||||
} break;
|
||||
default:break;
|
||||
}
|
||||
|
||||
self.cropRect = rect;
|
||||
|
||||
switch (gesture.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(editViewCropRectBeginChange)]) {
|
||||
[self.delegate editViewCropRectBeginChange];
|
||||
}
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled: {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(editViewCropRectEndChange)]) {
|
||||
[self.delegate editViewCropRectEndChange];
|
||||
}
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation TZVideoPictureCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self initSubViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initSubViews {
|
||||
_imgView = [[UIImageView alloc] initWithFrame:self.bounds];
|
||||
_imgView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_imgView.clipsToBounds = YES;
|
||||
[self.contentView addSubview:_imgView];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// TZVideoEditedPreviewController.h
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 肖兰月 on 2021/5/29.
|
||||
// Copyright © 2021 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TZVideoEditedPreviewController : UIViewController
|
||||
@property (nonatomic, copy) NSURL *videoURL;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// TZVideoEditedPreviewController.m
|
||||
// TZImagePickerController
|
||||
//
|
||||
// Created by 肖兰月 on 2021/5/29.
|
||||
// Copyright © 2021 谭真. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TZVideoEditedPreviewController.h"
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import "TZImageManager.h"
|
||||
#import "TZImagePickerController.h"
|
||||
#import "UIView+TZLayout.h"
|
||||
|
||||
@interface TZVideoEditedPreviewController () {
|
||||
AVPlayer *_player;
|
||||
AVPlayerLayer *_playerLayer;
|
||||
UIButton *_playButton;
|
||||
UIImage *_cover;
|
||||
|
||||
UIView *_toolBar;
|
||||
UIButton *_doneButton;
|
||||
UIProgressView *_progress;
|
||||
|
||||
UIStatusBarStyle _originStatusBarStyle;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TZVideoEditedPreviewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
[self configMoviePlayer];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)configMoviePlayer {
|
||||
_player = [AVPlayer playerWithURL:self.videoURL];
|
||||
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
|
||||
[self.view.layer addSublayer:_playerLayer];
|
||||
|
||||
[self configPlayButton];
|
||||
[self configBottomToolBar];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self->_player.currentItem];
|
||||
}
|
||||
|
||||
- (void)configPlayButton {
|
||||
_playButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted];
|
||||
[_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:_playButton];
|
||||
}
|
||||
|
||||
- (void)configBottomToolBar {
|
||||
_toolBar = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
CGFloat rgb = 34 / 255.0;
|
||||
_toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7];
|
||||
|
||||
_doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_doneButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside];
|
||||
TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController;
|
||||
if (tzImagePickerVc) {
|
||||
[_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal];
|
||||
} else {
|
||||
[_doneButton setTitle:[NSBundle tz_localizedStringForKey:@"Done"] forState:UIControlStateNormal];
|
||||
[_doneButton setTitleColor:[UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0] forState:UIControlStateNormal];
|
||||
}
|
||||
[_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled];
|
||||
[_toolBar addSubview:_doneButton];
|
||||
[self.view addSubview:_toolBar];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height;
|
||||
CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0;
|
||||
CGFloat statusBarAndNaviBarHeight = statusBarHeight + self.navigationController.navigationBar.tz_height;
|
||||
|
||||
CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom;
|
||||
_toolBar.frame = CGRectMake(0, self.view.tz_height - toolBarHeight, self.view.tz_width, toolBarHeight);
|
||||
[_doneButton sizeToFit];
|
||||
_doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44);
|
||||
_playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, self.view.tz_height - statusBarAndNaviBarHeight - toolBarHeight);
|
||||
_playerLayer.frame = self.view.bounds;
|
||||
}
|
||||
|
||||
#pragma mark - Click Event
|
||||
|
||||
- (void)playButtonClick {
|
||||
CMTime currentTime = _player.currentItem.currentTime;
|
||||
CMTime durationTime = _player.currentItem.duration;
|
||||
if (_player.rate == 0.0f) {
|
||||
if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)];
|
||||
[_player play];
|
||||
_toolBar.hidden = YES;
|
||||
[_playButton setImage:nil forState:UIControlStateNormal];
|
||||
} else {
|
||||
[self pausePlayerAndShowNaviBar];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doneButtonClick {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Notification Method
|
||||
|
||||
- (void)pausePlayerAndShowNaviBar {
|
||||
[_player pause];
|
||||
_toolBar.hidden = NO;
|
||||
[_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|