Skip to content

Commit d16ff34

Browse files
committed
fix(zoomimage,ios): better zoomimage component
1 parent 48c8d6b commit d16ff34

File tree

4 files changed

+348
-3
lines changed

4 files changed

+348
-3
lines changed
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
//
2+
// ImageScrollView.swift
3+
// Beauty
4+
//
5+
// Created by Nguyen Cong Huy on 1/19/16.
6+
// Copyright © 2016 Nguyen Cong Huy. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate {
12+
func imageScrollViewDidChangeOrientation(imageScrollView: ImageScrollView)
13+
}
14+
15+
open class ImageScrollView: UIScrollView {
16+
17+
// @objc public enum ScaleMode: Int {
18+
// case aspectFill
19+
// case aspectFit
20+
// case widthFill
21+
// case heightFill
22+
// }
23+
24+
@objc public enum Offset: Int {
25+
case begining
26+
case center
27+
}
28+
29+
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
30+
31+
// @objc open var imageContentMode: ScaleMode = .widthFill
32+
@objc open var initialOffset: Offset = .begining
33+
34+
private var _zoomView: UIImageView? = nil
35+
@objc public var zoomView: UIImageView? {
36+
get {
37+
return self._zoomView
38+
}
39+
set {
40+
if (_zoomView != nil) {
41+
_zoomView?.removeFromSuperview()
42+
}
43+
self._zoomView = newValue
44+
_zoomView!.isUserInteractionEnabled = true
45+
addSubview(_zoomView!)
46+
47+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ImageScrollView.doubleTapGestureRecognizer(_:)))
48+
tapGesture.numberOfTapsRequired = 2
49+
zoomView!.addGestureRecognizer(tapGesture)
50+
}
51+
}
52+
53+
@objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate?
54+
55+
var imageSize: CGSize = CGSize.zero
56+
private var pointToCenterAfterResize: CGPoint = CGPoint.zero
57+
private var scaleToRestoreAfterResize: CGFloat = 1.0
58+
open var maxScaleFromMinScale: CGFloat = 3.0
59+
60+
override open var frame: CGRect {
61+
willSet {
62+
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
63+
prepareToResize()
64+
}
65+
}
66+
67+
didSet {
68+
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
69+
recoverFromResizing()
70+
}
71+
}
72+
}
73+
74+
override public init(frame: CGRect) {
75+
super.init(frame: frame)
76+
77+
initialize()
78+
}
79+
80+
required public init?(coder aDecoder: NSCoder) {
81+
super.init(coder: aDecoder)
82+
83+
initialize()
84+
}
85+
86+
deinit {
87+
NotificationCenter.default.removeObserver(self)
88+
}
89+
90+
private func initialize() {
91+
showsVerticalScrollIndicator = false
92+
showsHorizontalScrollIndicator = false
93+
bouncesZoom = true
94+
decelerationRate = UIScrollView.DecelerationRate.fast
95+
delegate = self
96+
97+
NotificationCenter.default.addObserver(self, selector: #selector(ImageScrollView.changeOrientationNotification), name: UIDevice.orientationDidChangeNotification, object: nil)
98+
}
99+
100+
@objc public func adjustFrameToCenter() {
101+
102+
guard let unwrappedZoomView = _zoomView else {
103+
return
104+
}
105+
106+
var frameToCenter = unwrappedZoomView.frame
107+
108+
// center horizontally
109+
if frameToCenter.size.width < bounds.width {
110+
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
111+
}
112+
else {
113+
frameToCenter.origin.x = 0
114+
}
115+
116+
// center vertically
117+
if frameToCenter.size.height < bounds.height {
118+
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
119+
}
120+
else {
121+
frameToCenter.origin.y = 0
122+
}
123+
124+
unwrappedZoomView.frame = frameToCenter
125+
}
126+
127+
private func prepareToResize() {
128+
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
129+
pointToCenterAfterResize = convert(boundsCenter, to: _zoomView)
130+
131+
scaleToRestoreAfterResize = zoomScale
132+
133+
// If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
134+
// allowable scale when the scale is restored.
135+
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
136+
scaleToRestoreAfterResize = 0
137+
}
138+
}
139+
140+
private func recoverFromResizing() {
141+
setMaxMinZoomScalesForCurrentBounds()
142+
143+
// restore zoom scale, first making sure it is within the allowable range.
144+
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
145+
zoomScale = min(maximumZoomScale, maxZoomScale)
146+
147+
// restore center point, first making sure it is within the allowable range.
148+
149+
// convert our desired center point back to our own coordinate space
150+
let boundsCenter = convert(pointToCenterAfterResize, to: _zoomView)
151+
152+
// calculate the content offset that would yield that center point
153+
var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0)
154+
155+
// restore offset, adjusted to be within the allowable range
156+
let maxOffset = maximumContentOffset()
157+
let minOffset = minimumContentOffset()
158+
159+
var realMaxOffset = min(maxOffset.x, offset.x)
160+
offset.x = max(minOffset.x, realMaxOffset)
161+
162+
realMaxOffset = min(maxOffset.y, offset.y)
163+
offset.y = max(minOffset.y, realMaxOffset)
164+
165+
contentOffset = offset
166+
}
167+
168+
private func maximumContentOffset() -> CGPoint {
169+
return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height)
170+
}
171+
172+
private func minimumContentOffset() -> CGPoint {
173+
return CGPoint.zero
174+
}
175+
176+
177+
@objc func updateForImage(_ size: CGSize) {
178+
imageSize = size
179+
contentSize = imageSize
180+
// setMaxMinZoomScalesForCurrentBounds()
181+
// zoomScale = minimumZoomScale
182+
183+
switch initialOffset {
184+
case .begining:
185+
contentOffset = CGPoint.zero
186+
case .center:
187+
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
188+
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
189+
190+
switch _zoomView?.contentMode {
191+
case .scaleAspectFit:
192+
contentOffset = CGPoint.zero
193+
case .scaleAspectFill:
194+
contentOffset = CGPoint(x: xOffset, y: yOffset)
195+
case .scaleToFill:
196+
contentOffset = CGPoint(x: xOffset, y: yOffset)
197+
default:
198+
contentOffset = CGPoint.zero
199+
}
200+
}
201+
}
202+
203+
private func setMaxMinZoomScalesForCurrentBounds() {
204+
// calculate min/max zoomscale
205+
// let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise
206+
// let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise
207+
//
208+
// var minScale: CGFloat = 1
209+
//
210+
// switch imageContentMode {
211+
// case .aspectFill:
212+
// minScale = max(xScale, yScale)
213+
// case .aspectFit:
214+
// minScale = min(xScale, yScale)
215+
// case .widthFill:
216+
// minScale = xScale
217+
// case .heightFill:
218+
// minScale = yScale
219+
// }
220+
//
221+
//
222+
// let maxScale = maxScaleFromMinScale*minScale
223+
//
224+
// // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
225+
// if minScale > maxScale {
226+
// minScale = maxScale
227+
// }
228+
229+
// maximumZoomScale = maxScale
230+
// minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
231+
}
232+
233+
// MARK: - Gesture
234+
235+
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
236+
// zoom out if it bigger than the scale factor after double-tap scaling. Else, zoom in
237+
if zoomScale >= minimumZoomScale * ImageScrollView.kZoomInFactorFromMinWhenDoubleTap - 0.01 {
238+
setZoomScale(minimumZoomScale, animated: true)
239+
} else {
240+
let center = gestureRecognizer.location(in: gestureRecognizer.view)
241+
let zoomRect = zoomRectForScale(ImageScrollView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center)
242+
zoom(to: zoomRect, animated: true)
243+
}
244+
}
245+
246+
private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
247+
var zoomRect = CGRect.zero
248+
249+
// the zoom rect is in the content view's coordinates.
250+
// at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
251+
// as the zoom scale decreases, so more content is visible, the size of the rect grows.
252+
zoomRect.size.height = frame.size.height / scale
253+
zoomRect.size.width = frame.size.width / scale
254+
255+
// choose an origin so as to get the right center.
256+
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
257+
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
258+
259+
return zoomRect
260+
}
261+
262+
// MARK: - Actions
263+
264+
@objc func changeOrientationNotification() {
265+
// A weird bug that frames are not update right after orientation changed. Need delay a little bit with async.
266+
DispatchQueue.main.async {
267+
self.updateForImage(self.imageSize)
268+
self.imageScrollViewDelegate?.imageScrollViewDidChangeOrientation(imageScrollView: self)
269+
}
270+
}
271+
}
272+
273+
extension ImageScrollView: UIScrollViewDelegate {
274+
275+
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
276+
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
277+
}
278+
279+
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
280+
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
281+
}
282+
283+
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
284+
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
285+
}
286+
287+
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
288+
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
289+
}
290+
291+
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
292+
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
293+
}
294+
295+
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
296+
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
297+
}
298+
299+
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
300+
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
301+
}
302+
303+
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
304+
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
305+
}
306+
307+
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
308+
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
309+
}
310+
311+
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
312+
return false
313+
}
314+
315+
@available(iOS 11.0, *)
316+
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
317+
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
318+
}
319+
320+
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
321+
return _zoomView
322+
}
323+
324+
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
325+
adjustFrameToCenter()
326+
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)
327+
}
328+
329+
}

