Add double-tap to zoom out.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-08-24 16:39:15 -04:00
parent 2b50eb5acc
commit e7b32f9fd1

View file

@ -50,22 +50,32 @@ class CropScaleImageViewController: OWSViewController {
var imageView: UIView? var imageView: UIView?
// We use a CALayer to render the image for performance reasons.
var imageLayer: CALayer? var imageLayer: CALayer?
var dashedBorderLayer: CAShapeLayer? var dashedBorderLayer: CAShapeLayer?
// In width/height. // In width/height.
let targetAspectRatio: CGFloat = 1.0 let dstAspectRatio: CGFloat = 1.0
// The size of the src image in points.
var srcImageSizePoints: CGSize = CGSize.zero var srcImageSizePoints: CGSize = CGSize.zero
var unitDefaultCropSizePoints: CGSize = CGSize.zero // The size of the default crop region, which is the
// largest crop region with the correct dst aspect ratio
// that fits in the src image's aspect ratio,
// in src image point coordinates.
var srcDefaultCropSizePoints: CGSize = CGSize.zero
// N = Scaled, zoomed in. // N = Scaled, zoomed in.
let kMaxImageScale: CGFloat = 4.0 let kMaxImageScale: CGFloat = 4.0
// 1.0 = Unscaled, cropped to fill crop rect. // 1.0 = Unscaled, cropped to fill crop rect.
let kMinImageScale: CGFloat = 1.0 let kMinImageScale: CGFloat = 1.0
// This represents the current scaling of the src image.
var imageScale: CGFloat = 1.0 var imageScale: CGFloat = 1.0
// This represents the current translation from the
// upper-left corner of the src image to the upper-left
// corner of the crop region in src image point coordinates.
var srcTranslation: CGPoint = CGPoint.zero var srcTranslation: CGPoint = CGPoint.zero
// MARK: Initializers // MARK: Initializers
@ -91,36 +101,31 @@ class CropScaleImageViewController: OWSViewController {
private func configureCropAndScale() { private func configureCropAndScale() {
// Size of bounding box that reflects the target aspect ratio, whose longer side = 1. // Size of bounding box that reflects the target aspect ratio, whose longer side = 1.
let unitSquareHeight: CGFloat = (targetAspectRatio >= 1.0 ? 1.0 : 1.0 / targetAspectRatio) let unitSquareHeight: CGFloat = (dstAspectRatio >= 1.0 ? 1.0 : 1.0 / dstAspectRatio)
let unitSquareWidth: CGFloat = (targetAspectRatio >= 1.0 ? targetAspectRatio * unitSquareHeight : 1.0) let unitSquareWidth: CGFloat = (dstAspectRatio >= 1.0 ? dstAspectRatio * unitSquareHeight : 1.0)
let unitSquareSize = CGSize(width: unitSquareWidth, height: unitSquareHeight) let unitSquareSize = CGSize(width: unitSquareWidth, height: unitSquareHeight)
let imageSizePoints = srcImage.size srcImageSizePoints = srcImage.size
guard guard
(imageSizePoints.width > 0 && imageSizePoints.height > 0) else { (srcImageSizePoints.width > 0 && srcImageSizePoints.height > 0) else {
return return
} }
self.srcImageSizePoints = imageSizePoints
Logger.error("----")
Logger.error("imageSizePoints: \(imageSizePoints)")
Logger.error("unitSquareWidth: \(unitSquareWidth)")
Logger.error("unitSquareHeight: \(unitSquareHeight)")
// Default // Default
// The "default" (no scaling, no translation) crop frame, expressed in // The "default" (no scaling, no translation) crop frame, expressed in
// srcImage's coordinate system. // srcImage's coordinate system.
unitDefaultCropSizePoints = defaultCropSizePoints(dstSizePoints:unitSquareSize) srcDefaultCropSizePoints = defaultCropSizePoints(dstSizePoints:unitSquareSize)
assert(imageSizePoints.width >= unitDefaultCropSizePoints.width) assert(srcImageSizePoints.width >= srcDefaultCropSizePoints.width)
assert(imageSizePoints.height >= unitDefaultCropSizePoints.height) assert(srcImageSizePoints.height >= srcDefaultCropSizePoints.height)
Logger.error("unitDefaultCropSizePoints: \(unitDefaultCropSizePoints)") // By default, center the crop region in the src image.
srcTranslation = CGPoint(x:(imageSizePoints.width - unitDefaultCropSizePoints.width) * 0.5, srcTranslation = CGPoint(x:(srcImageSizePoints.width - srcDefaultCropSizePoints.width) * 0.5,
y:(imageSizePoints.height - unitDefaultCropSizePoints.height) * 0.5) y:(srcImageSizePoints.height - srcDefaultCropSizePoints.height) * 0.5)
Logger.error("srcTranslation: \(srcTranslation)")
} }
// Given a dst size, find the size of the largest crop region
// that fits in the src image.
private func defaultCropSizePoints(dstSizePoints: CGSize) -> (CGSize) { private func defaultCropSizePoints(dstSizePoints: CGSize) -> (CGSize) {
assert(srcImageSizePoints.width > 0) assert(srcImageSizePoints.width > 0)
assert(srcImageSizePoints.height > 0) assert(srcImageSizePoints.height > 0)
@ -186,14 +191,18 @@ class CropScaleImageViewController: OWSViewController {
let dashedBorderLayer = CAShapeLayer() let dashedBorderLayer = CAShapeLayer()
self.dashedBorderLayer = dashedBorderLayer self.dashedBorderLayer = dashedBorderLayer
dashedBorderLayer.strokeColor = UIColor.ows_materialBlue().cgColor dashedBorderLayer.strokeColor = UIColor.ows_materialBlue().cgColor
dashedBorderLayer.lineDashPattern = [6, 6] dashedBorderLayer.lineDashPattern = [10, 10]
dashedBorderLayer.lineWidth = 2 dashedBorderLayer.lineWidth = 4
dashedBorderLayer.fillColor = nil dashedBorderLayer.fillColor = nil
imageView.layer.addSublayer(dashedBorderLayer) imageView.layer.addSublayer(dashedBorderLayer)
contentView.isUserInteractionEnabled = true contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(sender:)))) contentView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(sender:))))
contentView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:)))) contentView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:))))
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(sender:)))
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
contentView.addGestureRecognizer(doubleTap)
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
@ -235,6 +244,7 @@ class CropScaleImageViewController: OWSViewController {
return CGRect(origin:defaultCropOriginPoints, size:defaultCropSizePoints) return CGRect(origin:defaultCropOriginPoints, size:defaultCropSizePoints)
} }
// Updates the image view _AND_ normalizes the current scale/translate state.
private func updateImageLayout() { private func updateImageLayout() {
guard let imageView = self.imageView else { guard let imageView = self.imageView else {
return return
@ -248,7 +258,7 @@ class CropScaleImageViewController: OWSViewController {
guard srcImageSizePoints.width > 0 && srcImageSizePoints.height > 0 else { guard srcImageSizePoints.width > 0 && srcImageSizePoints.height > 0 else {
return return
} }
guard unitDefaultCropSizePoints.width > 0 && unitDefaultCropSizePoints.height > 0 else { guard srcDefaultCropSizePoints.width > 0 && srcDefaultCropSizePoints.height > 0 else {
return return
} }
@ -258,41 +268,28 @@ class CropScaleImageViewController: OWSViewController {
return return
} }
Logger.error("----") // Normalize the scaling property.
Logger.error("srcImageSizePoints: \(srcImageSizePoints)")
Logger.error("viewSizePoints: \(viewSizePoints)")
Logger.error("imageScale: \(imageScale)")
imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale)) imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale))
Logger.error("imageScale (normalized): \(imageScale)")
let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, let srcCropSizePoints = CGSize(width:srcDefaultCropSizePoints.width / imageScale,
height:unitDefaultCropSizePoints.height / imageScale) height:srcDefaultCropSizePoints.height / imageScale)
Logger.error("srcCropSizePoints: \(srcCropSizePoints)")
let minSrcTranslationPoints = CGPoint.zero let minSrcTranslationPoints = CGPoint.zero
let maxSrcTranslationPoints = CGPoint(x:srcImageSizePoints.width - srcCropSizePoints.width, let maxSrcTranslationPoints = CGPoint(x:srcImageSizePoints.width - srcCropSizePoints.width,
y:srcImageSizePoints.height - srcCropSizePoints.height y:srcImageSizePoints.height - srcCropSizePoints.height
) )
Logger.error("minSrcTranslationPoints: \(minSrcTranslationPoints)") // Normalize the translation property.
Logger.error("maxSrcTranslationPoints: \(maxSrcTranslationPoints)")
Logger.error("srcTranslation: \(srcTranslation)")
srcTranslation = CGPoint(x: max(minSrcTranslationPoints.x, min(maxSrcTranslationPoints.x, srcTranslation.x)), srcTranslation = CGPoint(x: max(minSrcTranslationPoints.x, min(maxSrcTranslationPoints.x, srcTranslation.x)),
y: max(minSrcTranslationPoints.y, min(maxSrcTranslationPoints.y, srcTranslation.y))) y: max(minSrcTranslationPoints.y, min(maxSrcTranslationPoints.y, srcTranslation.y)))
Logger.error("srcTranslation (normalized): \(srcTranslation)")
let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width
Logger.error("srcToViewRatio: \(srcToViewRatio)")
let imageViewFrame = CGRect(origin: CGPoint(x:srcTranslation.x * -srcToViewRatio, let imageViewFrame = CGRect(origin: CGPoint(x:srcTranslation.x * -srcToViewRatio,
y:srcTranslation.y * -srcToViewRatio), y:srcTranslation.y * -srcToViewRatio),
size: CGSize(width:srcImageSizePoints.width * +srcToViewRatio, size: CGSize(width:srcImageSizePoints.width * +srcToViewRatio,
height:srcImageSizePoints.height * +srcToViewRatio height:srcImageSizePoints.height * +srcToViewRatio
)) ))
Logger.error("imageViewFrame: \(imageViewFrame)")
// Disable implicit animations. // Disable implicit animations.
CATransaction.begin() CATransaction.begin()
@ -310,7 +307,6 @@ class CropScaleImageViewController: OWSViewController {
var lastPinchScale: CGFloat = 1.0 var lastPinchScale: CGFloat = 1.0
func handlePinch(sender: UIPinchGestureRecognizer) { func handlePinch(sender: UIPinchGestureRecognizer) {
Logger.error("pinch scale: \(sender.scale)")
switch (sender.state) { switch (sender.state) {
case .possible: case .possible:
break break
@ -327,45 +323,32 @@ class CropScaleImageViewController: OWSViewController {
return return
} }
Logger.error("--- pinch")
if sender.numberOfTouches > 1 { if sender.numberOfTouches > 1 {
let location = let location =
sender.location(in: sender.view) sender.location(in: sender.view)
let scaleDiff = sender.scale / lastPinchScale let scaleDiff = sender.scale / lastPinchScale
Logger.error("scaling \(lastPinchScale) \(sender.scale) -> \(scaleDiff)")
// Update the scaling // Update scaling.
let srcCropSizeBeforeScalePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, let srcCropSizeBeforeScalePoints = CGSize(width:srcDefaultCropSizePoints.width / imageScale,
height:unitDefaultCropSizePoints.height / imageScale) height:srcDefaultCropSizePoints.height / imageScale)
imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale * scaleDiff)) imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale * scaleDiff))
let srcCropSizeAfterScalePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, let srcCropSizeAfterScalePoints = CGSize(width:srcDefaultCropSizePoints.width / imageScale,
height:unitDefaultCropSizePoints.height / imageScale) height:srcDefaultCropSizePoints.height / imageScale)
// Since the translation state reflects the "upper left" corner of the crop region, we need to // Since the translation state reflects the "upper left" corner of the crop region, we need to
// adjust the translation when scaling. // adjust the translation when scaling.
srcTranslation.x += (srcCropSizeBeforeScalePoints.width - srcCropSizeAfterScalePoints.width) * 0.5 srcTranslation.x += (srcCropSizeBeforeScalePoints.width - srcCropSizeAfterScalePoints.width) * 0.5
srcTranslation.y += (srcCropSizeBeforeScalePoints.height - srcCropSizeAfterScalePoints.height) * 0.5 srcTranslation.y += (srcCropSizeBeforeScalePoints.height - srcCropSizeAfterScalePoints.height) * 0.5
// Update translation // Update translation.
let viewSizePoints = imageView.frame.size let viewSizePoints = imageView.frame.size
Logger.error("viewSizePoints: \(viewSizePoints)") let srcCropSizePoints = CGSize(width:srcDefaultCropSizePoints.width / imageScale,
let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, height:srcDefaultCropSizePoints.height / imageScale)
height:unitDefaultCropSizePoints.height / imageScale)
Logger.error("srcCropSizePoints: \(srcCropSizePoints)")
let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width
Logger.error("srcToViewRatio: \(srcToViewRatio)")
let viewToSrcRatio = 1 / srcToViewRatio
Logger.error("viewToSrcRatio: \(viewToSrcRatio)")
let gestureTranslation = CGPoint(x:location.x - lastPinchLocation.x, let gestureTranslation = CGPoint(x:location.x - lastPinchLocation.x,
y:location.y - lastPinchLocation.y) y:location.y - lastPinchLocation.y)
Logger.error("location: \(location)")
Logger.error("lastPinchLocation: \(lastPinchLocation)")
Logger.error("gestureTranslation: \(gestureTranslation)")
srcTranslation = CGPoint(x:srcTranslation.x + gestureTranslation.x * -viewToSrcRatio, srcTranslation = CGPoint(x:srcTranslation.x + gestureTranslation.x * -viewToSrcRatio,
y:srcTranslation.y + gestureTranslation.y * -viewToSrcRatio) y:srcTranslation.y + gestureTranslation.y * -viewToSrcRatio)
@ -396,21 +379,15 @@ class CropScaleImageViewController: OWSViewController {
return return
} }
let viewSizePoints = imageView.frame.size let viewSizePoints = imageView.frame.size
Logger.error("viewSizePoints: \(viewSizePoints)") let srcCropSizePoints = CGSize(width:srcDefaultCropSizePoints.width / imageScale,
let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, height:srcDefaultCropSizePoints.height / imageScale)
height:unitDefaultCropSizePoints.height / imageScale)
Logger.error("srcCropSizePoints: \(srcCropSizePoints)")
let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width let viewToSrcRatio = srcCropSizePoints.width / viewSizePoints.width
Logger.error("srcToViewRatio: \(srcToViewRatio)")
let viewToSrcRatio = 1 / srcToViewRatio
Logger.error("viewToSrcRatio: \(viewToSrcRatio)")
let gestureTranslation = let gestureTranslation =
sender.translation(in: sender.view) sender.translation(in: sender.view)
Logger.error("gestureTranslation: \(gestureTranslation)") // Update translation.
srcTranslation = CGPoint(x:srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio, srcTranslation = CGPoint(x:srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio,
y:srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio) y:srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio)
break break
@ -423,6 +400,16 @@ class CropScaleImageViewController: OWSViewController {
updateImageLayout() updateImageLayout()
} }
func handleDoubleTap(sender: UIPanGestureRecognizer) {
if (sender.state == .recognized) {
if imageScale > 1.5 {
imageScale = kMinImageScale
}
}
updateImageLayout()
}
private func createButtonRow(contentView: UIView) { private func createButtonRow(contentView: UIView) {
let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40) let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40)
let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40) let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40)
@ -471,7 +458,7 @@ class CropScaleImageViewController: OWSViewController {
func donePressed(sender: UIButton) { func donePressed(sender: UIButton) {
let successCompletion = self.successCompletion let successCompletion = self.successCompletion
dismiss(animated: true, completion: { dismiss(animated: true, completion: {
// TODO // TODO:
let dstImage = self.srcImage let dstImage = self.srcImage
successCompletion?(dstImage) successCompletion?(dstImage)
}) })