ZLThumbnailViewController.swift 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480
  1. //
  2. // ZLThumbnailViewController.swift
  3. // ZLPhotoBrowser
  4. //
  5. // Created by long on 2020/8/19.
  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. extension ZLThumbnailViewController {
  29. enum SlideSelectType {
  30. case none
  31. case select
  32. case cancel
  33. }
  34. }
  35. class ZLThumbnailViewController: UIViewController {
  36. var albumList: ZLAlbumListModel
  37. var externalNavView: ZLExternalAlbumListNavView?
  38. var embedNavView: ZLEmbedAlbumListNavView?
  39. var embedAlbumListView: ZLEmbedAlbumListView?
  40. var collectionView: UICollectionView!
  41. var bottomView: UIView!
  42. var bottomBlurView: UIVisualEffectView?
  43. var limitAuthTipsView: ZLLimitedAuthorityTipsView?
  44. var previewBtn: UIButton!
  45. var originalBtn: UIButton!
  46. var doneBtn: UIButton!
  47. var arrDataSources: [ZLPhotoModel] = []
  48. var showCameraCell: Bool {
  49. if ZLPhotoConfiguration.default().allowTakePhotoInLibrary && self.albumList.isCameraRoll {
  50. return true
  51. }
  52. return false
  53. }
  54. /// 所有滑动经过的indexPath
  55. lazy var arrSlideIndexPaths: [IndexPath] = []
  56. /// 所有滑动经过的indexPath的初始选择状态
  57. lazy var dicOriSelectStatus: [IndexPath: Bool] = [:]
  58. var isLayoutOK = false
  59. /// 设备旋转前第一个可视indexPath
  60. var firstVisibleIndexPathBeforeRotation: IndexPath?
  61. /// 是否触发了横竖屏切换
  62. var isSwitchOrientation = false
  63. /// 是否开始出发滑动选择
  64. var beginPanSelect = false
  65. /// 滑动选择 或 取消
  66. /// 当初始滑动的cell处于未选择状态,则开始选择,反之,则开始取消选择
  67. var panSelectType: ZLThumbnailViewController.SlideSelectType = .none
  68. /// 开始滑动的indexPath
  69. var beginSlideIndexPath: IndexPath?
  70. /// 最后滑动经过的index,开始的indexPath不计入
  71. /// 优化拖动手势计算,避免单个cell中冗余计算多次
  72. var lastSlideIndex: Int?
  73. /// 预览所选择图片,手势返回时候不调用scrollToIndex
  74. var isPreviewPush = false
  75. /// 拍照后置为true,需要刷新相册列表
  76. var hasTakeANewAsset = false
  77. var slideCalculateQueue = DispatchQueue(label: "com.ZLhotoBrowser.slide")
  78. var autoScrollTimer: CADisplayLink?
  79. var lastPanUpdateTime = CACurrentMediaTime()
  80. let showLimitAuthTipsView: Bool = {
  81. if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited, ZLPhotoConfiguration.default().showEnterSettingTips {
  82. return true
  83. } else {
  84. return false
  85. }
  86. }()
  87. private enum AutoScrollDirection {
  88. case none
  89. case top
  90. case bottom
  91. }
  92. private var autoScrollInfo: (direction: AutoScrollDirection, speed: CGFloat) = (.none, 0)
  93. @available(iOS 14, *)
  94. var showAddPhotoCell: Bool {
  95. PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited && ZLPhotoConfiguration.default().showAddPhotoButton && self.albumList.isCameraRoll
  96. }
  97. /// 照相按钮+添加图片按钮的数量
  98. /// the count of addPhotoButton & cameraButton
  99. private var offset: Int {
  100. if #available(iOS 14, *) {
  101. return Int(self.showAddPhotoCell) + Int(self.showCameraCell)
  102. } else {
  103. return Int(self.showCameraCell)
  104. }
  105. }
  106. override var prefersStatusBarHidden: Bool {
  107. return false
  108. }
  109. override var preferredStatusBarStyle: UIStatusBarStyle {
  110. return ZLPhotoConfiguration.default().statusBarStyle
  111. }
  112. var panGes: UIPanGestureRecognizer!
  113. deinit {
  114. zl_debugPrint("ZLThumbnailViewController deinit")
  115. self.cleanTimer()
  116. }
  117. init(albumList: ZLAlbumListModel) {
  118. self.albumList = albumList
  119. super.init(nibName: nil, bundle: nil)
  120. }
  121. required init?(coder: NSCoder) {
  122. fatalError("init(coder:) has not been implemented")
  123. }
  124. override func viewDidLoad() {
  125. super.viewDidLoad()
  126. self.setupUI()
  127. if ZLPhotoConfiguration.default().allowSlideSelect {
  128. self.panGes = UIPanGestureRecognizer(target: self, action: #selector(slideSelectAction(_:)))
  129. self.panGes.delegate = self
  130. self.view.addGestureRecognizer(self.panGes)
  131. }
  132. NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationChanged(_:)), name: UIApplication.willChangeStatusBarOrientationNotification, object: nil)
  133. self.loadPhotos()
  134. // 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.
  135. if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited {
  136. PHPhotoLibrary.shared().register(self)
  137. }
  138. }
  139. override func viewWillAppear(_ animated: Bool) {
  140. super.viewWillAppear(animated)
  141. self.navigationController?.navigationBar.isHidden = true
  142. self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
  143. self.resetBottomToolBtnStatus()
  144. }
  145. override func viewDidAppear(_ animated: Bool) {
  146. super.viewDidAppear(animated)
  147. self.isLayoutOK = true
  148. self.isPreviewPush = false
  149. }
  150. override func viewDidLayoutSubviews() {
  151. super.viewDidLayoutSubviews()
  152. let navViewNormalH: CGFloat = 44
  153. var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
  154. var collectionViewInsetTop: CGFloat = 20
  155. if #available(iOS 11.0, *) {
  156. insets = self.view.safeAreaInsets
  157. collectionViewInsetTop = navViewNormalH
  158. } else {
  159. collectionViewInsetTop += navViewNormalH
  160. }
  161. let navViewFrame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: insets.top + navViewNormalH)
  162. self.externalNavView?.frame = navViewFrame
  163. self.embedNavView?.frame = navViewFrame
  164. self.embedAlbumListView?.frame = CGRect(x: 0, y: navViewFrame.maxY, width: self.view.bounds.width, height: self.view.bounds.height-navViewFrame.maxY)
  165. var showBottomToolBtns = true
  166. let config = ZLPhotoConfiguration.default()
  167. let condition1 = config.editAfterSelectThumbnailImage &&
  168. config.maxSelectCount == 1 &&
  169. (config.allowEditImage || config.allowEditVideo)
  170. let condition2 = config.allowPreviewPhotos && config.maxSelectCount == 1 && !config.showSelectBtnWhenSingleSelect
  171. if condition1 || condition2 {
  172. showBottomToolBtns = false
  173. }
  174. let bottomViewH: CGFloat
  175. if self.showLimitAuthTipsView, showBottomToolBtns {
  176. bottomViewH = ZLLayout.bottomToolViewH + ZLLimitedAuthorityTipsView.height
  177. } else if self.showLimitAuthTipsView {
  178. bottomViewH = ZLLimitedAuthorityTipsView.height
  179. } else if showBottomToolBtns {
  180. bottomViewH = ZLLayout.bottomToolViewH
  181. } else {
  182. bottomViewH = 0
  183. }
  184. let totalWidth = self.view.frame.width - insets.left - insets.right
  185. self.collectionView.frame = CGRect(x: insets.left, y: 0, width: totalWidth, height: self.view.frame.height)
  186. self.collectionView.contentInset = UIEdgeInsets(top: collectionViewInsetTop, left: 0, bottom: bottomViewH, right: 0)
  187. self.collectionView.scrollIndicatorInsets = UIEdgeInsets(top: insets.top, left: 0, bottom: bottomViewH, right: 0)
  188. if !self.isLayoutOK {
  189. self.scrollToBottom()
  190. } else if self.isSwitchOrientation {
  191. self.isSwitchOrientation = false
  192. if let ip = self.firstVisibleIndexPathBeforeRotation {
  193. self.collectionView.scrollToItem(at: ip, at: .top, animated: false)
  194. }
  195. }
  196. guard showBottomToolBtns || self.showLimitAuthTipsView else { return }
  197. let btnH = ZLLayout.bottomToolBtnH
  198. self.bottomView.frame = CGRect(x: 0, y: self.view.frame.height-insets.bottom-bottomViewH, width: self.view.bounds.width, height: bottomViewH+insets.bottom)
  199. self.bottomBlurView?.frame = self.bottomView.bounds
  200. if self.showLimitAuthTipsView {
  201. self.limitAuthTipsView?.frame = CGRect(x: 0, y: 0, width: self.bottomView.bounds.width, height: ZLLimitedAuthorityTipsView.height)
  202. }
  203. if showBottomToolBtns {
  204. let btnY = self.showLimitAuthTipsView ? ZLLimitedAuthorityTipsView.height + ZLLayout.bottomToolBtnY : ZLLayout.bottomToolBtnY
  205. let previewTitle = localLanguageTextValue(.preview)
  206. let previewBtnW = previewTitle.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)).width
  207. self.previewBtn.frame = CGRect(x: 15, y: btnY, width: previewBtnW, height: btnH)
  208. let originalTitle = localLanguageTextValue(.originalPhoto)
  209. let originBtnW = originalTitle.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)).width + 30
  210. self.originalBtn.frame = CGRect(x: (self.bottomView.bounds.width-originBtnW)/2-5, y: btnY, width: originBtnW, height: btnH)
  211. self.refreshDoneBtnFrame()
  212. }
  213. }
  214. func setupUI() {
  215. self.automaticallyAdjustsScrollViewInsets = true
  216. self.edgesForExtendedLayout = .all
  217. self.view.backgroundColor = .thumbnailBgColor
  218. let layout = UICollectionViewFlowLayout()
  219. layout.sectionInset = UIEdgeInsets(top: 3, left: 0, bottom: 3, right: 0)
  220. self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
  221. self.collectionView.backgroundColor = .thumbnailBgColor
  222. self.collectionView.dataSource = self
  223. self.collectionView.delegate = self
  224. self.collectionView.alwaysBounceVertical = true
  225. if #available(iOS 11.0, *) {
  226. self.collectionView.contentInsetAdjustmentBehavior = .always
  227. }
  228. self.view.addSubview(self.collectionView)
  229. ZLCameraCell.zl_register(self.collectionView)
  230. ZLThumbnailPhotoCell.zl_register(self.collectionView)
  231. ZLAddPhotoCell.zl_register(self.collectionView)
  232. self.bottomView = UIView()
  233. self.bottomView.backgroundColor = .bottomToolViewBgColor
  234. self.view.addSubview(self.bottomView)
  235. if let effect = ZLPhotoConfiguration.default().bottomToolViewBlurEffect {
  236. self.bottomBlurView = UIVisualEffectView(effect: effect)
  237. self.bottomView.addSubview(self.bottomBlurView!)
  238. }
  239. if self.showLimitAuthTipsView {
  240. self.limitAuthTipsView = ZLLimitedAuthorityTipsView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: ZLLimitedAuthorityTipsView.height))
  241. self.bottomView.addSubview(self.limitAuthTipsView!)
  242. }
  243. func createBtn(_ title: String, _ action: Selector) -> UIButton {
  244. let btn = UIButton(type: .custom)
  245. btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
  246. btn.setTitle(title, for: .normal)
  247. btn.setTitleColor(.bottomToolViewBtnNormalTitleColor, for: .normal)
  248. btn.setTitleColor(.bottomToolViewBtnDisableTitleColor, for: .disabled)
  249. btn.addTarget(self, action: action, for: .touchUpInside)
  250. return btn
  251. }
  252. self.previewBtn = createBtn(localLanguageTextValue(.preview), #selector(previewBtnClick))
  253. self.previewBtn.isHidden = !ZLPhotoConfiguration.default().showPreviewButtonInAlbum
  254. self.bottomView.addSubview(self.previewBtn)
  255. self.originalBtn = createBtn(localLanguageTextValue(.originalPhoto), #selector(originalPhotoClick))
  256. self.originalBtn.setImage(getImage("zl_btn_original_circle"), for: .normal)
  257. self.originalBtn.setImage(getImage("zl_btn_original_selected"), for: .selected)
  258. self.originalBtn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
  259. self.originalBtn.isHidden = !(ZLPhotoConfiguration.default().allowSelectOriginal && ZLPhotoConfiguration.default().allowSelectImage)
  260. self.originalBtn.isSelected = (self.navigationController as! ZLImageNavController).isSelectedOriginal
  261. self.bottomView.addSubview(self.originalBtn)
  262. self.doneBtn = createBtn(localLanguageTextValue(.done), #selector(doneBtnClick))
  263. self.doneBtn.layer.masksToBounds = true
  264. self.doneBtn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
  265. self.bottomView.addSubview(self.doneBtn)
  266. self.setupNavView()
  267. }
  268. func setupNavView() {
  269. if ZLPhotoConfiguration.default().style == .embedAlbumList {
  270. self.embedNavView = ZLEmbedAlbumListNavView(title: self.albumList.title)
  271. self.embedNavView?.selectAlbumBlock = { [weak self] in
  272. if self?.embedAlbumListView?.isHidden == true {
  273. self?.embedAlbumListView?.show(reloadAlbumList: self?.hasTakeANewAsset ?? false)
  274. self?.hasTakeANewAsset = false
  275. } else {
  276. self?.embedAlbumListView?.hide()
  277. }
  278. }
  279. self.embedNavView?.cancelBlock = { [weak self] in
  280. let nav = self?.navigationController as? ZLImageNavController
  281. nav?.cancelBlock?()
  282. nav?.dismiss(animated: true, completion: nil)
  283. }
  284. self.view.addSubview(self.embedNavView!)
  285. self.embedAlbumListView = ZLEmbedAlbumListView(selectedAlbum: self.albumList)
  286. self.embedAlbumListView?.isHidden = true
  287. self.embedAlbumListView?.selectAlbumBlock = { [weak self] (album) in
  288. guard self?.albumList != album else {
  289. return
  290. }
  291. self?.albumList = album
  292. self?.embedNavView?.title = album.title
  293. self?.loadPhotos()
  294. self?.embedNavView?.reset()
  295. }
  296. self.embedAlbumListView?.hideBlock = { [weak self] in
  297. self?.embedNavView?.reset()
  298. }
  299. self.view.addSubview(self.embedAlbumListView!)
  300. } else if ZLPhotoConfiguration.default().style == .externalAlbumList {
  301. self.externalNavView = ZLExternalAlbumListNavView(title: self.albumList.title)
  302. self.externalNavView?.backBlock = { [weak self] in
  303. self?.navigationController?.popViewController(animated: true)
  304. }
  305. self.externalNavView?.cancelBlock = { [weak self] in
  306. let nav = self?.navigationController as? ZLImageNavController
  307. nav?.cancelBlock?()
  308. nav?.dismiss(animated: true, completion: nil)
  309. }
  310. self.view.addSubview(self.externalNavView!)
  311. }
  312. }
  313. func loadPhotos() {
  314. let nav = self.navigationController as! ZLImageNavController
  315. if self.albumList.models.isEmpty {
  316. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  317. hud.show()
  318. DispatchQueue.global().async {
  319. self.albumList.refetchPhotos()
  320. DispatchQueue.main.async {
  321. self.arrDataSources.removeAll()
  322. self.arrDataSources.append(contentsOf: self.albumList.models)
  323. markSelected(source: &self.arrDataSources, selected: &nav.arrSelectedModels)
  324. hud.hide()
  325. self.collectionView.reloadData()
  326. self.scrollToBottom()
  327. }
  328. }
  329. } else {
  330. self.arrDataSources.removeAll()
  331. self.arrDataSources.append(contentsOf: self.albumList.models)
  332. markSelected(source: &self.arrDataSources, selected: &nav.arrSelectedModels)
  333. self.collectionView.reloadData()
  334. self.scrollToBottom()
  335. }
  336. }
  337. // MARK: btn actions
  338. @objc func previewBtnClick() {
  339. let nav = self.navigationController as! ZLImageNavController
  340. let vc = ZLPhotoPreviewController(photos: nav.arrSelectedModels, index: 0)
  341. self.show(vc, sender: nil)
  342. }
  343. @objc func originalPhotoClick() {
  344. self.originalBtn.isSelected = !self.originalBtn.isSelected
  345. (self.navigationController as? ZLImageNavController)?.isSelectedOriginal = self.originalBtn.isSelected
  346. }
  347. @objc func doneBtnClick() {
  348. let nav = self.navigationController as? ZLImageNavController
  349. nav?.selectImageBlock?()
  350. }
  351. @objc func deviceOrientationChanged(_ notify: Notification) {
  352. let pInView = self.collectionView.convert(CGPoint(x: 100, y: 100), from: self.view)
  353. self.firstVisibleIndexPathBeforeRotation = self.collectionView.indexPathForItem(at: pInView)
  354. self.isSwitchOrientation = true
  355. }
  356. @objc func slideSelectAction(_ pan: UIPanGestureRecognizer) {
  357. let point = pan.location(in: self.collectionView)
  358. guard let indexPath = self.collectionView.indexPathForItem(at: point) else {
  359. return
  360. }
  361. let config = ZLPhotoConfiguration.default()
  362. let nav = self.navigationController as! ZLImageNavController
  363. let cell = self.collectionView.cellForItem(at: indexPath) as? ZLThumbnailPhotoCell
  364. let asc = config.sortAscending
  365. if pan.state == .began {
  366. self.beginPanSelect = (cell != nil)
  367. if self.beginPanSelect {
  368. let index = asc ? indexPath.row : indexPath.row - self.offset
  369. let m = self.arrDataSources[index]
  370. self.panSelectType = m.isSelected ? .cancel : .select
  371. self.beginSlideIndexPath = indexPath
  372. if !m.isSelected, nav.arrSelectedModels.count < config.maxSelectCount, canAddModel(m, currentSelectCount: nav.arrSelectedModels.count, sender: self) {
  373. if self.shouldDirectEdit(m) {
  374. self.panSelectType = .none
  375. return
  376. } else {
  377. m.isSelected = true
  378. nav.arrSelectedModels.append(m)
  379. }
  380. } else if m.isSelected {
  381. m.isSelected = false
  382. nav.arrSelectedModels.removeAll { $0 == m }
  383. }
  384. cell?.btnSelect.isSelected = m.isSelected
  385. self.refreshCellIndexAndMaskView()
  386. self.resetBottomToolBtnStatus()
  387. self.lastSlideIndex = indexPath.row
  388. }
  389. } else if pan.state == .changed {
  390. self.autoScrollWhenSlideSelect(pan)
  391. if !self.beginPanSelect || indexPath.row == self.lastSlideIndex || self.panSelectType == .none || cell == nil {
  392. return
  393. }
  394. guard let beginIndexPath = self.beginSlideIndexPath else {
  395. return
  396. }
  397. self.lastPanUpdateTime = CACurrentMediaTime()
  398. let visiblePaths = self.collectionView.indexPathsForVisibleItems
  399. self.slideCalculateQueue.async {
  400. self.lastSlideIndex = indexPath.row
  401. let minIndex = min(indexPath.row, beginIndexPath.row)
  402. let maxIndex = max(indexPath.row, beginIndexPath.row)
  403. let minIsBegin = minIndex == beginIndexPath.row
  404. var i = beginIndexPath.row
  405. while (minIsBegin ? i <= maxIndex : i >= minIndex) {
  406. if i != beginIndexPath.row {
  407. let p = IndexPath(row: i, section: 0)
  408. if !self.arrSlideIndexPaths.contains(p) {
  409. self.arrSlideIndexPaths.append(p)
  410. let index = asc ? i : i - self.offset
  411. let m = self.arrDataSources[index]
  412. self.dicOriSelectStatus[p] = m.isSelected
  413. }
  414. }
  415. i += (minIsBegin ? 1 : -1)
  416. }
  417. var selectedArrHasChange = false
  418. for path in self.arrSlideIndexPaths {
  419. if !visiblePaths.contains(path) {
  420. continue
  421. }
  422. let index = asc ? path.row : path.row - self.offset
  423. // 是否在最初和现在的间隔区间内
  424. let inSection = path.row >= minIndex && path.row <= maxIndex
  425. let m = self.arrDataSources[index]
  426. if self.panSelectType == .select {
  427. if inSection,
  428. !m.isSelected,
  429. canAddModel(m, currentSelectCount: nav.arrSelectedModels.count, sender: self, showAlert: false) {
  430. m.isSelected = true
  431. }
  432. } else if self.panSelectType == .cancel {
  433. if inSection {
  434. m.isSelected = false
  435. }
  436. }
  437. if !inSection {
  438. // 未在区间内的model还原为初始选择状态
  439. m.isSelected = self.dicOriSelectStatus[path] ?? false
  440. }
  441. if !m.isSelected {
  442. if let index = nav.arrSelectedModels.firstIndex(where: { $0 == m }) {
  443. nav.arrSelectedModels.remove(at: index)
  444. selectedArrHasChange = true
  445. }
  446. } else {
  447. if !nav.arrSelectedModels.contains(where: { $0 == m }) {
  448. nav.arrSelectedModels.append(m)
  449. selectedArrHasChange = true
  450. }
  451. }
  452. DispatchQueue.main.async {
  453. let c = self.collectionView.cellForItem(at: path) as? ZLThumbnailPhotoCell
  454. c?.btnSelect.isSelected = m.isSelected
  455. }
  456. }
  457. if selectedArrHasChange {
  458. DispatchQueue.main.async {
  459. self.refreshCellIndexAndMaskView()
  460. self.resetBottomToolBtnStatus()
  461. }
  462. }
  463. }
  464. } else if pan.state == .ended || pan.state == .cancelled {
  465. self.cleanTimer()
  466. self.panSelectType = .none
  467. self.arrSlideIndexPaths.removeAll()
  468. self.dicOriSelectStatus.removeAll()
  469. self.resetBottomToolBtnStatus()
  470. }
  471. }
  472. func autoScrollWhenSlideSelect(_ pan: UIPanGestureRecognizer) {
  473. guard ZLPhotoConfiguration.default().autoScrollWhenSlideSelectIsActive else {
  474. return
  475. }
  476. let arrSel = (self.navigationController as? ZLImageNavController)?.arrSelectedModels ?? []
  477. guard arrSel.count < ZLPhotoConfiguration.default().maxSelectCount else {
  478. // Stop auto scroll when reach the max select count.
  479. self.cleanTimer()
  480. return
  481. }
  482. let top = ((self.embedNavView?.frame.height ?? self.externalNavView?.frame.height) ?? 44) + 30
  483. let bottom = self.bottomView.frame.minY - 30
  484. let point = pan.location(in: self.view)
  485. var diff: CGFloat = 0
  486. var direction: AutoScrollDirection = .none
  487. if point.y < top {
  488. diff = top - point.y
  489. direction = .top
  490. } else if point.y > bottom {
  491. diff = point.y - bottom
  492. direction = .bottom
  493. } else {
  494. self.autoScrollInfo = (.none, 0)
  495. self.cleanTimer()
  496. return
  497. }
  498. guard diff > 0 else { return }
  499. let s = min(diff, 60) / 60 * ZLPhotoConfiguration.default().autoScrollMaxSpeed
  500. self.autoScrollInfo = (direction, s)
  501. if self.autoScrollTimer == nil {
  502. self.cleanTimer()
  503. self.autoScrollTimer = CADisplayLink(target: ZLWeakProxy(target: self), selector: #selector(autoScrollAction))
  504. self.autoScrollTimer?.add(to: RunLoop.current, forMode: .common)
  505. }
  506. }
  507. func cleanTimer() {
  508. self.autoScrollTimer?.remove(from: RunLoop.current, forMode: .common)
  509. self.autoScrollTimer?.invalidate()
  510. self.autoScrollTimer = nil
  511. }
  512. @objc func autoScrollAction() {
  513. guard self.autoScrollInfo.direction != .none else { return }
  514. let duration = CGFloat(self.autoScrollTimer?.duration ?? 1 / 60)
  515. if CACurrentMediaTime() - self.lastPanUpdateTime > 0.2 {
  516. // Finger may be not moved in slide selection mode
  517. self.slideSelectAction(self.panGes)
  518. }
  519. let distance = self.autoScrollInfo.speed * duration
  520. let offset = self.collectionView.contentOffset
  521. let inset = self.collectionView.contentInset
  522. if self.autoScrollInfo.direction == .top, offset.y + inset.top > distance {
  523. self.collectionView.contentOffset = CGPoint(x: 0, y: offset.y - distance)
  524. } else if self.autoScrollInfo.direction == .bottom, offset.y + self.collectionView.bounds.height + distance - inset.bottom < self.collectionView.contentSize.height {
  525. self.collectionView.contentOffset = CGPoint(x: 0, y: offset.y + distance)
  526. }
  527. }
  528. func resetBottomToolBtnStatus() {
  529. let nav = self.navigationController as! ZLImageNavController
  530. if nav.arrSelectedModels.count > 0 {
  531. self.previewBtn.isEnabled = true
  532. self.doneBtn.isEnabled = true
  533. let doneTitle = localLanguageTextValue(.done) + "(" + String(nav.arrSelectedModels.count) + ")"
  534. self.doneBtn.setTitle(doneTitle, for: .normal)
  535. self.doneBtn.backgroundColor = .bottomToolViewBtnNormalBgColor
  536. } else {
  537. self.previewBtn.isEnabled = false
  538. self.doneBtn.isEnabled = false
  539. self.doneBtn.setTitle(localLanguageTextValue(.done), for: .normal)
  540. self.doneBtn.backgroundColor = .bottomToolViewBtnDisableBgColor
  541. }
  542. self.originalBtn.isSelected = nav.isSelectedOriginal
  543. self.refreshDoneBtnFrame()
  544. }
  545. func refreshDoneBtnFrame() {
  546. let selCount = (self.navigationController as? ZLImageNavController)?.arrSelectedModels.count ?? 0
  547. var doneTitle = localLanguageTextValue(.done)
  548. if selCount > 0 {
  549. doneTitle += "(" + String(selCount) + ")"
  550. }
  551. let doneBtnW = doneTitle.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)).width + 20
  552. let btnY = self.showLimitAuthTipsView ? ZLLimitedAuthorityTipsView.height + ZLLayout.bottomToolBtnY : ZLLayout.bottomToolBtnY
  553. self.doneBtn.frame = CGRect(x: self.bottomView.bounds.width-doneBtnW-15, y: btnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH)
  554. }
  555. func scrollToBottom() {
  556. guard ZLPhotoConfiguration.default().sortAscending, self.arrDataSources.count > 0 else {
  557. return
  558. }
  559. let index = self.arrDataSources.count - 1 + self.offset
  560. self.collectionView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredVertically, animated: false)
  561. }
  562. func showCamera() {
  563. let config = ZLPhotoConfiguration.default()
  564. if config.useCustomCamera {
  565. let camera = ZLCustomCamera()
  566. camera.takeDoneBlock = { [weak self] (image, videoUrl) in
  567. self?.save(image: image, videoUrl: videoUrl)
  568. }
  569. self.showDetailViewController(camera, sender: nil)
  570. } else {
  571. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  572. let picker = UIImagePickerController()
  573. picker.delegate = self
  574. picker.allowsEditing = false
  575. picker.videoQuality = .typeHigh
  576. picker.sourceType = .camera
  577. picker.cameraFlashMode = config.cameraFlashMode.imagePickerFlashMode
  578. var mediaTypes = [String]()
  579. if config.allowTakePhoto {
  580. mediaTypes.append("public.image")
  581. }
  582. if config.allowRecordVideo {
  583. mediaTypes.append("public.movie")
  584. }
  585. picker.mediaTypes = mediaTypes
  586. picker.videoMaximumDuration = TimeInterval(config.maxRecordDuration)
  587. self.showDetailViewController(picker, sender: nil)
  588. } else {
  589. showAlertView(localLanguageTextValue(.cameraUnavailable), self)
  590. }
  591. }
  592. }
  593. func save(image: UIImage?, videoUrl: URL?) {
  594. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  595. if let image = image {
  596. hud.show()
  597. ZLPhotoManager.saveImageToAlbum(image: image) { [weak self] (suc, asset) in
  598. if suc, let at = asset {
  599. let model = ZLPhotoModel(asset: at)
  600. self?.handleDataArray(newModel: model)
  601. } else {
  602. showAlertView(localLanguageTextValue(.saveImageError), self)
  603. }
  604. hud.hide()
  605. }
  606. } else if let videoUrl = videoUrl {
  607. hud.show()
  608. ZLPhotoManager.saveVideoToAlbum(url: videoUrl) { [weak self] (suc, asset) in
  609. if suc, let at = asset {
  610. let model = ZLPhotoModel(asset: at)
  611. self?.handleDataArray(newModel: model)
  612. } else {
  613. showAlertView(localLanguageTextValue(.saveVideoError), self)
  614. }
  615. hud.hide()
  616. }
  617. }
  618. }
  619. func handleDataArray(newModel: ZLPhotoModel) {
  620. self.hasTakeANewAsset = true
  621. self.albumList.refreshResult()
  622. let nav = self.navigationController as? ZLImageNavController
  623. let config = ZLPhotoConfiguration.default()
  624. var insertIndex = 0
  625. if config.sortAscending {
  626. insertIndex = self.arrDataSources.count
  627. self.arrDataSources.append(newModel)
  628. } else {
  629. // 保存拍照的照片或者视频,说明肯定有camera cell
  630. insertIndex = self.offset
  631. self.arrDataSources.insert(newModel, at: 0)
  632. }
  633. var canSelect = true
  634. // If mixed selection is not allowed, and the newModel type is video, it will not be selected.
  635. if !config.allowMixSelect, newModel.type == .video {
  636. canSelect = false
  637. }
  638. if canSelect, canAddModel(newModel, currentSelectCount: nav?.arrSelectedModels.count ?? 0, sender: self, showAlert: false) {
  639. if !self.shouldDirectEdit(newModel) {
  640. newModel.isSelected = true
  641. nav?.arrSelectedModels.append(newModel)
  642. }
  643. }
  644. let insertIndexPath = IndexPath(row: insertIndex, section: 0)
  645. self.collectionView.performBatchUpdates({
  646. self.collectionView.insertItems(at: [insertIndexPath])
  647. }) { (_) in
  648. self.collectionView.scrollToItem(at: insertIndexPath, at: .centeredVertically, animated: true)
  649. self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
  650. }
  651. self.resetBottomToolBtnStatus()
  652. }
  653. func showEditImageVC(model: ZLPhotoModel) {
  654. let nav = self.navigationController as! ZLImageNavController
  655. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  656. hud.show()
  657. hud.show()
  658. ZLPhotoManager.fetchImage(for: model.asset, size: model.previewSize) { [weak self, weak nav] (image, isDegraded) in
  659. if !isDegraded {
  660. if let image = image {
  661. ZLEditImageViewController.showEditImageVC(parentVC: self, image: image, editModel: model.editImageModel) { [weak nav] (ei, editImageModel) in
  662. model.isSelected = true
  663. model.editImage = ei
  664. model.editImageModel = editImageModel
  665. nav?.arrSelectedModels.append(model)
  666. nav?.selectImageBlock?()
  667. }
  668. } else {
  669. showAlertView(localLanguageTextValue(.imageLoadFailed), self)
  670. }
  671. hud.hide()
  672. }
  673. }
  674. }
  675. func showEditVideoVC(model: ZLPhotoModel) {
  676. let nav = self.navigationController as! ZLImageNavController
  677. let hud = ZLProgressHUD(style: ZLPhotoConfiguration.default().hudStyle)
  678. var requestAvAssetID: PHImageRequestID?
  679. hud.show(timeout: 20)
  680. hud.timeoutBlock = { [weak self] in
  681. showAlertView(localLanguageTextValue(.timeout), self)
  682. if let _ = requestAvAssetID {
  683. PHImageManager.default().cancelImageRequest(requestAvAssetID!)
  684. }
  685. }
  686. func inner_showEditVideoVC(_ avAsset: AVAsset) {
  687. let vc = ZLEditVideoViewController(avAsset: avAsset)
  688. vc.editFinishBlock = { [weak self, weak nav] (url) in
  689. if let u = url {
  690. ZLPhotoManager.saveVideoToAlbum(url: u) { [weak self, weak nav] (suc, asset) in
  691. if suc, asset != nil {
  692. let m = ZLPhotoModel(asset: asset!)
  693. m.isSelected = true
  694. nav?.arrSelectedModels.append(m)
  695. nav?.selectImageBlock?()
  696. } else {
  697. showAlertView(localLanguageTextValue(.saveVideoError), self)
  698. }
  699. }
  700. } else {
  701. nav?.arrSelectedModels.append(model)
  702. nav?.selectImageBlock?()
  703. }
  704. }
  705. vc.modalPresentationStyle = .fullScreen
  706. self.showDetailViewController(vc, sender: nil)
  707. }
  708. // 提前fetch一下 avasset
  709. requestAvAssetID = ZLPhotoManager.fetchAVAsset(forVideo: model.asset) { [weak self] (avAsset, _) in
  710. hud.hide()
  711. if let _ = avAsset {
  712. inner_showEditVideoVC(avAsset!)
  713. } else {
  714. showAlertView(localLanguageTextValue(.timeout), self)
  715. }
  716. }
  717. }
  718. }
  719. // MARK: Gesture delegate
  720. extension ZLThumbnailViewController: UIGestureRecognizerDelegate {
  721. func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  722. let config = ZLPhotoConfiguration.default()
  723. if config.maxSelectCount == 1, !config.showSelectBtnWhenSingleSelect {
  724. return false
  725. }
  726. return true
  727. }
  728. }
  729. // MARK: CollectionView Delegate & DataSource
  730. extension ZLThumbnailViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  731. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
  732. return ZLLayout.thumbCollectionViewItemSpacing
  733. }
  734. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  735. return ZLLayout.thumbCollectionViewLineSpacing
  736. }
  737. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  738. let defaultCount = CGFloat(ZLPhotoConfiguration.default().columnCount)
  739. var columnCount: CGFloat = deviceIsiPad() ? (defaultCount+2) : defaultCount
  740. if UIApplication.shared.statusBarOrientation.isLandscape {
  741. columnCount += 2
  742. }
  743. let totalW = collectionView.bounds.width - (columnCount - 1) * ZLLayout.thumbCollectionViewItemSpacing
  744. let singleW = totalW / columnCount
  745. return CGSize(width: singleW, height: singleW)
  746. }
  747. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  748. return self.arrDataSources.count + self.offset
  749. }
  750. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  751. let config = ZLPhotoConfiguration.default()
  752. if self.showCameraCell && ((config.sortAscending && indexPath.row == self.arrDataSources.count) || (!config.sortAscending && indexPath.row == 0)) {
  753. // camera cell
  754. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLCameraCell.zl_identifier(), for: indexPath) as! ZLCameraCell
  755. if config.showCaptureImageOnTakePhotoBtn {
  756. cell.startCapture()
  757. }
  758. return cell
  759. }
  760. if #available(iOS 14, *) {
  761. if self.showAddPhotoCell && ((config.sortAscending && indexPath.row == self.arrDataSources.count - 1 + self.offset) || (!config.sortAscending && indexPath.row == self.offset - 1)) {
  762. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLAddPhotoCell.zl_identifier(), for: indexPath) as? ZLAddPhotoCell else {
  763. return UICollectionViewCell()
  764. }
  765. return cell
  766. }
  767. }
  768. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLThumbnailPhotoCell.zl_identifier(), for: indexPath) as! ZLThumbnailPhotoCell
  769. let model: ZLPhotoModel
  770. if !config.sortAscending {
  771. model = self.arrDataSources[indexPath.row - self.offset]
  772. } else {
  773. model = self.arrDataSources[indexPath.row]
  774. }
  775. let nav = self.navigationController as? ZLImageNavController
  776. cell.selectedBlock = { [weak self, weak nav, weak cell] (isSelected) in
  777. if !isSelected {
  778. let currentSelectCount = nav?.arrSelectedModels.count ?? 0
  779. guard canAddModel(model, currentSelectCount: currentSelectCount, sender: self) else {
  780. return
  781. }
  782. if self?.shouldDirectEdit(model) == false {
  783. model.isSelected = true
  784. nav?.arrSelectedModels.append(model)
  785. cell?.btnSelect.isSelected = true
  786. self?.refreshCellIndexAndMaskView()
  787. }
  788. } else {
  789. cell?.btnSelect.isSelected = false
  790. model.isSelected = false
  791. nav?.arrSelectedModels.removeAll { $0 == model }
  792. self?.refreshCellIndexAndMaskView()
  793. }
  794. self?.resetBottomToolBtnStatus()
  795. }
  796. cell.indexLabel.isHidden = true
  797. if ZLPhotoConfiguration.default().showSelectedIndex {
  798. for (index, selM) in (nav?.arrSelectedModels ?? []).enumerated() {
  799. if model == selM {
  800. self.setCellIndex(cell, showIndexLabel: true, index: index + 1)
  801. break
  802. }
  803. }
  804. }
  805. self.setCellMaskView(cell, isSelected: model.isSelected, model: model)
  806. cell.model = model
  807. return cell
  808. }
  809. func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  810. guard let c = cell as? ZLThumbnailPhotoCell else {
  811. return
  812. }
  813. var index = indexPath.row
  814. if !ZLPhotoConfiguration.default().sortAscending {
  815. index -= self.offset
  816. }
  817. let model = self.arrDataSources[index]
  818. self.setCellMaskView(c, isSelected: model.isSelected, model: model)
  819. }
  820. func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  821. let c = collectionView.cellForItem(at: indexPath)
  822. if c is ZLCameraCell {
  823. self.showCamera()
  824. return
  825. }
  826. if #available(iOS 14, *) {
  827. if c is ZLAddPhotoCell {
  828. PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
  829. return
  830. }
  831. }
  832. guard let cell = c as? ZLThumbnailPhotoCell else {
  833. return
  834. }
  835. if !ZLPhotoConfiguration.default().allowPreviewPhotos {
  836. cell.btnSelectClick()
  837. return
  838. }
  839. if !cell.enableSelect, ZLPhotoConfiguration.default().showInvalidMask {
  840. return
  841. }
  842. let config = ZLPhotoConfiguration.default()
  843. var index = indexPath.row
  844. if !config.sortAscending {
  845. index -= self.offset
  846. }
  847. let m = self.arrDataSources[index]
  848. if self.shouldDirectEdit(m) {
  849. return
  850. }
  851. let vc = ZLPhotoPreviewController(photos: self.arrDataSources, index: index)
  852. self.show(vc, sender: nil)
  853. }
  854. func shouldDirectEdit(_ model: ZLPhotoModel) -> Bool {
  855. let config = ZLPhotoConfiguration.default()
  856. let canEditImage = config.editAfterSelectThumbnailImage &&
  857. config.allowEditImage &&
  858. config.maxSelectCount == 1 &&
  859. model.type.rawValue < ZLPhotoModel.MediaType.video.rawValue
  860. let canEditVideo = (config.editAfterSelectThumbnailImage &&
  861. config.allowEditVideo &&
  862. model.type == .video &&
  863. config.maxSelectCount == 1) ||
  864. (config.allowEditVideo &&
  865. model.type == .video &&
  866. !config.allowMixSelect &&
  867. config.cropVideoAfterSelectThumbnail)
  868. //当前未选择图片 或已经选择了一张并且点击的是已选择的图片
  869. let nav = self.navigationController as? ZLImageNavController
  870. let arrSelectedModels = nav?.arrSelectedModels ?? []
  871. let flag = arrSelectedModels.isEmpty || (arrSelectedModels.count == 1 && arrSelectedModels.first?.ident == model.ident)
  872. if canEditImage, flag {
  873. self.showEditImageVC(model: model)
  874. } else if canEditVideo, flag {
  875. self.showEditVideoVC(model: model)
  876. }
  877. return flag && (canEditImage || canEditVideo)
  878. }
  879. func setCellIndex(_ cell: ZLThumbnailPhotoCell?, showIndexLabel: Bool, index: Int) {
  880. guard ZLPhotoConfiguration.default().showSelectedIndex else {
  881. return
  882. }
  883. cell?.index = index
  884. cell?.indexLabel.isHidden = !showIndexLabel
  885. }
  886. func refreshCellIndexAndMaskView() {
  887. let showIndex = ZLPhotoConfiguration.default().showSelectedIndex
  888. let showMask = ZLPhotoConfiguration.default().showSelectedMask || ZLPhotoConfiguration.default().showInvalidMask
  889. guard showIndex || showMask else {
  890. return
  891. }
  892. let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems
  893. visibleIndexPaths.forEach { (indexPath) in
  894. guard let cell = self.collectionView.cellForItem(at: indexPath) as? ZLThumbnailPhotoCell else {
  895. return
  896. }
  897. var row = indexPath.row
  898. if !ZLPhotoConfiguration.default().sortAscending {
  899. row -= self.offset
  900. }
  901. let m = self.arrDataSources[row]
  902. let arrSel = (self.navigationController as? ZLImageNavController)?.arrSelectedModels ?? []
  903. var show = false
  904. var idx = 0
  905. var isSelected = false
  906. for (index, selM) in arrSel.enumerated() {
  907. if m == selM {
  908. show = true
  909. idx = index + 1
  910. isSelected = true
  911. break
  912. }
  913. }
  914. if showIndex {
  915. self.setCellIndex(cell, showIndexLabel: show, index: idx)
  916. }
  917. if showMask {
  918. self.setCellMaskView(cell, isSelected: isSelected, model: m)
  919. }
  920. }
  921. }
  922. func setCellMaskView(_ cell: ZLThumbnailPhotoCell, isSelected: Bool, model: ZLPhotoModel) {
  923. cell.coverView.isHidden = true
  924. cell.enableSelect = true
  925. let arrSel = (self.navigationController as? ZLImageNavController)?.arrSelectedModels ?? []
  926. let config = ZLPhotoConfiguration.default()
  927. if isSelected {
  928. cell.coverView.backgroundColor = .selectedMaskColor
  929. cell.coverView.isHidden = !config.showSelectedMask
  930. if config.showSelectedBorder {
  931. cell.layer.borderWidth = 4
  932. }
  933. } else {
  934. let selCount = arrSel.count
  935. if selCount < config.maxSelectCount {
  936. if config.allowMixSelect {
  937. let videoCount = arrSel.filter { $0.type == .video }.count
  938. if videoCount >= config.maxVideoSelectCount, model.type == .video {
  939. cell.coverView.backgroundColor = .invalidMaskColor
  940. cell.coverView.isHidden = !config.showInvalidMask
  941. cell.enableSelect = false
  942. } else if (config.maxSelectCount - selCount) <= (config.minVideoSelectCount - videoCount), model.type != .video {
  943. cell.coverView.backgroundColor = .invalidMaskColor
  944. cell.coverView.isHidden = !config.showInvalidMask
  945. cell.enableSelect = false
  946. }
  947. } else if selCount > 0 {
  948. cell.coverView.backgroundColor = .invalidMaskColor
  949. cell.coverView.isHidden = (!config.showInvalidMask || model.type != .video)
  950. cell.enableSelect = model.type != .video
  951. }
  952. } else if selCount >= config.maxSelectCount {
  953. cell.coverView.backgroundColor = .invalidMaskColor
  954. cell.coverView.isHidden = !config.showInvalidMask
  955. cell.enableSelect = false
  956. }
  957. if config.showSelectedBorder {
  958. cell.layer.borderWidth = 0
  959. }
  960. }
  961. }
  962. }
  963. extension ZLThumbnailViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  964. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  965. picker.dismiss(animated: true) {
  966. let image = info[.originalImage] as? UIImage
  967. let url = info[.mediaURL] as? URL
  968. self.save(image: image, videoUrl: url)
  969. }
  970. }
  971. }
  972. extension ZLThumbnailViewController: PHPhotoLibraryChangeObserver {
  973. func photoLibraryDidChange(_ changeInstance: PHChange) {
  974. guard let changes = changeInstance.changeDetails(for: self.albumList.result)
  975. else { return }
  976. DispatchQueue.main.sync {
  977. // 变化后再次显示相册列表需要刷新
  978. self.hasTakeANewAsset = true
  979. self.albumList.result = changes.fetchResultAfterChanges
  980. let nav = (self.navigationController as! ZLImageNavController)
  981. if changes.hasIncrementalChanges {
  982. for sm in nav.arrSelectedModels {
  983. let isDelete = changeInstance.changeDetails(for: sm.asset)?.objectWasDeleted ?? false
  984. if isDelete {
  985. nav.arrSelectedModels.removeAll { $0 == sm }
  986. }
  987. }
  988. if (!changes.removedObjects.isEmpty || !changes.insertedObjects.isEmpty) {
  989. self.albumList.models.removeAll()
  990. }
  991. self.loadPhotos()
  992. } else {
  993. for sm in nav.arrSelectedModels {
  994. let isDelete = changeInstance.changeDetails(for: sm.asset)?.objectWasDeleted ?? false
  995. if isDelete {
  996. nav.arrSelectedModels.removeAll { $0 == sm }
  997. }
  998. }
  999. self.albumList.models.removeAll()
  1000. self.loadPhotos()
  1001. }
  1002. self.resetBottomToolBtnStatus()
  1003. }
  1004. }
  1005. }
  1006. // MARK: embed album list nav view
  1007. class ZLEmbedAlbumListNavView: UIView {
  1008. static let titleViewH: CGFloat = 32
  1009. static let arrowH: CGFloat = 20
  1010. var title: String {
  1011. didSet {
  1012. self.albumTitleLabel.text = title
  1013. self.refreshTitleViewFrame()
  1014. }
  1015. }
  1016. var navBlurView: UIVisualEffectView?
  1017. var titleBgControl: UIControl!
  1018. var albumTitleLabel: UILabel!
  1019. var arrow: UIImageView!
  1020. var cancelBtn: UIButton!
  1021. var selectAlbumBlock: ( () -> Void )?
  1022. var cancelBlock: ( () -> Void )?
  1023. init(title: String) {
  1024. self.title = title
  1025. super.init(frame: .zero)
  1026. self.setupUI()
  1027. }
  1028. required init?(coder: NSCoder) {
  1029. fatalError("init(coder:) has not been implemented")
  1030. }
  1031. override func layoutSubviews() {
  1032. super.layoutSubviews()
  1033. var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
  1034. if #available(iOS 11.0, *) {
  1035. insets = self.safeAreaInsets
  1036. }
  1037. self.refreshTitleViewFrame()
  1038. let cancelBtnW = localLanguageTextValue(.cancel).boundingRect(font: ZLLayout.navTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 44)).width
  1039. self.cancelBtn.frame = CGRect(x: insets.left+20, y: insets.top, width: cancelBtnW, height: 44)
  1040. }
  1041. func refreshTitleViewFrame() {
  1042. var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
  1043. if #available(iOS 11.0, *) {
  1044. insets = self.safeAreaInsets
  1045. }
  1046. self.navBlurView?.frame = self.bounds
  1047. let albumTitleW = min(self.bounds.width / 2, self.title.boundingRect(font: ZLLayout.navTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 44)).width)
  1048. let titleBgControlW = albumTitleW + ZLEmbedAlbumListNavView.arrowH + 20
  1049. UIView.animate(withDuration: 0.25) {
  1050. self.titleBgControl.frame = CGRect(x: (self.frame.width-titleBgControlW)/2, y: insets.top+(44-ZLEmbedAlbumListNavView.titleViewH)/2, width: titleBgControlW, height: ZLEmbedAlbumListNavView.titleViewH)
  1051. self.albumTitleLabel.frame = CGRect(x: 10, y: 0, width: albumTitleW, height: ZLEmbedAlbumListNavView.titleViewH)
  1052. self.arrow.frame = CGRect(x: self.albumTitleLabel.frame.maxX+5, y: (ZLEmbedAlbumListNavView.titleViewH-ZLEmbedAlbumListNavView.arrowH)/2.0, width: ZLEmbedAlbumListNavView.arrowH, height: ZLEmbedAlbumListNavView.arrowH)
  1053. }
  1054. }
  1055. func setupUI() {
  1056. self.backgroundColor = .navBarColor
  1057. if let effect = ZLPhotoConfiguration.default().navViewBlurEffect {
  1058. self.navBlurView = UIVisualEffectView(effect: effect)
  1059. self.addSubview(self.navBlurView!)
  1060. }
  1061. self.titleBgControl = UIControl()
  1062. self.titleBgControl.backgroundColor = .navEmbedTitleViewBgColor
  1063. self.titleBgControl.layer.cornerRadius = ZLEmbedAlbumListNavView.titleViewH / 2
  1064. self.titleBgControl.layer.masksToBounds = true
  1065. self.titleBgControl.addTarget(self, action: #selector(titleBgControlClick), for: .touchUpInside)
  1066. self.addSubview(titleBgControl)
  1067. self.albumTitleLabel = UILabel()
  1068. self.albumTitleLabel.textColor = .navTitleColor
  1069. self.albumTitleLabel.font = ZLLayout.navTitleFont
  1070. self.albumTitleLabel.text = self.title
  1071. self.albumTitleLabel.textAlignment = .center
  1072. self.titleBgControl.addSubview(self.albumTitleLabel)
  1073. self.arrow = UIImageView(image: getImage("zl_downArrow"))
  1074. self.arrow.clipsToBounds = true
  1075. self.arrow.contentMode = .scaleAspectFill
  1076. self.titleBgControl.addSubview(self.arrow)
  1077. self.cancelBtn = UIButton(type: .custom)
  1078. self.cancelBtn.titleLabel?.font = ZLLayout.navTitleFont
  1079. self.cancelBtn.setTitle(localLanguageTextValue(.cancel), for: .normal)
  1080. self.cancelBtn.setTitleColor(.navTitleColor, for: .normal)
  1081. self.cancelBtn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
  1082. self.addSubview(self.cancelBtn)
  1083. }
  1084. @objc func titleBgControlClick() {
  1085. self.selectAlbumBlock?()
  1086. if self.arrow.transform == .identity {
  1087. UIView.animate(withDuration: 0.25) {
  1088. self.arrow.transform = CGAffineTransform(rotationAngle: .pi)
  1089. }
  1090. } else {
  1091. UIView.animate(withDuration: 0.25) {
  1092. self.arrow.transform = .identity
  1093. }
  1094. }
  1095. }
  1096. @objc func cancelBtnClick() {
  1097. self.cancelBlock?()
  1098. }
  1099. func reset() {
  1100. UIView.animate(withDuration: 0.25) {
  1101. self.arrow.transform = .identity
  1102. }
  1103. }
  1104. }
  1105. // MARK: external album list nav view
  1106. class ZLExternalAlbumListNavView: UIView {
  1107. let title: String
  1108. var navBlurView: UIVisualEffectView?
  1109. var backBtn: UIButton!
  1110. var albumTitleLabel: UILabel!
  1111. var cancelBtn: UIButton!
  1112. var backBlock: ( () -> Void )?
  1113. var cancelBlock: ( () -> Void )?
  1114. init(title: String) {
  1115. self.title = title
  1116. super.init(frame: .zero)
  1117. self.setupUI()
  1118. }
  1119. required init?(coder: NSCoder) {
  1120. fatalError("init(coder:) has not been implemented")
  1121. }
  1122. override func layoutSubviews() {
  1123. super.layoutSubviews()
  1124. var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
  1125. if #available(iOS 11.0, *) {
  1126. insets = self.safeAreaInsets
  1127. }
  1128. self.navBlurView?.frame = self.bounds
  1129. self.backBtn.frame = CGRect(x: insets.left, y: insets.top, width: 60, height: 44)
  1130. let albumTitleW = min(self.bounds.width / 2, self.title.boundingRect(font: ZLLayout.navTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 44)).width)
  1131. self.albumTitleLabel.frame = CGRect(x: (self.frame.width-albumTitleW)/2, y: insets.top, width: albumTitleW, height: 44)
  1132. let cancelBtnW = localLanguageTextValue(.cancel).boundingRect(font: ZLLayout.navTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 44)).width + 40
  1133. self.cancelBtn.frame = CGRect(x: self.frame.width-insets.right-cancelBtnW, y: insets.top, width: cancelBtnW, height: 44)
  1134. }
  1135. func setupUI() {
  1136. self.backgroundColor = .navBarColor
  1137. if let effect = ZLPhotoConfiguration.default().navViewBlurEffect {
  1138. self.navBlurView = UIVisualEffectView(effect: effect)
  1139. self.addSubview(self.navBlurView!)
  1140. }
  1141. self.backBtn = UIButton(type: .custom)
  1142. self.backBtn.setImage(getImage("zl_navBack"), for: .normal)
  1143. self.backBtn.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0)
  1144. self.backBtn.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
  1145. self.addSubview(self.backBtn)
  1146. self.albumTitleLabel = UILabel()
  1147. self.albumTitleLabel.textColor = .navTitleColor
  1148. self.albumTitleLabel.font = ZLLayout.navTitleFont
  1149. self.albumTitleLabel.text = self.title
  1150. self.albumTitleLabel.textAlignment = .center
  1151. self.addSubview(self.albumTitleLabel)
  1152. self.cancelBtn = UIButton(type: .custom)
  1153. self.cancelBtn.titleLabel?.font = ZLLayout.navTitleFont
  1154. self.cancelBtn.setTitle(localLanguageTextValue(.cancel), for: .normal)
  1155. self.cancelBtn.setTitleColor(.navTitleColor, for: .normal)
  1156. self.cancelBtn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
  1157. self.addSubview(self.cancelBtn)
  1158. }
  1159. @objc func backBtnClick() {
  1160. self.backBlock?()
  1161. }
  1162. @objc func cancelBtnClick() {
  1163. self.cancelBlock?()
  1164. }
  1165. }
  1166. class ZLLimitedAuthorityTipsView: UIView {
  1167. static let height: CGFloat = 70
  1168. var icon: UIImageView!
  1169. var tipsLabel: UILabel!
  1170. var arrow: UIImageView!
  1171. override init(frame: CGRect) {
  1172. super.init(frame: frame)
  1173. self.icon = UIImageView(image: getImage("zl_warning"))
  1174. self.addSubview(self.icon)
  1175. self.tipsLabel = UILabel()
  1176. self.tipsLabel.font = getFont(14)
  1177. self.tipsLabel.text = localLanguageTextValue(.unableToAccessAllPhotos)
  1178. self.tipsLabel.textColor = .bottomToolViewBtnDisableTitleColor
  1179. self.tipsLabel.numberOfLines = 2
  1180. self.tipsLabel.lineBreakMode = .byTruncatingTail
  1181. self.tipsLabel.adjustsFontSizeToFitWidth = true
  1182. self.tipsLabel.minimumScaleFactor = 0.5
  1183. self.addSubview(self.tipsLabel)
  1184. self.arrow = UIImageView(image: getImage("zl_right_arrow"))
  1185. self.addSubview(self.arrow)
  1186. let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction))
  1187. self.addGestureRecognizer(tap)
  1188. }
  1189. required init?(coder: NSCoder) {
  1190. fatalError("init(coder:) has not been implemented")
  1191. }
  1192. override func layoutSubviews() {
  1193. super.layoutSubviews()
  1194. self.icon.frame = CGRect(x: 18, y: (ZLLimitedAuthorityTipsView.height - 25) / 2, width: 25, height: 25)
  1195. self.tipsLabel.frame = CGRect(x: 55, y: (ZLLimitedAuthorityTipsView.height - 40) / 2, width: self.frame.width-55-30, height: 40)
  1196. self.arrow.frame = CGRect(x: self.frame.width-25, y: (ZLLimitedAuthorityTipsView.height - 12) / 2, width: 12, height: 12)
  1197. }
  1198. @objc func tapAction() {
  1199. guard let url = URL(string: UIApplication.openSettingsURLString) else {
  1200. return
  1201. }
  1202. if UIApplication.shared.canOpenURL(url) {
  1203. UIApplication.shared.open(url, options: [:], completionHandler: nil)
  1204. }
  1205. }
  1206. }