src/zoomimage/index.ios.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,28 @@ export class UIZoomImgScrollViewDelegateImpl extends NSObject implements UIScrol
1818
}
1919

2020
export class ZoomImg extends ZoomImageBase {
21-
nativeViewProtected: UIScrollView;
21+
nativeViewProtected: ImageScrollView;
2222
_image: SDAnimatedImageView | UIImageView;
2323
private delegate: UIZoomImgScrollViewDelegateImpl;
2424
public createNativeView() {
2525
this._image = super.createNativeView() as any;
2626
this._image.clipsToBounds = true;
27-
const nativeView = UIScrollView.new();
28-
nativeView.addSubview(this._image);
27+
28+
const nativeView = ImageScrollView.new();
29+
nativeView.zoomView = this._image;
30+
// const nativeView = UIScrollView.new();
31+
// nativeView.addSubview(this._image);
2932
nativeView.zoomScale = this.zoomScale;
3033
nativeView.minimumZoomScale = this.minZoom;
3134
nativeView.maximumZoomScale = this.maxZoom;
3235
return nativeView;
3336
}
37+
38+
_setNativeImage(nativeImage, animated = true) {
39+
//@ts-ignore
40+
super._setNativeImage(nativeImage, animated);
41+
this.nativeViewProtected.updateForImage(nativeImage.size);
42+
}
3443
get nativeImageViewProtected() {
3544
return this._image;
3645
}

src/zoomimage/references.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/// <reference path="../image/references.d.ts" />
22
/// <reference path="./typings/android.d.ts" />
3+
/// <reference path="./typings/ios.d.ts" />

src/zoomimage/typings/ios.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare class ImageScrollView extends UIScrollView {
2+
static new(): ImageScrollView
3+
zoomView: UIImageView | SDAnimatedImageView
4+
5+
updateForImage(size: CGSize)
6+
}

0 commit comments

Comments
 (0)