ZLPhotoPreviewSheet.swift 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. //
  2. // ZLPhotoPreviewSheet.swift
  3. // ZLPhotoBrowser
  4. //
  5. // Created by long on 2020/8/11.
  6. //
  7. // Copyright (c) 2020 Long Zhang <495181165@qq.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import UIKit
  27. import Photos
  28. public class ZLPhotoPreviewSheet: UIView {
  29. struct Layout {
  30. static let colH: CGFloat = 155
  31. static let btnH: CGFloat = 45
  32. static let spacing: CGFloat = 1 / UIScreen.main.scale
  33. }
  34. private var baseView: UIView!
  35. private var collectionView: UICollectionView!
  36. private var cameraBtn: UIButton!
  37. private var photoLibraryBtn: UIButton!
  38. private var cancelBtn: UIButton!
  39. private var flexibleView: UIView!
  40. private var placeholderLabel: UILabel!
  41. private var arrDataSources: [ZLPhotoModel] = []
  42. private var arrSelectedModels: [ZLPhotoModel] = []
  43. private var preview = false
  44. private var animate = true
  45. private var senderTabBarIsHidden: Bool?
  46. private var baseViewHeight: CGFloat = 0
  47. private var isSelectOriginal = false
  48. private var panBeginPoint: CGPoint = .zero
  49. private var panImageView: UIImageView?
  50. private var panModel: ZLPhotoModel?
  51. private var panCell: ZLThumbnailPhotoCell?
  52. private weak var sender: UIViewController?
  53. private var fetchImageQueue: OperationQueue = OperationQueue()
  54. /// Success callback
  55. /// block params
  56. /// - params1: images for asset.
  57. /// - params2: selected assets
  58. /// - params3: is full image
  59. @objc public var selectImageBlock: ( ([UIImage], [PHAsset], Bool) -> Void )?
  60. /// Callback for photos that failed to parse
  61. /// block params
  62. /// - params1: failed assets.
  63. /// - params2: index for asset
  64. @objc public var selectImageRequestErrorBlock: ( ([PHAsset], [Int]) -> Void )?
  65. @objc public var cancelBlock: ( () -> Void )?
  66. deinit {
  67. zl_debugPrint("ZLPhotoPreviewSheet deinit")
  68. }
  69. /// - Parameter selectedAssets: preselected assets
  70. @objc public init(selectedAssets: [PHAsset] = []) {
  71. super.init(frame: .zero)
  72. if !ZLPhotoConfiguration.default().allowSelectImage &&
  73. !ZLPhotoConfiguration.default().allowSelectVideo {
  74. assert(false, "ZLPhotoBrowser: error configuration")
  75. ZLPhotoConfiguration.default().allowSelectImage = true
  76. }
  77. self.fetchImageQueue.maxConcurrentOperationCount = 3
  78. self.setupUI()
  79. self.arrSelectedModels.removeAll()
  80. selectedAssets.removeDuplicate().forEach { (asset) in
  81. if !ZLPhotoConfiguration.default().allowMixSelect, asset.mediaType == .video {
  82. return
  83. }
  84. let m = ZLPhotoModel(asset: asset)
  85. m.isSelected = true
  86. self.arrSelectedModels.append(m)
  87. }
  88. }
  89. public required init?(coder: NSCoder) {
  90. fatalError("init(coder:) has not been implemented")
  91. }
  92. public override func layoutSubviews() {
  93. super.layoutSubviews()
  94. self.baseView.frame = CGRect(x: 0, y: self.bounds.height - self.baseViewHeight, width: self.bounds.width, height: self.baseViewHeight)
  95. var btnY: CGFloat = 0
  96. if ZLPhotoConfiguration.default().maxPreviewCount > 0 {
  97. self.collectionView.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: ZLPhotoPreviewSheet.Layout.colH)
  98. btnY += (self.collectionView.frame.maxY + ZLPhotoPreviewSheet.Layout.spacing)
  99. }
  100. if self.canShowCameraBtn() {
  101. self.cameraBtn.frame = CGRect(x: 0, y: btnY, width: self.bounds.width, height: ZLPhotoPreviewSheet.Layout.btnH)
  102. btnY += (ZLPhotoPreviewSheet.Layout.btnH + ZLPhotoPreviewSheet.Layout.spacing)
  103. }
  104. self.photoLibraryBtn.frame = CGRect(x: 0, y: btnY, width: self.bounds.width, height: ZLPhotoPreviewSheet.Layout.btnH)
  105. btnY += (ZLPhotoPreviewSheet.Layout.btnH + ZLPhotoPreviewSheet.Layout.spacing)
  106. self.cancelBtn.frame = CGRect(x: 0, y: btnY, width: self.bounds.width, height: ZLPhotoPreviewSheet.Layout.btnH)
  107. btnY += ZLPhotoPreviewSheet.Layout.btnH
  108. self.flexibleView.frame = CGRect(x: 0, y: btnY, width: self.bounds.width, height: self.baseViewHeight - btnY)
  109. }
  110. func setupUI() {
  111. self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  112. self.backgroundColor = .previewBgColor
  113. let showCameraBtn = self.canShowCameraBtn()
  114. var bh: CGFloat = 0
  115. if ZLPhotoConfiguration.default().maxPreviewCount > 0 {
  116. bh += ZLPhotoPreviewSheet.Layout.colH
  117. }
  118. bh += (ZLPhotoPreviewSheet.Layout.spacing + ZLPhotoPreviewSheet.Layout.btnH) * (showCameraBtn ? 3 : 2)
  119. bh += deviceSafeAreaInsets().bottom
  120. self.baseViewHeight = bh
  121. self.baseView = UIView()
  122. self.baseView.backgroundColor = zlRGB(230, 230, 230)
  123. self.addSubview(self.baseView)
  124. let layout = UICollectionViewFlowLayout()
  125. layout.scrollDirection = .horizontal
  126. layout.minimumInteritemSpacing = 3
  127. layout.minimumLineSpacing = 3
  128. layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
  129. self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
  130. self.collectionView.backgroundColor = .previewBtnBgColor
  131. self.collectionView.delegate = self
  132. self.collectionView.dataSource = self
  133. self.collectionView.isHidden = ZLPhotoConfiguration.default().maxPreviewCount == 0
  134. ZLThumbnailPhotoCell.zl_register(self.collectionView)
  135. self.baseView.addSubview(self.collectionView)
  136. self.placeholderLabel = UILabel()
  137. self.placeholderLabel.font = getFont(15)
  138. self.placeholderLabel.text = localLanguageTextValue(.noPhotoTips)
  139. self.placeholderLabel.textAlignment = .center
  140. self.placeholderLabel.textColor = .previewBtnTitleColor
  141. self.collectionView.backgroundView = self.placeholderLabel
  142. func createBtn(_ title: String) -> UIButton {
  143. let btn = UIButton(type: .custom)
  144. btn.backgroundColor = .previewBtnBgColor
  145. btn.setTitleColor(.previewBtnTitleColor, for: .normal)
  146. btn.setTitle(title, for: .normal)
  147. btn.titleLabel?.font = getFont(17)
  148. return btn
  149. }
  150. let cameraTitle: String
  151. if !ZLPhotoConfiguration.default().allowTakePhoto, ZLPhotoConfiguration.default().allowRecordVideo {
  152. cameraTitle = localLanguageTextValue(.previewCameraRecord)
  153. } else {
  154. cameraTitle = localLanguageTextValue(.previewCamera)
  155. }
  156. self.cameraBtn = createBtn(cameraTitle)
  157. self.cameraBtn.isHidden = !showCameraBtn
  158. self.cameraBtn.addTarget(self, action: #selector(cameraBtnClick), for: .touchUpInside)
  159. self.baseView.addSubview(self.cameraBtn)
  160. self.photoLibraryBtn = createBtn(localLanguageTextValue(.previewAlbum))
  161. self.photoLibraryBtn.addTarget(self, action: #selector(photoLibraryBtnClick), for: .touchUpInside)
  162. self.baseView.addSubview(self.photoLibraryBtn)
  163. self.cancelBtn = createBtn(localLanguageTextValue(.cancel))
  164. self.cancelBtn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
  165. self.baseView.addSubview(self.cancelBtn)
  166. self.flexibleView = UIView()
  167. self.flexibleView.backgroundColor = .previewBtnBgColor
  168. self.baseView.addSubview(self.flexibleView)
  169. if ZLPhotoConfiguration.default().allowDragSelect {
  170. let pan = UIPanGestureRecognizer(target: self, action: #selector(panSelectAction(_:)))
  171. self.baseView.addGestureRecognizer(pan)
  172. }
  173. let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
  174. tap.delegate = self
  175. self.addGestureRecognizer(tap)
  176. }
  177. func canShowCameraBtn() -> Bool {
  178. if !ZLPhotoConfiguration.default().allowTakePhoto, !ZLPhotoConfiguration.default().allowRecordVideo {
  179. return false
  180. }
  181. return true
  182. }
  183. @objc public func showPreview(animate: Bool = true, sender: UIViewController) {
  184. self.show(preview: true, animate: animate, sender: sender)
  185. }
  186. @objc public func showPhotoLibrary(sender: UIViewController) {
  187. self.show(preview: false, animate: false, sender: sender)
  188. }
  189. /// 传入已选择的assets,并预览
  190. @objc public func previewAssets(sender: UIViewController, assets: [PHAsset], index: Int, isOriginal: Bool, showBottomViewAndSelectBtn: Bool = true) {
  191. let models = assets.removeDuplicate().map { (asset) -> ZLPhotoModel in
  192. let m = ZLPhotoModel(asset: asset)
  193. m.isSelected = true
  194. return m
  195. }
  196. self.arrSelectedModels.removeAll()
  197. self.arrSelectedModels.append(contentsOf: models)
  198. self.sender = sender
  199. self.isSelectOriginal = isOriginal
  200. self.isHidden = true
  201. self.sender?.view.addSubview(self)
  202. let vc = ZLPhotoPreviewController(photos: models, index: index, showBottomViewAndSelectBtn: showBottomViewAndSelectBtn)
  203. vc.autoSelectCurrentIfNotSelectAnyone = false
  204. let nav = self.getImageNav(rootViewController: vc)
  205. vc.backBlock = { [weak self] in
  206. self?.hide()
  207. }
  208. self.sender?.showDetailViewController(nav, sender: nil)
  209. }
  210. func show(preview: Bool, animate: Bool, sender: UIViewController) {
  211. self.preview = preview
  212. self.animate = animate
  213. self.sender = sender
  214. let status = PHPhotoLibrary.authorizationStatus()
  215. if status == .restricted || status == .denied {
  216. self.showNoAuthorityAlert()
  217. } else if status == .notDetermined {
  218. PHPhotoLibrary.requestAuthorization { (status) in
  219. DispatchQueue.main.async {
  220. if status == .denied {
  221. self.showNoAuthorityAlert()
  222. } else if status == .authorized {
  223. if self.preview {
  224. self.loadPhotos()
  225. self.show()
  226. } else {
  227. self.photoLibraryBtnClick()
  228. }
  229. }
  230. }
  231. }
  232. self.sender?.view.addSubview(self)
  233. } else {
  234. if preview {
  235. self.loadPhotos()
  236. self.show()
  237. } else {
  238. self.sender?.view.addSubview(self)
  239. self.photoLibraryBtnClick()
  240. }
  241. }
  242. // Register for the album change notification when the status is limited, because the photoLibraryDidChange method will be repeated multiple times each time the album changes, causing the interface to refresh multiple times. So the album changes are not monitored in other authority.
  243. if #available(iOS 14.0, *), preview, PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited {
  244. PHPhotoLibrary.shared().register(self)
  245. }
  246. }
  247. func loadPhotos() {
  248. self.arrDataSources.removeAll()
  249. let config = ZLPhotoConfiguration.default()
  250. ZLPhotoManager.getCameraRollAlbum(allowSelectImage: config.allowSelectImage, allowSelectVideo: config.allowSelectVideo) { [weak self] (cameraRoll) in
  251. guard let `self` = self else { return }
  252. var totalPhotos = ZLPhotoManager.fetchPhoto(in: cameraRoll.result, ascending: false, allowSelectImage: config.allowSelectImage, allowSelectVideo: config.allowSelectVideo, limitCount: config.maxPreviewCount)
  253. markSelected(source: &totalPhotos, selected: &self.arrSelectedModels)
  254. self.arrDataSources.append(contentsOf: totalPhotos)
  255. self.collectionView.reloadData()
  256. }
  257. }
  258. func show() {
  259. self.frame = self.sender?.view.bounds ?? .zero
  260. self.collectionView.contentOffset = .zero
  261. if self.superview == nil {
  262. self.sender?.view.addSubview(self)
  263. }
  264. if let tabBar = self.sender?.tabBarController?.tabBar, !tabBar.isHidden {
  265. self.senderTabBarIsHidden = tabBar.isHidden
  266. tabBar.isHidden = true
  267. }
  268. if self.animate {
  269. self.backgroundColor = UIColor.previewBgColor.withAlphaComponent(0)
  270. var frame = self.baseView.frame
  271. frame.origin.y = self.bounds.height
  272. self.baseView.frame = frame
  273. UIView.animate(withDuration: 0.2) {
  274. self.backgroundColor = UIColor.previewBgColor
  275. frame.origin.y -= self.baseViewHeight
  276. self.baseView.frame = frame
  277. }
  278. }
  279. }
  280. func hide() {
  281. if self.animate {
  282. var frame = self.baseView.frame
  283. frame.origin.y += self.baseViewHeight
  284. UIView.animate(withDuration: 0.2, animations: {
  285. self.backgroundColor = UIColor.previewBgColor.withAlphaComponent(0)
  286. self.baseView.frame = frame
  287. }) { (_) in
  288. self.isHidden = true
  289. self.removeFromSuperview()
  290. }
  291. } else {
  292. self.isHidden = true
  293. self.removeFromSuperview()
  294. }
  295. if let temp = self.senderTabBarIsHidden {
  296. self.sender?.tabBarController?.tabBar.isHidden = temp
  297. }
  298. }
  299. func showNoAuthorityAlert() {
  300. let alert = UIAlertController(title: nil, message: String(format: localLanguageTextValue(.noPhotoLibratyAuthority), getAppName()), preferredStyle: .alert)
  301. let action = UIAlertAction(title: localLanguageTextValue(.ok), style: .default) { (_) in
  302. ZLPhotoConfiguration.default().noAuthorityCallback?(.library)
  303. }
  304. alert.addAction(action)
  305. self.sender?.showDetailViewController(alert, sender: nil)
  306. }
  307. @objc func tapAction(_ tap: UITapGestureRecognizer) {
  308. self.cancelBlock?()
  309. self.hide()
  310. }
  311. @objc func cameraBtnClick() {
  312. let config = ZLPhotoConfiguration.default()
  313. if config.useCustomCamera {
  314. let camera = ZLCustomCamera()
  315. camera.takeDoneBlock = { [weak self] (image, videoUrl) in
  316. self?.save(image: image, videoUrl: videoUrl)
  317. }
  318. self.sender?.showDetailViewController(camera, sender: nil)
  319. } else {
  320. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  321. let picker = UIImagePickerController()
  322. picker.delegate = self
  323. picker.allowsEditing = false
  324. picker.videoQuality = .typeHigh
  325. picker.sourceType = .camera
  326. picker.cameraFlashMode = config.cameraFlashMode.imagePickerFlashMode
  327. var mediaTypes = [String]()
  328. if config.allowTakePhoto {
  329. mediaTypes.append("public.image")
  330. }
  331. if config.allowRecordVideo {
  332. mediaTypes.append("public.movie")
  333. }
  334. picker.mediaTypes = mediaTypes
  335. picker.videoMaximumDuration = TimeInterval(config.maxRecordDuration)
  336. self.sender?.showDetailViewController(picker, sender: nil)
  337. } else {
  338. showAlertView(localLanguageTextValue(.cameraUnavailable), self.sender)
  339. }
  340. }
  341. }
  342. @objc func photoLibraryBtnClick() {
  343. PHPhotoLibrary.shared().unregisterChangeObserver(self)
  344. self.animate = false
  345. self.showThumbnailViewController()
  346. }
  347. @objc func cancelBtnClick() {
  348. guard !self.arrSelectedModels.isEmpty else {
  349. self.cancelBlock?()
  350. self.hide()
  351. return
  352. }
  353. self.requestSelectPhoto()
  354. }
  355. @objc func panSelectAction(_ pan: UIPanGestureRecognizer) {
  356. let point = pan.location(in: self.collectionView)
  357. if pan.state == .began {
  358. let cp = self.baseView.convert(point, from: self.collectionView)
  359. guard self.collectionView.frame.contains(cp) else {
  360. self.panBeginPoint = .zero
  361. return
  362. }
  363. self.panBeginPoint = point
  364. } else if pan.state == .changed {
  365. guard self.panBeginPoint != .zero else {
  366. return
  367. }
  368. guard let indexPath = self.collectionView.indexPathForItem(at: self.panBeginPoint) else {
  369. return
  370. }
  371. if self.panImageView == nil {
  372. guard point.y < self.panBeginPoint.y else {
  373. return
  374. }
  375. guard let cell = self.collectionView.cellForItem(at: indexPath) as? ZLThumbnailPhotoCell else {
  376. return
  377. }
  378. self.panModel = self.arrDataSources[indexPath.row]
  379. self.panCell = cell
  380. self.panImageView = UIImageView(frame: cell.bounds)
  381. self.panImageView?.contentMode = .scaleAspectFill
  382. self.panImageView?.clipsToBounds = true
  383. self.panImageView?.image = cell.imageView.image
  384. cell.imageView.image = nil
  385. self.addSubview(self.panImageView!)
  386. }
  387. self.panImageView?.center = self.convert(point, from: self.collectionView)
  388. } else if pan.state == .cancelled || pan.state == .ended {
  389. guard let pv = self.panImageView else {
  390. return
  391. }
  392. let pvRect = self.baseView.convert(pv.frame, from: self)
  393. var callBack = false
  394. if pvRect.midY < -10 {
  395. self.arrSelectedModels.removeAll()
  396. self.arrSelectedModels.append(self.panModel!)
  397. self.requestSelectPhoto()
  398. callBack = true
  399. }
  400. self.panModel = nil
  401. if !callBack {
  402. let toRect = self.convert(self.panCell?.frame ?? .zero, from: self.collectionView)
  403. UIView.animate(withDuration: 0.25, animations: {
  404. self.panImageView?.frame = toRect
  405. }) { (_) in
  406. self.panCell?.imageView.image = self.panImageView?.image
  407. self.panCell = nil
  408. self.panImageView?.removeFromSuperview()
  409. self.panImageView = nil
  410. }
  411. } else {
  412. self.panCell?.imageView.image = self.panImageView?.image
  413. self.panImageView?.removeFromSuperview()
  414. self.panImageView = nil
  415. self.panCell = nil
  416. }
  417. }
  418. }
  419. func requestSelectPhoto(viewController: UIViewController? = nil) {
  420. guard !self.arrSelectedModels.isEmpty else {
  421. self.selectImageBlock?([], [], self.isSelectOriginal)
  422. self.hide()
  423. viewController?.dismiss(animated: true, completion: nil)
  424. return
  425. }
  426. let config = ZLPhotoConfiguration.default()
  427. if config.allowMixSelect {
  428. let videoCount = self.arrSelectedModels.filter { $0.type == .video }.count
  429. if videoCount > config.maxVideoSelectCount {
  430. showAlertView(String(format: localLanguageTextValue(.exceededMaxVideoSelectCount), ZLPhotoConfiguration.default().maxVideoSelectCount), viewController)
  431. return
  432. } else if videoCount < config.minVideoSelectCount {
  433. showAlertView(String(format: localLanguageTextValue(.lessThanMinVideoSelectCount), ZLPhotoConfiguration.default().minVideoSelectCount), viewController)
  434. return
  435. }
  436. }
  437. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  438. var timeout = false
  439. hud.timeoutBlock = { [weak self] in
  440. timeout = true
  441. showAlertView(localLanguageTextValue(.timeout), viewController ?? self?.sender)
  442. self?.fetchImageQueue.cancelAllOperations()
  443. }
  444. hud.show(timeout: ZLPhotoConfiguration.default().timeout)
  445. guard ZLPhotoConfiguration.default().shouldAnialysisAsset else {
  446. hud.hide()
  447. self.selectImageBlock?([], self.arrSelectedModels.map { $0.asset }, self.isSelectOriginal)
  448. self.arrSelectedModels.removeAll()
  449. self.hide()
  450. viewController?.dismiss(animated: true, completion: nil)
  451. return
  452. }
  453. var images: [UIImage?] = Array(repeating: nil, count: self.arrSelectedModels.count)
  454. var assets: [PHAsset?] = Array(repeating: nil, count: self.arrSelectedModels.count)
  455. var errorAssets: [PHAsset] = []
  456. var errorIndexs: [Int] = []
  457. var sucCount = 0
  458. let totalCount = self.arrSelectedModels.count
  459. for (i, m) in self.arrSelectedModels.enumerated() {
  460. let operation = ZLFetchImageOperation(model: m, isOriginal: self.isSelectOriginal) { [weak self] (image, asset) in
  461. guard !timeout else { return }
  462. sucCount += 1
  463. if let image = image {
  464. images[i] = image
  465. assets[i] = asset ?? m.asset
  466. zl_debugPrint("ZLPhotoBrowser: suc request \(i)")
  467. } else {
  468. errorAssets.append(m.asset)
  469. errorIndexs.append(i)
  470. zl_debugPrint("ZLPhotoBrowser: failed request \(i)")
  471. }
  472. guard sucCount >= totalCount else { return }
  473. let sucImages = images.compactMap { $0 }
  474. let sucAssets = assets.compactMap { $0 }
  475. hud.hide()
  476. self?.selectImageBlock?(sucImages, sucAssets, self?.isSelectOriginal ?? false)
  477. self?.arrSelectedModels.removeAll()
  478. if !errorAssets.isEmpty {
  479. self?.selectImageRequestErrorBlock?(errorAssets, errorIndexs)
  480. }
  481. self?.arrDataSources.removeAll()
  482. self?.hide()
  483. viewController?.dismiss(animated: true, completion: nil)
  484. }
  485. self.fetchImageQueue.addOperation(operation)
  486. }
  487. }
  488. func showThumbnailViewController() {
  489. ZLPhotoManager.getCameraRollAlbum(allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage, allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo) { [weak self] (cameraRoll) in
  490. guard let `self` = self else { return }
  491. let nav: ZLImageNavController
  492. if ZLPhotoConfiguration.default().style == .embedAlbumList {
  493. let tvc = ZLThumbnailViewController(albumList: cameraRoll)
  494. nav = self.getImageNav(rootViewController: tvc)
  495. } else {
  496. nav = self.getImageNav(rootViewController: ZLAlbumListController())
  497. let tvc = ZLThumbnailViewController(albumList: cameraRoll)
  498. nav.pushViewController(tvc, animated: true)
  499. }
  500. self.sender?.showDetailViewController(nav, sender: nil)
  501. }
  502. }
  503. func showPreviewController(_ models: [ZLPhotoModel], index: Int) {
  504. let vc = ZLPhotoPreviewController(photos: models, index: index)
  505. let nav = self.getImageNav(rootViewController: vc)
  506. vc.backBlock = { [weak self, weak nav] in
  507. guard let `self` = self else { return }
  508. self.isSelectOriginal = nav?.isSelectedOriginal ?? false
  509. self.arrSelectedModels.removeAll()
  510. self.arrSelectedModels.append(contentsOf: nav?.arrSelectedModels ?? [])
  511. markSelected(source: &self.arrDataSources, selected: &self.arrSelectedModels)
  512. self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
  513. self.changeCancelBtnTitle()
  514. }
  515. self.sender?.showDetailViewController(nav, sender: nil)
  516. }
  517. func showEditImageVC(model: ZLPhotoModel) {
  518. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  519. hud.show()
  520. ZLPhotoManager.fetchImage(for: model.asset, size: model.previewSize) { [weak self] (image, isDegraded) in
  521. if !isDegraded {
  522. if let image = image {
  523. ZLEditImageViewController.showEditImageVC(parentVC: self?.sender, image: image, editModel: model.editImageModel) { [weak self] (ei, editImageModel) in
  524. model.isSelected = true
  525. model.editImage = ei
  526. model.editImageModel = editImageModel
  527. self?.arrSelectedModels.append(model)
  528. self?.requestSelectPhoto()
  529. }
  530. } else {
  531. showAlertView(localLanguageTextValue(.imageLoadFailed), self?.sender)
  532. }
  533. hud.hide()
  534. }
  535. }
  536. }
  537. func showEditVideoVC(model: ZLPhotoModel) {
  538. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  539. var requestAvAssetID: PHImageRequestID?
  540. hud.show(timeout: 20)
  541. hud.timeoutBlock = { [weak self] in
  542. showAlertView(localLanguageTextValue(.timeout), self?.sender)
  543. if let _ = requestAvAssetID {
  544. PHImageManager.default().cancelImageRequest(requestAvAssetID!)
  545. }
  546. }
  547. func inner_showEditVideoVC(_ avAsset: AVAsset) {
  548. let vc = ZLEditVideoViewController(avAsset: avAsset)
  549. vc.editFinishBlock = { [weak self] (url) in
  550. if let u = url {
  551. ZLPhotoManager.saveVideoToAlbum(url: u) { [weak self] (suc, asset) in
  552. if suc, asset != nil {
  553. let m = ZLPhotoModel(asset: asset!)
  554. m.isSelected = true
  555. self?.arrSelectedModels.removeAll()
  556. self?.arrSelectedModels.append(m)
  557. self?.requestSelectPhoto()
  558. } else {
  559. showAlertView(localLanguageTextValue(.saveVideoError), self?.sender)
  560. }
  561. }
  562. } else {
  563. self?.arrSelectedModels.removeAll()
  564. self?.arrSelectedModels.append(model)
  565. self?.requestSelectPhoto()
  566. }
  567. }
  568. vc.modalPresentationStyle = .fullScreen
  569. self.sender?.showDetailViewController(vc, sender: nil)
  570. }
  571. // 提前fetch一下 avasset
  572. requestAvAssetID = ZLPhotoManager.fetchAVAsset(forVideo: model.asset) { [weak self] (avAsset, _) in
  573. hud.hide()
  574. if let _ = avAsset {
  575. inner_showEditVideoVC(avAsset!)
  576. } else {
  577. showAlertView(localLanguageTextValue(.timeout), self?.sender)
  578. }
  579. }
  580. }
  581. func getImageNav(rootViewController: UIViewController) -> ZLImageNavController {
  582. let nav = ZLImageNavController(rootViewController: rootViewController)
  583. nav.modalPresentationStyle = .fullScreen
  584. nav.selectImageBlock = { [weak self, weak nav] in
  585. self?.isSelectOriginal = nav?.isSelectedOriginal ?? false
  586. self?.arrSelectedModels.removeAll()
  587. self?.arrSelectedModels.append(contentsOf: nav?.arrSelectedModels ?? [])
  588. self?.requestSelectPhoto(viewController: nav)
  589. }
  590. nav.cancelBlock = { [weak self] in
  591. self?.cancelBlock?()
  592. self?.hide()
  593. }
  594. nav.isSelectedOriginal = self.isSelectOriginal
  595. nav.arrSelectedModels.removeAll()
  596. nav.arrSelectedModels.append(contentsOf: self.arrSelectedModels)
  597. return nav
  598. }
  599. func save(image: UIImage?, videoUrl: URL?) {
  600. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  601. if let image = image {
  602. hud.show()
  603. ZLPhotoManager.saveImageToAlbum(image: image) { [weak self] (suc, asset) in
  604. if suc, let at = asset {
  605. let model = ZLPhotoModel(asset: at)
  606. self?.handleDataArray(newModel: model)
  607. } else {
  608. showAlertView(localLanguageTextValue(.saveImageError), self?.sender)
  609. }
  610. hud.hide()
  611. }
  612. } else if let videoUrl = videoUrl {
  613. hud.show()
  614. ZLPhotoManager.saveVideoToAlbum(url: videoUrl) { [weak self] (suc, asset) in
  615. if suc, let at = asset {
  616. let model = ZLPhotoModel(asset: at)
  617. self?.handleDataArray(newModel: model)
  618. } else {
  619. showAlertView(localLanguageTextValue(.saveVideoError), self?.sender)
  620. }
  621. hud.hide()
  622. }
  623. }
  624. }
  625. func handleDataArray(newModel: ZLPhotoModel) {
  626. self.arrDataSources.insert(newModel, at: 0)
  627. var canSelect = true
  628. // If mixed selection is not allowed, and the newModel type is video, it will not be selected.
  629. if !ZLPhotoConfiguration.default().allowMixSelect, newModel.type == .video {
  630. canSelect = false
  631. }
  632. if canSelect, canAddModel(newModel, currentSelectCount: self.arrSelectedModels.count, sender: self.sender, showAlert: false) {
  633. if !self.shouldDirectEdit(newModel) {
  634. newModel.isSelected = true
  635. self.arrSelectedModels.append(newModel)
  636. }
  637. }
  638. let insertIndexPath = IndexPath(row: 0, section: 0)
  639. self.collectionView.performBatchUpdates({
  640. self.collectionView.insertItems(at: [insertIndexPath])
  641. }) { (_) in
  642. self.collectionView.scrollToItem(at: insertIndexPath, at: .centeredHorizontally, animated: true)
  643. self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
  644. }
  645. self.changeCancelBtnTitle()
  646. }
  647. }
  648. extension ZLPhotoPreviewSheet: UIGestureRecognizerDelegate {
  649. public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  650. let location = gestureRecognizer.location(in: self)
  651. return !self.baseView.frame.contains(location)
  652. }
  653. }
  654. extension ZLPhotoPreviewSheet: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  655. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  656. let m = self.arrDataSources[indexPath.row]
  657. let w = CGFloat(m.asset.pixelWidth)
  658. let h = CGFloat(m.asset.pixelHeight)
  659. let scale = min(1.7, max(0.5, w / h))
  660. return CGSize(width: collectionView.frame.height * scale, height: collectionView.frame.height)
  661. }
  662. public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  663. self.placeholderLabel.isHidden = self.arrSelectedModels.isEmpty
  664. return self.arrDataSources.count
  665. }
  666. public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  667. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLThumbnailPhotoCell.zl_identifier(), for: indexPath) as! ZLThumbnailPhotoCell
  668. let model = self.arrDataSources[indexPath.row]
  669. cell.selectedBlock = { [weak self, weak cell] (isSelected) in
  670. guard let `self` = self else { return }
  671. if !isSelected {
  672. guard canAddModel(model, currentSelectCount: self.arrSelectedModels.count, sender: self.sender) else {
  673. return
  674. }
  675. if !self.shouldDirectEdit(model) {
  676. model.isSelected = true
  677. self.arrSelectedModels.append(model)
  678. cell?.btnSelect.isSelected = true
  679. self.refreshCellIndex()
  680. }
  681. } else {
  682. cell?.btnSelect.isSelected = false
  683. model.isSelected = false
  684. self.arrSelectedModels.removeAll { $0 == model }
  685. self.refreshCellIndex()
  686. }
  687. self.changeCancelBtnTitle()
  688. }
  689. cell.indexLabel.isHidden = true
  690. if ZLPhotoConfiguration.default().showSelectedIndex {
  691. for (index, selM) in self.arrSelectedModels.enumerated() {
  692. if model == selM {
  693. self.setCellIndex(cell, showIndexLabel: true, index: index + 1)
  694. break
  695. }
  696. }
  697. }
  698. self.setCellMaskView(cell, isSelected: model.isSelected, model: model)
  699. cell.model = model
  700. return cell
  701. }
  702. public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  703. guard let c = cell as? ZLThumbnailPhotoCell else {
  704. return
  705. }
  706. let model = self.arrDataSources[indexPath.row]
  707. self.setCellMaskView(c, isSelected: model.isSelected, model: model)
  708. }
  709. public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  710. guard let cell = collectionView.cellForItem(at: indexPath) as? ZLThumbnailPhotoCell else {
  711. return
  712. }
  713. if !ZLPhotoConfiguration.default().allowPreviewPhotos {
  714. cell.btnSelectClick()
  715. return
  716. }
  717. if !cell.enableSelect, ZLPhotoConfiguration.default().showInvalidMask {
  718. return
  719. }
  720. let model = self.arrDataSources[indexPath.row]
  721. if self.shouldDirectEdit(model) {
  722. return
  723. }
  724. let config = ZLPhotoConfiguration.default()
  725. let hud = ZLProgressHUD(style: config.hudStyle)
  726. hud.show()
  727. ZLPhotoManager.getCameraRollAlbum(allowSelectImage: config.allowSelectImage, allowSelectVideo: config.allowSelectVideo) { [weak self] (cameraRoll) in
  728. guard let `self` = self else {
  729. hud.hide()
  730. return
  731. }
  732. var totalPhotos = ZLPhotoManager.fetchPhoto(in: cameraRoll.result, ascending: config.sortAscending, allowSelectImage: config.allowSelectImage, allowSelectVideo: config.allowSelectVideo)
  733. markSelected(source: &totalPhotos, selected: &self.arrSelectedModels)
  734. let defaultIndex = config.sortAscending ? totalPhotos.count - 1 : 0
  735. var index: Int?
  736. // last和first效果一样,只是排序方式不同时候分别从前后开始查找可以更快命中
  737. if config.sortAscending {
  738. index = totalPhotos.lastIndex { $0 == model }
  739. } else {
  740. index = totalPhotos.firstIndex { $0 == model }
  741. }
  742. hud.hide()
  743. self.showPreviewController(totalPhotos, index: index ?? defaultIndex)
  744. }
  745. }
  746. func shouldDirectEdit(_ model: ZLPhotoModel) -> Bool {
  747. let config = ZLPhotoConfiguration.default()
  748. let canEditImage = config.editAfterSelectThumbnailImage &&
  749. config.allowEditImage &&
  750. config.maxSelectCount == 1 &&
  751. model.type.rawValue < ZLPhotoModel.MediaType.video.rawValue
  752. let canEditVideo = (config.editAfterSelectThumbnailImage &&
  753. config.allowEditVideo &&
  754. model.type == .video &&
  755. config.maxSelectCount == 1) ||
  756. (config.allowEditVideo &&
  757. model.type == .video &&
  758. !config.allowMixSelect &&
  759. config.cropVideoAfterSelectThumbnail)
  760. //当前未选择图片 或已经选择了一张并且点击的是已选择的图片
  761. let flag = self.arrSelectedModels.isEmpty || (self.arrSelectedModels.count == 1 && self.arrSelectedModels.first?.ident == model.ident)
  762. if canEditImage, flag {
  763. self.showEditImageVC(model: model)
  764. } else if canEditVideo, flag {
  765. self.showEditVideoVC(model: model)
  766. }
  767. return flag && (canEditImage || canEditVideo)
  768. }
  769. func setCellIndex(_ cell: ZLThumbnailPhotoCell?, showIndexLabel: Bool, index: Int) {
  770. guard ZLPhotoConfiguration.default().showSelectedIndex else {
  771. return
  772. }
  773. cell?.index = index
  774. cell?.indexLabel.isHidden = !showIndexLabel
  775. }
  776. func refreshCellIndex() {
  777. let showIndex = ZLPhotoConfiguration.default().showSelectedIndex
  778. let showMask = ZLPhotoConfiguration.default().showSelectedMask || ZLPhotoConfiguration.default().showInvalidMask
  779. guard showIndex || showMask else {
  780. return
  781. }
  782. let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems
  783. visibleIndexPaths.forEach { (indexPath) in
  784. guard let cell = self.collectionView.cellForItem(at: indexPath) as? ZLThumbnailPhotoCell else {
  785. return
  786. }
  787. let m = self.arrDataSources[indexPath.row]
  788. var show = false
  789. var idx = 0
  790. var isSelected = false
  791. for (index, selM) in self.arrSelectedModels.enumerated() {
  792. if m == selM {
  793. show = true
  794. idx = index + 1
  795. isSelected = true
  796. break
  797. }
  798. }
  799. if showIndex {
  800. self.setCellIndex(cell, showIndexLabel: show, index: idx)
  801. }
  802. if showMask {
  803. self.setCellMaskView(cell, isSelected: isSelected, model: m)
  804. }
  805. }
  806. }
  807. func setCellMaskView(_ cell: ZLThumbnailPhotoCell, isSelected: Bool, model: ZLPhotoModel) {
  808. cell.coverView.isHidden = true
  809. cell.enableSelect = true
  810. let config = ZLPhotoConfiguration.default()
  811. if isSelected {
  812. cell.coverView.backgroundColor = .selectedMaskColor
  813. cell.coverView.isHidden = !config.showSelectedMask
  814. if config.showSelectedBorder {
  815. cell.layer.borderWidth = 4
  816. }
  817. } else {
  818. let selCount = self.arrSelectedModels.count
  819. if selCount < config.maxSelectCount {
  820. if config.allowMixSelect {
  821. let videoCount = self.arrSelectedModels.filter { $0.type == .video }.count
  822. if videoCount >= config.maxVideoSelectCount, model.type == .video {
  823. cell.coverView.backgroundColor = .invalidMaskColor
  824. cell.coverView.isHidden = !config.showInvalidMask
  825. cell.enableSelect = false
  826. } else if (config.maxSelectCount - selCount) <= (config.minVideoSelectCount - videoCount), model.type != .video {
  827. cell.coverView.backgroundColor = .invalidMaskColor
  828. cell.coverView.isHidden = !config.showInvalidMask
  829. cell.enableSelect = false
  830. }
  831. } else if selCount > 0 {
  832. cell.coverView.backgroundColor = .invalidMaskColor
  833. cell.coverView.isHidden = (!config.showInvalidMask || model.type != .video)
  834. cell.enableSelect = model.type != .video
  835. }
  836. } else if selCount >= config.maxSelectCount {
  837. cell.coverView.backgroundColor = .invalidMaskColor
  838. cell.coverView.isHidden = !config.showInvalidMask
  839. cell.enableSelect = false
  840. }
  841. if config.showSelectedBorder {
  842. cell.layer.borderWidth = 0
  843. }
  844. }
  845. }
  846. func changeCancelBtnTitle() {
  847. if self.arrSelectedModels.count > 0 {
  848. self.cancelBtn.setTitle(String(format: "%@(%ld)", localLanguageTextValue(.done), self.arrSelectedModels.count), for: .normal)
  849. self.cancelBtn.setTitleColor(.previewBtnHighlightTitleColor, for: .normal)
  850. } else {
  851. self.cancelBtn.setTitle(localLanguageTextValue(.cancel), for: .normal)
  852. self.cancelBtn.setTitleColor(.previewBtnTitleColor, for: .normal)
  853. }
  854. }
  855. }
  856. extension ZLPhotoPreviewSheet: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  857. public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  858. picker.dismiss(animated: true) {
  859. let image = info[.originalImage] as? UIImage
  860. let url = info[.mediaURL] as? URL
  861. self.save(image: image, videoUrl: url)
  862. }
  863. }
  864. }
  865. extension ZLPhotoPreviewSheet: PHPhotoLibraryChangeObserver {
  866. public func photoLibraryDidChange(_ changeInstance: PHChange) {
  867. PHPhotoLibrary.shared().unregisterChangeObserver(self)
  868. DispatchQueue.main.async {
  869. self.loadPhotos()
  870. }
  871. }
  872. }