diff --git a/PhotoBrowser.xcodeproj/project.pbxproj b/PhotoBrowser.xcodeproj/project.pbxproj index d5de503..0ec80e2 100644 --- a/PhotoBrowser.xcodeproj/project.pbxproj +++ b/PhotoBrowser.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1E11D49920AAC4CD00E48785 /* MiniMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E11D49820AAC4CD00E48785 /* MiniMap.swift */; }; 4A0739991C98FB3C0004FEA5 /* PBCustomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0739981C98FB3C0004FEA5 /* PBCustomView.swift */; }; 4A0B3FDF1C76DB300049338C /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A0B3FDE1C76DB300049338C /* Kingfisher.framework */; }; 4A52D1E41C72CB2E001C257B /* PhotoBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A52D1E31C72CB2E001C257B /* PhotoBrowser.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -23,6 +24,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1E11D49820AAC4CD00E48785 /* MiniMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiniMap.swift; sourceTree = ""; }; 4A0739981C98FB3C0004FEA5 /* PBCustomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PBCustomView.swift; sourceTree = ""; }; 4A0B3FDE1C76DB300049338C /* Kingfisher.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kingfisher.framework; path = Carthage/Build/iOS/Kingfisher.framework; sourceTree = ""; }; 4A52D1E01C72CB2E001C257B /* PhotoBrowser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PhotoBrowser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -85,6 +87,7 @@ 4A0739981C98FB3C0004FEA5 /* PBCustomView.swift */, A18117DF1E83B66900F8CE8E /* CustomPhotoBroswerManager.swift */, C4C79EA02057808200C92C0C /* UIView+Frame.swift */, + 1E11D49820AAC4CD00E48785 /* MiniMap.swift */, 4A52D1E51C72CB2E001C257B /* Info.plist */, ); path = PhotoBrowser; @@ -178,6 +181,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E11D49920AAC4CD00E48785 /* MiniMap.swift in Sources */, 864F45E81E9F0A4900FF9215 /* Skitch.swift in Sources */, 4A52D2301C72F47C001C257B /* Photo.swift in Sources */, 4A0739991C98FB3C0004FEA5 /* PBCustomView.swift in Sources */, @@ -241,7 +245,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -293,7 +297,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/PhotoBrowser/MiniMap.swift b/PhotoBrowser/MiniMap.swift new file mode 100644 index 0000000..64da47b --- /dev/null +++ b/PhotoBrowser/MiniMap.swift @@ -0,0 +1,138 @@ +// +// MiniMap.swift +// ImageMap +// +// Created by wzxjiang on 2018/5/10. +// Copyright © 2018 wzxjiang. All rights reserved. +// + +import UIKit + +struct Ratios { + let top: CGFloat + let left: CGFloat + let width: CGFloat + let height: CGFloat + + static let zero = Ratios(top: 0, left: 0, width: 0, height: 0) + + init(top: CGFloat, left: CGFloat, width: CGFloat, height: CGFloat) { + self.top = min(max(top, 0), 1) + self.left = min(max(left, 0), 1) + self.width = min(max(width, 0), 1) + self.height = min(max(height, 0), 1) + } +} + +public class MiniMap: UIView { + public var image = UIImage() { + didSet { + updateimageViewSize() + imageView.image = image + } + } + + private let imageView = UIImageView() + + private let backgroundLayer = CALayer() + + private var lineLayer = CAShapeLayer() + + private var maskLayer = CAShapeLayer() + + private let _size: CGSize + + var ratios: Ratios = .zero { + didSet { + updateLayer() + } + } + + private var imageViewSize: CGSize + + required public init(size: CGSize) { + _size = size + imageViewSize = size + super.init(frame: .zero) + setup() + addLayer() + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + backgroundColor = .black + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + updateImageViewSize() + } + + private func addLayer() { + backgroundLayer.frame = CGRect(origin: .zero, size: _size) + backgroundLayer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor + imageView.layer.addSublayer(backgroundLayer) + + lineLayer.strokeColor = UIColor.white.cgColor + lineLayer.fillColor = UIColor.clear.cgColor + imageView.layer.addSublayer(lineLayer) + } + + private func updateLayer() { + var top: CGFloat = imageViewSize.height * ratios.top + var left: CGFloat = imageViewSize.width * ratios.left + let width: CGFloat = imageViewSize.width * ratios.width + let height: CGFloat = imageViewSize.height * ratios.height + + if top + height > imageViewSize.height { + top = imageViewSize.height - height + } + + if left + width > imageViewSize.width { + left = imageViewSize.width - width + } + + let maskBezierPath = UIBezierPath() + maskBezierPath.move(to: CGPoint(x: 0, y: 0)) + maskBezierPath.addLine(to: CGPoint(x: 0, y: imageViewSize.height)) + maskBezierPath.addLine(to: CGPoint(x: imageViewSize.width, y: imageViewSize.height)) + maskBezierPath.addLine(to: CGPoint(x: imageViewSize.width, y: 0)) + maskBezierPath.move(to: CGPoint(x: left, y: top)) + maskBezierPath.addLine(to: CGPoint(x: left + width, y: top)) + maskBezierPath.addLine(to: CGPoint(x: left + width, y: top + height)) + maskBezierPath.addLine(to: CGPoint(x: left, y: top + height)) + maskBezierPath.close() + maskLayer.path = maskBezierPath.cgPath + backgroundLayer.mask = maskLayer + + + let lineBezierPath = UIBezierPath() + lineBezierPath.move(to: CGPoint(x: left, y: top)) + lineBezierPath.addLine(to: CGPoint(x: left + width, y: top)) + lineBezierPath.addLine(to: CGPoint(x: left + width, y: top + height)) + lineBezierPath.addLine(to: CGPoint(x: left, y: top + height)) + lineBezierPath.close() + lineBezierPath.stroke() + lineLayer.path = lineBezierPath.cgPath + } + + private func updateimageViewSize() { + let ratio = image.size.width / image.size.height + + if image.size.width > image.size.height { + imageViewSize = CGSize(width: _size.width, height: _size.width / ratio) + } else { + imageViewSize = CGSize(width: _size.height * ratio, height: _size.height) + } + + updateImageViewSize() + } + + private func updateImageViewSize() { + imageView.widthAnchor.constraint(equalToConstant: imageViewSize.width).isActive = true + imageView.heightAnchor.constraint(equalToConstant: imageViewSize.height).isActive = true + } +} diff --git a/PhotoBrowser/PhotoPreviewController.swift b/PhotoBrowser/PhotoPreviewController.swift index b1a9496..b68907f 100644 --- a/PhotoBrowser/PhotoPreviewController.swift +++ b/PhotoBrowser/PhotoPreviewController.swift @@ -11,6 +11,20 @@ import UIKit import Kingfisher import Photos +private extension UIDevice { + static let isIPhoneX = (UIScreen.main.bounds.size == DeviceSize.Portrait.iPhoneX || UIScreen.main.bounds.size == DeviceSize.Landscape.iPhoneX) +} + +private struct DeviceSize { + struct Landscape { + static let iPhoneX = CGSize(width: 812, height: 375) + } + + struct Portrait { + static let iPhoneX = CGSize(width: 375, height: 812) + } +} + // MARK: - PhotoPreviewControllerDelegate protocol PhotoPreviewControllerDelegate: class { var isFullScreenMode: Bool {get set} @@ -56,7 +70,7 @@ class PhotoPreviewController: UIViewController { lazy var imageView: UIImageView = self.makeImageView() var waitingView: WaitingView? - weak var delegate:PhotoPreviewControllerDelegate? + weak var delegate: PhotoPreviewControllerDelegate? fileprivate let minPanY: CGFloat = -10 fileprivate let maxMoveOfY: CGFloat = 250 @@ -81,10 +95,37 @@ class PhotoPreviewController: UIViewController { fileprivate var scrollNewY: CGFloat = 0 fileprivate var scrollOldY: CGFloat = 0 - fileprivate var isFullScreenMode: Bool = false + fileprivate var isFullScreenMode: Bool = false { + didSet { + updateMiniMapLayout() + } + } fileprivate var panLastY: CGFloat = 0 - + + fileprivate var miniMap: MiniMap? + + fileprivate var miniMapTopConstraint: NSLayoutConstraint? + + private func makeMiniMap() -> MiniMap { + let miniMap = MiniMap(size: miniMapSize) + miniMap.isHidden = true + view.addSubview(miniMap) + miniMap.translatesAutoresizingMaskIntoConstraints = false + miniMap.widthAnchor.constraint(equalToConstant: miniMapSize.width).isActive = true + miniMap.heightAnchor.constraint(equalToConstant: miniMapSize.height).isActive = true + miniMap.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15).isActive = true + if #available(iOS 11.0, *), UIDevice.isIPhoneX { + miniMapTopConstraint = miniMap.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: isFullScreenMode ? 15 : 15 + 44) + } else { + miniMapTopConstraint = miniMap.topAnchor.constraint(equalTo: view.topAnchor, constant: isFullScreenMode ? 15 : 15 + 64) + } + miniMapTopConstraint?.isActive = true + return miniMap + } + + public var miniMapSize: CGSize = CGSize(width: 100, height: 100) + init(photo: Photo, index: NSInteger, skitches: [[String: Any]]? = nil, isSkitchButtonHidden: Bool = true) { super.init(nibName: nil, bundle: nil) self.index = index @@ -183,6 +224,7 @@ class PhotoPreviewController: UIViewController { } fileprivate func setImageViewFrame(_ image: UIImage) { + miniMap?.image = image imageView.width = screenWidth imageView.height = image.size.height / image.size.width * screenWidth imageView.center = self.view.center @@ -218,6 +260,8 @@ class PhotoPreviewController: UIViewController { singleTap.require(toFail: doubleTap) + miniMap = makeMiniMap() + if let image = photo.localOriginalPhoto() { setImageViewFrame(image) imageView.image = image @@ -265,6 +309,10 @@ class PhotoPreviewController: UIViewController { } } + @objc fileprivate func hideMiniMap() { + miniMap?.isHidden = true + } + func updateConstraint() { updateSkitchViewConstraint() view.layoutIfNeeded() @@ -276,6 +324,22 @@ class PhotoPreviewController: UIViewController { } return 2 * scrollView.minimumZoomScale } + + fileprivate func updateMiniMapLayout() { + guard miniMap != nil else { + return + } + + if UIDevice.isIPhoneX { + miniMapTopConstraint?.constant = isFullScreenMode ? 15 : 15 + 44 + } else { + miniMapTopConstraint?.constant = isFullScreenMode ? 15 : 15 + 64 + } + + UIView.animate(withDuration: 0.25) { + self.view.layoutIfNeeded() + } + } } extension PhotoPreviewController: SkitchViewDelegate { @@ -304,6 +368,7 @@ extension PhotoPreviewController { guard let delegate = delegate else { return } + isFullScreenMode = !isFullScreenMode delegate.isFullScreenMode = !delegate.isFullScreenMode } @@ -344,11 +409,31 @@ extension PhotoPreviewController: UIScrollViewDelegate { } func scrollViewDidScroll(_ scrollView: UIScrollView) { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideMiniMap), object: nil) + scrollNewY = scrollView.contentOffset.y if (scrollView.contentOffset.y < minPanY || isPanning) && !isZooming { doPan(scrollView.panGestureRecognizer) } scrollOldY = scrollNewY + + miniMap?.isHidden = scrollView.contentSize.width <= view.frame.width || moveImage != nil + + miniMap?.ratios = + Ratios( + top: scrollView.contentOffset.y / scrollView.contentSize.height, + left: scrollView.contentOffset.x / scrollView.contentSize.width, + width: view.frame.width / scrollView.contentSize.width, + height: view.frame.height / scrollView.contentSize.height + ) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if scrollView.panGestureRecognizer.state == .ended { + perform(#selector(hideMiniMap), with: self, afterDelay: 3) + } else { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideMiniMap), object: nil) + } } } @@ -421,12 +506,12 @@ extension PhotoPreviewController { // 判断是否向下拖拽 isDirectionDown = panCurrentY > panLastY panLastY = panCurrentY - + // 拖拽进度 let progress = (panCurrentY - panBeginY) / maxMoveOfY panningProgress = min(progress, 1.0) delegate?.doDraging(panningProgress) - + if panCurrentY > panBeginY { moveImage?.width = imageWidthBeforeDrag - (imageWidthBeforeDrag - imageWidthBeforeDrag * minZoom) * panningProgress moveImage?.height = imageHeightBeforeDrag - (imageHeightBeforeDrag - imageHeightBeforeDrag * minZoom) * panningProgress @@ -462,6 +547,7 @@ extension PhotoPreviewController { self.moveImage?.removeFromSuperview() self.moveImage = nil self.updateSkitchButtonStatus(false) + self.miniMap?.isHidden = self.scrollView.contentSize.width <= self.view.frame.width }) } else { guard let image = moveImage else { return } diff --git a/PhotoBrowserDemo.xcodeproj/project.pbxproj b/PhotoBrowserDemo.xcodeproj/project.pbxproj index 03bff99..0d9d7d3 100644 --- a/PhotoBrowserDemo.xcodeproj/project.pbxproj +++ b/PhotoBrowserDemo.xcodeproj/project.pbxproj @@ -267,7 +267,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -316,7 +316,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";