UIScrollView:在不更改 contentView 的情况下放大时将滚动锁定到垂直轴
UIScrollView: Lock scrolling to vertical axis when zoomed in without changing the contentView
根据缩放比例将 UIScrollView
中的滚动动态锁定到垂直轴的最佳方法是什么?
我想允许在缩小时向任何方向滚动大 canvas (scrollView.zoomScale < 1.0
)
但在放大时完全防止水平滚动 (scrollView.zoomScale == 1.0
)。
这里的挑战是 UIScrollView
似乎没有内置设置来限制滚动到一个方向,如果 contentView
在两个方向上都大于视口。我想使用相同的大 contentView
但在放大时不允许水平滚动。
(我知道 scrollView.isDirectionalLockEnabled
,但这不是我需要的:它只检查用户的平移手势是否有主导的滚动方向,然后动态锁定滚动到任一方向。)
谢谢!
如果我正确理解您的目标...
- 您有一个 大于 滚动视图的“contentView”
- 如果缩放比例为 1.0,则只允许垂直滚动
- 如果缩放比例小于 1.0,则允许垂直和水平滚动
因此,如果我们有一个大小为 388 x 661
的滚动视图框架和一个大小为 2100 x 2100
的“contentView”,我们从缩放比例 1.0 开始(亮绿色是滚动视图框架):
并且只允许垂直滚动。
如果用户缩小到 0.8 比例:
允许垂直和水平滚动。
如果用户然后放大回 1.0 比例:
我们回到仅垂直滚动。
您可以通过使您的控制器符合 UIScrollViewDelegate
、将自己指定为 scrollView 的委托、添加“last scrollView content offset X”变量,然后实现 scrollViewDidScroll()
:
var lastOffsetX: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
这是您可以试用的完整示例:
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
return v
}()
// we'll use this to track the current content X offset
var lastOffsetX: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
scrollView.addSubview(contentView)
view.addSubview(scrollView)
view.addSubview(infoLabel)
[contentView, scrollView, infoLabel].forEach {
[=11=].translatesAutoresizingMaskIntoConstraints = false
}
// let's add a 8 x 8 "grid" of labels to the content view
let outerVerticalStack = UIStackView()
outerVerticalStack.axis = .vertical
outerVerticalStack.spacing = 20
outerVerticalStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(outerVerticalStack)
var j: Int = 1
for _ in 1...8 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = 20
rowStack.distribution = .fillEqually
for _ in 1...8 {
let v = UILabel()
v.font = .systemFont(ofSize: 48.0, weight: .regular)
v.text = "\(j)"
v.textAlignment = .center
v.backgroundColor = .green
v.widthAnchor.constraint(equalToConstant: 240.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
rowStack.addArrangedSubview(v)
j += 1
}
outerVerticalStack.addArrangedSubview(rowStack)
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -120.0),
contentView.topAnchor.constraint(equalTo: contentG.topAnchor),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
outerVerticalStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
outerVerticalStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
outerVerticalStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
outerVerticalStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
// put the info label below the scroll view
infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
])
// we'll update min zoom in viewDidAppear
// (after all views have been laid out)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 1.0
// we need to disable zoom bouncing, or
// we get really bad positioning effect
// when zooming in past 1.0
scrollView.bouncesZoom = false
// assign the delegate
scrollView.delegate = self
// update the info label
updateInfo()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
print(contentView.frame.size)
let xScale = scrollView.frame.width / contentView.frame.width
let yScale = scrollView.frame.height / contentView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 && !scrollView.isZooming {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateInfo()
}
func updateInfo() {
let s = String(format: "%0.4f", scrollView.zoomScale)
infoLabel.text = "Zoom Scale: \(s)"
}
}
根据缩放比例将 UIScrollView
中的滚动动态锁定到垂直轴的最佳方法是什么?
我想允许在缩小时向任何方向滚动大 canvas (scrollView.zoomScale < 1.0
)
但在放大时完全防止水平滚动 (scrollView.zoomScale == 1.0
)。
这里的挑战是 UIScrollView
似乎没有内置设置来限制滚动到一个方向,如果 contentView
在两个方向上都大于视口。我想使用相同的大 contentView
但在放大时不允许水平滚动。
(我知道 scrollView.isDirectionalLockEnabled
,但这不是我需要的:它只检查用户的平移手势是否有主导的滚动方向,然后动态锁定滚动到任一方向。)
谢谢!
如果我正确理解您的目标...
- 您有一个 大于 滚动视图的“contentView”
- 如果缩放比例为 1.0,则只允许垂直滚动
- 如果缩放比例小于 1.0,则允许垂直和水平滚动
因此,如果我们有一个大小为 388 x 661
的滚动视图框架和一个大小为 2100 x 2100
的“contentView”,我们从缩放比例 1.0 开始(亮绿色是滚动视图框架):
并且只允许垂直滚动。
如果用户缩小到 0.8 比例:
允许垂直和水平滚动。
如果用户然后放大回 1.0 比例:
我们回到仅垂直滚动。
您可以通过使您的控制器符合 UIScrollViewDelegate
、将自己指定为 scrollView 的委托、添加“last scrollView content offset X”变量,然后实现 scrollViewDidScroll()
:
var lastOffsetX: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
这是您可以试用的完整示例:
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
return v
}()
// we'll use this to track the current content X offset
var lastOffsetX: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
scrollView.addSubview(contentView)
view.addSubview(scrollView)
view.addSubview(infoLabel)
[contentView, scrollView, infoLabel].forEach {
[=11=].translatesAutoresizingMaskIntoConstraints = false
}
// let's add a 8 x 8 "grid" of labels to the content view
let outerVerticalStack = UIStackView()
outerVerticalStack.axis = .vertical
outerVerticalStack.spacing = 20
outerVerticalStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(outerVerticalStack)
var j: Int = 1
for _ in 1...8 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = 20
rowStack.distribution = .fillEqually
for _ in 1...8 {
let v = UILabel()
v.font = .systemFont(ofSize: 48.0, weight: .regular)
v.text = "\(j)"
v.textAlignment = .center
v.backgroundColor = .green
v.widthAnchor.constraint(equalToConstant: 240.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
rowStack.addArrangedSubview(v)
j += 1
}
outerVerticalStack.addArrangedSubview(rowStack)
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -120.0),
contentView.topAnchor.constraint(equalTo: contentG.topAnchor),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
outerVerticalStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
outerVerticalStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
outerVerticalStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
outerVerticalStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
// put the info label below the scroll view
infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
])
// we'll update min zoom in viewDidAppear
// (after all views have been laid out)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 1.0
// we need to disable zoom bouncing, or
// we get really bad positioning effect
// when zooming in past 1.0
scrollView.bouncesZoom = false
// assign the delegate
scrollView.delegate = self
// update the info label
updateInfo()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
print(contentView.frame.size)
let xScale = scrollView.frame.width / contentView.frame.width
let yScale = scrollView.frame.height / contentView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 && !scrollView.isZooming {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateInfo()
}
func updateInfo() {
let s = String(format: "%0.4f", scrollView.zoomScale)
infoLabel.text = "Zoom Scale: \(s)"
}
}