由捏合和平移手势识别器同时触发的交互式 ViewController 转换
Interactive ViewController transition triggered by pinch and pan gesture recognisers simultaneously
我有两个 viewControllers:
ViewController1
复杂的子视图控制器堆栈,中间某处有一个 imageView
ViewController2
嵌入了 imageView 的 scrollView
我想要实现的是两个 viewController 之间的过渡,它是通过从 viewController 1 捏住 imageView 触发的,导致它放大并切换到 viewController 2. 当过渡结束时,imageView 应该被放大到在捏合手势触发的过渡期间被放大的程度。
同时我想支持在执行缩放过渡时平移图像,这样就像缩放一样,最终状态的图像将转换到它被平移到的地方。
到目前为止,我已经尝试了 Hero transitions pod 和我自己编写的自定义 viewController transitions。英雄转换的问题是图像没有正确地捕捉到第二个 viewController 中的结束状态。我在使用自定义 viewController 过渡时遇到的问题是我无法同时进行缩放和平移。
有没有人知道如何在 Swift 中实现它?非常感谢帮助。
看来 UIView.animate(withDuration: animations: completion:)
应该对您有所帮助;例如,在 animations
块中,您可以设置新的图像帧,在 completion:
中 - 显示第二个视图控制器(无动画);
问题可以分为两部分:
- 如何在
imageView
上使用平移手势实现缩放和拖动
- 如何呈现一个视图控制器,其子视图之一(vc2 中的
imageView
)与呈现视图控制器中的子视图(vc1 中的imageView
)定位相同
双指缩放: 使用 UIScrollView
更容易实现双指缩放,因为它开箱即用,无需添加手势识别器。创建一个 scrollView
并添加您想要缩放的视图作为其子视图 (scrollView.addSubview(imageView)
)。不要忘记添加 scrollView
本身 (view.addSubview(scrollView)
)。
配置 scrollView's
最小和最大缩放比例:scrollView.minimumZoomScale, scrollView.maximumZoomScale
。为 scrollView.delegate
设置委托并实现 UIScrollViewDelegate
:
func viewForZooming(in scrollView: UIScrollView) -> UIView?
在这种情况下 return 你的 imageView
和
也符合UIGestureRecognizerDelegate
并执行:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
哪个应该 return 正确。这是让我们让平移手势识别器与内部捏合手势识别器一起工作的关键。
平移手势拖动:只需创建一个带有目标的平移手势识别器并将其添加到您的滚动视图中scrollView.addGestureRecognizer(pan)
。
处理手势: 捏合缩放在此阶段运行良好,除非您想在捏合结束时显示第二个视图控制器。再实现一种 UIScrollViewDelegate
方法,以便在缩放结束时收到通知:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
并调用您提供详细信息视图控制器的方法presentDetail()
,我们稍后会实现它。
下一步是处理平移手势,我将让代码自行解释:
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
实现在平移位置周围移动 imageView
,并在手势结束时调用 presentDetail()
。
在我们实现 presentDetail()
之前,前往详细视图控制器并添加属性以保存 imageViewFrame
和 image
本身。现在在 vc1 中,我们这样实现 presentDetail()
:
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
// Note that we do not need the animation.
present(detail, animated: false, completion: nil)
}
在您的 DetailViewController
中,确保设置 imageViewFrame
和图像,例如viewDidLoad
一切就绪。
完整的工作示例:
class ViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let imageView: UIImageView = UIImageView()
let scrollView: UIScrollView = UIScrollView()
lazy var pan: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = // set your image
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.frame = view.frame
let w = view.bounds.width - 30 // padding of 15 on each side
imageView.frame = CGRect(x: 0, y: 0, width: w, height: w)
imageView.center = scrollView.center
scrollView.addGestureRecognizer(pan)
}
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
presentDetail()
}
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: Private
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
present(detail, animated: false, completion: nil)
}
}
class DetailViewController: UIViewController {
let imageView: UIImageView = UIImageView()
var imageViewFrame: CGRect!
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = imageViewFrame
imageView.image = image
view.addSubview(imageView)
view.addSubview(backButton)
}
lazy var backButton: UIButton = {
let button: UIButton = UIButton(frame: CGRect(x: 10, y: 30, width: 60, height: 30))
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
button.setTitle("back", for: .normal)
return button
}()
@objc func back(_ sender: UIButton) {
dismiss(animated: false, completion: nil)
}
}
我有两个 viewControllers:
ViewController1
复杂的子视图控制器堆栈,中间某处有一个 imageView
ViewController2
嵌入了 imageView 的 scrollView
我想要实现的是两个 viewController 之间的过渡,它是通过从 viewController 1 捏住 imageView 触发的,导致它放大并切换到 viewController 2. 当过渡结束时,imageView 应该被放大到在捏合手势触发的过渡期间被放大的程度。
同时我想支持在执行缩放过渡时平移图像,这样就像缩放一样,最终状态的图像将转换到它被平移到的地方。
到目前为止,我已经尝试了 Hero transitions pod 和我自己编写的自定义 viewController transitions。英雄转换的问题是图像没有正确地捕捉到第二个 viewController 中的结束状态。我在使用自定义 viewController 过渡时遇到的问题是我无法同时进行缩放和平移。
有没有人知道如何在 Swift 中实现它?非常感谢帮助。
看来 UIView.animate(withDuration: animations: completion:)
应该对您有所帮助;例如,在 animations
块中,您可以设置新的图像帧,在 completion:
中 - 显示第二个视图控制器(无动画);
问题可以分为两部分:
- 如何在
imageView
上使用平移手势实现缩放和拖动
- 如何呈现一个视图控制器,其子视图之一(vc2 中的
imageView
)与呈现视图控制器中的子视图(vc1 中的imageView
)定位相同
双指缩放: 使用 UIScrollView
更容易实现双指缩放,因为它开箱即用,无需添加手势识别器。创建一个 scrollView
并添加您想要缩放的视图作为其子视图 (scrollView.addSubview(imageView)
)。不要忘记添加 scrollView
本身 (view.addSubview(scrollView)
)。
配置 scrollView's
最小和最大缩放比例:scrollView.minimumZoomScale, scrollView.maximumZoomScale
。为 scrollView.delegate
设置委托并实现 UIScrollViewDelegate
:
func viewForZooming(in scrollView: UIScrollView) -> UIView?
在这种情况下 return 你的 imageView
和
也符合UIGestureRecognizerDelegate
并执行:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
哪个应该 return 正确。这是让我们让平移手势识别器与内部捏合手势识别器一起工作的关键。
平移手势拖动:只需创建一个带有目标的平移手势识别器并将其添加到您的滚动视图中scrollView.addGestureRecognizer(pan)
。
处理手势: 捏合缩放在此阶段运行良好,除非您想在捏合结束时显示第二个视图控制器。再实现一种 UIScrollViewDelegate
方法,以便在缩放结束时收到通知:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
并调用您提供详细信息视图控制器的方法presentDetail()
,我们稍后会实现它。
下一步是处理平移手势,我将让代码自行解释:
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
实现在平移位置周围移动 imageView
,并在手势结束时调用 presentDetail()
。
在我们实现 presentDetail()
之前,前往详细视图控制器并添加属性以保存 imageViewFrame
和 image
本身。现在在 vc1 中,我们这样实现 presentDetail()
:
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
// Note that we do not need the animation.
present(detail, animated: false, completion: nil)
}
在您的 DetailViewController
中,确保设置 imageViewFrame
和图像,例如viewDidLoad
一切就绪。
完整的工作示例:
class ViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let imageView: UIImageView = UIImageView()
let scrollView: UIScrollView = UIScrollView()
lazy var pan: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = // set your image
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.frame = view.frame
let w = view.bounds.width - 30 // padding of 15 on each side
imageView.frame = CGRect(x: 0, y: 0, width: w, height: w)
imageView.center = scrollView.center
scrollView.addGestureRecognizer(pan)
}
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
@objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
presentDetail()
}
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: Private
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
present(detail, animated: false, completion: nil)
}
}
class DetailViewController: UIViewController {
let imageView: UIImageView = UIImageView()
var imageViewFrame: CGRect!
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = imageViewFrame
imageView.image = image
view.addSubview(imageView)
view.addSubview(backButton)
}
lazy var backButton: UIButton = {
let button: UIButton = UIButton(frame: CGRect(x: 10, y: 30, width: 60, height: 30))
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
button.setTitle("back", for: .normal)
return button
}()
@objc func back(_ sender: UIButton) {
dismiss(animated: false, completion: nil)
}
}