ios - 在 ScrollView 内缩放和滚动 ImageView
问题描述
屏幕有一个目标视图居中。我需要更正 ScrollView:
- 缩放后 - 如果 imageView 到屏幕边缘有距离,则图像应水平/垂直居中
- 缩放后应该可以滚动ScrollView,这样imageView的任何部分都可以进入到aimView下
- 打开屏幕时,设置了缩放以使图像占据最大可能区域
现在看起来像这样:
class ScrollViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
var image: UIImage!
var aimView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.delegate = self
setupScrollView()
image = #imageLiteral(resourceName: "apple")
imageView = UIImageView(image: image)
setupImageView()
aimView = UIView()
setupAimView()
}
func setupScrollView() {
scrollView.backgroundColor = .yellow
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
scrollView.maximumZoomScale = 10
scrollView.minimumZoomScale = 0.1
scrollView.zoomScale = 1.0
}
func setupImageView() {
imageView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: image.size.width),
imageView.heightAnchor.constraint(equalToConstant: image.size.height),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
])
}
func setupAimView() {
aimView.translatesAutoresizingMaskIntoConstraints = false
aimView.backgroundColor = .green
aimView.alpha = 0.7
aimView.isUserInteractionEnabled = false
view.addSubview(aimView)
NSLayoutConstraint.activate([
aimView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
aimView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
aimView.widthAnchor.constraint(equalTo: aimView.heightAnchor),
aimView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
// MARK: - UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
imageView
}
}
解决方案
有几种方法可以解决这个问题……一种方法:
- 使用 a
UIView
作为滚动视图的“内容” - 将所有 4 面的“内容”视图约束到滚动视图的内容布局指南
- 将 imageView 嵌入到该“内容”视图中
- 约束 imageView 的顶部和前导,因此当内容视图滚动到时,它将出现在“目标”视图的右下角
0,0
- 约束 imageView 的 Trailing 和 Bottom,以便当内容视图滚动到其最大 x 和 y 时,它将出现在“目标”视图的左上角
给你一个思路...
虚线矩形是滚动视图框架。绿色矩形是“目标”视图。黄色矩形是“内容”视图。
我们将无法使用滚动视图的内置缩放功能,因为它还会“缩放”图像视图边缘和内容视图之间的空间。相反,我们可以UIPinchGestureRecognizer
在滚动视图中添加一个。当用户捏合缩放时,我们将获取手势的.scale
值并使用它来更改 imageView 的宽度和高度常量。由于我们已将该 imageView 限制为内容视图,因此内容视图将增长/缩小而不改变两侧的间距。
这是一个示例实现(它需要一个名为“apple”的资产图像):
class PinchScroller: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
self.addGestureRecognizer(pinchGesture)
}
var scaleStartCallback: (()->())?
var scaleChangeCallback: ((CGFloat)->())?
// assuming minimum scale of 1.0
var minScale: CGFloat = 1.0
// assuming maximum scale of 5.0
var maxScale: CGFloat = 5.0
private var curScale: CGFloat = 1.0
@objc private func handlePinchGesture(_ gesture:UIPinchGestureRecognizer) {
if gesture.state == .began {
// inform controller scaling started
scaleStartCallback?()
}
if gesture.state == .changed {
// inform controller the scale changed
let val: CGFloat = gesture.scale - 1.0
let scale = min(maxScale, max(minScale, curScale + val))
scaleChangeCallback?(scale)
}
if gesture.state == .ended {
// update current scale value
let val: CGFloat = gesture.scale - 1.0
curScale = min(maxScale, max(minScale, curScale + val))
}
}
}
class AimViewController: UIViewController {
var scrollView: PinchScroller!
var imageView: UIImageView!
var contentView: UIView!
var aimView: UIView!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewWidthConstraint: NSLayoutConstraint!
var imageViewHeightConstraint: NSLayoutConstraint!
var imageViewWidthFactor: CGFloat = 1.0
var imageViewHeightFactor: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
// make sure we can load the image
guard let img = UIImage(named: "apple") else {
fatalError("Could not load image!!!")
}
scrollView = PinchScroller()
imageView = UIImageView()
contentView = UIView()
aimView = UIView()
[scrollView, imageView, contentView, aimView].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(imageView)
scrollView.addSubview(aimView)
// init image view width constraint
imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
// to handle non-1:1 ratio images
if img.size.width > img.size.height {
imageViewHeightFactor = img.size.height / img.size.width
} else {
imageViewWidthFactor = img.size.width / img.size.height
}
// init image view Top / Leading / Trailing / Bottom constraints
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to all 4 sides of safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain "content" view to all 4 sides of scroll view's content layout guide
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// activate these constraints
imageViewTopConstraint,
imageViewLeadingConstraint,
imageViewTrailingConstraint,
imageViewBottomConstraint,
imageViewWidthConstraint,
imageViewHeightConstraint,
// "aim" view: 200x200, centered in scroll view frame
aimView.widthAnchor.constraint(equalToConstant: 200.0),
aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
])
// set the image
imageView.image = img
// disable interaction for "aim" view
aimView.isUserInteractionEnabled = false
// aim view translucent background color
aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
// probably don't want scroll bouncing
scrollView.bounces = false
// set the scaling callback closures
scrollView.scaleStartCallback = { [weak self] in
guard let self = self else {
return
}
self.didStartScale()
}
scrollView.scaleChangeCallback = { [weak self] v in
guard let self = self else {
return
}
self.didChangeScale(v)
}
contentView.backgroundColor = .yellow
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set constraint constants here, after all view have been initialized
let aimSize: CGSize = aimView.frame.size
imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
let w = (scrollView.frame.width - aimSize.width) * 0.5 + aimSize.width
let h = (scrollView.frame.height - aimSize.height) * 0.5 + aimSize.height
imageViewTopConstraint.constant = h
imageViewLeadingConstraint.constant = w
imageViewTrailingConstraint.constant = -w
imageViewBottomConstraint.constant = -h
DispatchQueue.main.async {
// center the content in the scroll view
let xOffset = aimSize.width - ((aimSize.width - self.imageView.frame.width) * 0.5)
let yOffset = aimSize.height - ((aimSize.height - self.imageView.frame.height) * 0.5)
self.scrollView.contentOffset = CGPoint(x: xOffset, y: yOffset)
}
}
private var startContentOffset: CGPoint = .zero
private var startSize: CGSize = .zero
func didStartScale() -> Void {
startContentOffset = scrollView.contentOffset
startSize = imageView.frame.size
}
func didChangeScale(_ scale: CGFloat) -> Void {
// all sizing is based on the "aim" view
let aimSize: CGSize = aimView.frame.size
// starting scroll offset
var cOffset = startContentOffset
// starting image view width and height
let w = startSize.width
let h = startSize.height
// new image view width and height
let newW = aimSize.width * scale * imageViewWidthFactor
let newH = aimSize.height * scale * imageViewHeightFactor
// change image view width based on pinch scaling
imageViewWidthConstraint.constant = newW
imageViewHeightConstraint.constant = newH
// adjust content offset so image view zooms from its center
let xDiff = (newW - w) * 0.5
let yDiff = (newH - h) * 0.5
cOffset.x += xDiff
cOffset.y += yDiff
// update scroll offset
scrollView.contentOffset = cOffset
}
}
试试看。如果它接近你的目标,那么你就有了一个开始的地方。
编辑
在玩了更多之后scrollView.contentInset
,这是一个更简单的方法。它使用UIScrollView
具有缩放/平移功能的标准,并且不需要任何额外的“缩放”计算或约束更改:
class AimInsetsViewController: UIViewController {
var scrollView: UIScrollView!
var imageView: UIImageView!
var aimView: UIView!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewWidthConstraint: NSLayoutConstraint!
var imageViewHeightConstraint: NSLayoutConstraint!
var imageViewWidthFactor: CGFloat = 1.0
var imageViewHeightFactor: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
var imageName: String = ""
imageName = "apple"
// testing different sized images
//imageName = "apple228x346"
//imageName = "zoom640x360"
// make sure we can load the image
guard let img = UIImage(named: imageName) else {
fatalError("Could not load image!!!")
}
scrollView = UIScrollView()
imageView = UIImageView()
aimView = UIView()
[scrollView, imageView, aimView].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(scrollView)
scrollView.addSubview(imageView)
scrollView.addSubview(aimView)
// init image view width constraint
imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
// to handle non-1:1 ratio images
if img.size.width > img.size.height {
imageViewHeightFactor = img.size.height / img.size.width
} else {
imageViewWidthFactor = img.size.width / img.size.height
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to all 4 sides of safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain "content" view to all 4 sides of scroll view's content layout guide
imageView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
imageView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
imageView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
imageView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
imageViewWidthConstraint,
imageViewHeightConstraint,
// "aim" view: 200x200, centered in scroll view frame
aimView.widthAnchor.constraint(equalToConstant: 200.0),
aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
])
// set the image
imageView.image = img
// disable interaction for "aim" view
aimView.isUserInteractionEnabled = false
// aim view translucent background color
aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
// probably don't want scroll bouncing
scrollView.bounces = false
// delegate
scrollView.delegate = self
// set max zoom scale
scrollView.maximumZoomScale = 10.0
// set min zoom scale to less than 1.0
// if you want to allow image view smaller than aim view
scrollView.minimumZoomScale = 1.0
// scroll view background
scrollView.backgroundColor = .yellow
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set constraint constants, scroll view insets and initial content offset here,
// after all view have been initialized
let aimSize: CGSize = aimView.frame.size
// aspect-fit image view to aim view
imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
// set content insets
let f = aimView.frame
scrollView.contentInset = .init(top: f.origin.y + f.height,
left: f.origin.x + f.width,
bottom: f.origin.y + f.height,
right: f.origin.x + f.width)
// center image view in aim view
var c = scrollView.contentOffset
c.x -= (aimSize.width - imageViewWidthConstraint.constant) * 0.5
c.y -= (aimSize.height - imageViewHeightConstraint.constant) * 0.5
scrollView.contentOffset = c
}
}
extension AimInsetsViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
我认为这将更接近您的目标。
推荐阅读
- javascript - 将 blob url 转换为 FormData() 的上传文件并通过 AJAX 将其发送到 PHP 文件
- .net-core - 如何在 ASP .Net Core 3 中使用 UseRequestLocalization?
- swift - Firebase 项目在数据库更改的集合视图中多次返回
- json - 我有 sql 数据框如何将其转换为所需的 json 格式
- awk - 检查文件中的多行内容
- c# - 带有异步 Task.Run 方法的死锁,带有来自 Synchronus 方法的 Wait 和超时
- c# - 在图像的特定位置显示对象
- java - 自动装配注释 bean 在侦听器类中为空
- c++ - Changed value through set function but; value not changing when called
- r - 如果在闪亮数据表的行中的任何位置存在条件,是否可以获得 formatStyle